Compare commits

..

3 Commits

14 changed files with 203 additions and 25 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.24) cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.31 LANGUAGES CXX RC) project(KSwitcher VERSION 0.6.34 LANGUAGES CXX RC)
if(NOT WIN32) if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs") message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif() endif()
+12 -3
View File
@@ -16,10 +16,15 @@ Run `build/KSwitcher.exe`, or extract `dist/KSwitcher-win-x64.zip` and run the e
The build also refreshes the release copy in `build/release/`. Exit KSwitcher before rebuilding. The build also refreshes the release copy in `build/release/`. Exit KSwitcher before rebuilding.
The application lives in the notification area. Right-click its icon for Settings, Snippets and history. The application lives in the notification area. Right-click its icon for Settings, Snippets and history.
Double-click the tray icon to open Settings, the bold default menu item. Double-click the tray icon to open Settings, the bold default menu item.
Closing Settings with unsaved changes (including accepted auto-correction or OCR options)
asks whether to save, discard changes, or keep editing. Unchanged settings close without a prompt.
If saving fails, the window stays open with your edits intact.
Menus support access keys: press the underlined letter while the menu is open (for example, Menus support access keys: press the underlined letter while the menu is open (for example,
S for Settings or X for Exit in the tray menu). In forms, press Alt+the underlined letter S for Settings or X for Exit in the tray menu). In forms, press Alt+the underlined letter
to activate a button, toggle a checkbox or focus a labeled field. Press Alt to reveal to activate a button, toggle a checkbox or focus a labeled field. Dialog underline visibility
the underlines when Windows hides them. Shortcut-capture fields still record Alt combinations; still follows Windows preferences; press Alt in a dialog to reveal them.
From v0.6.33, tray and history menus show access letters in the right-hand column.
Press the menu letter without Alt: Alt dismisses an open menu. Shortcut-capture fields still record Alt combinations;
press Tab to leave a shortcut field before using form access keys. press Tab to leave a shortcut field before using form access keys.
Settings, the snippet editor and log viewer run on demand in **KSwitcher.Config.exe**, beside the main executable. Settings, the snippet editor and log viewer run on demand in **KSwitcher.Config.exe**, beside the main executable.
Repeated launches activate the existing configurator; closing its last window exits that process. Repeated launches activate the existing configurator; closing its last window exits that process.
@@ -170,7 +175,8 @@ for shapes.
**Canvas zoom:** use **Ctrl++ / Ctrl+-** (including numpad keys) or **Ctrl+mouse wheel**. **Canvas zoom:** use **Ctrl++ / Ctrl+-** (including numpad keys) or **Ctrl+mouse wheel**.
Each step changes the view by 25%; the wheel zooms around the cursor, keyboard shortcuts around Each step changes the view by 25%; the wheel zooms around the cursor, keyboard shortcuts around
the viewport center. **Ctrl+0** restores the initial fit. Drag with the **middle mouse button** the viewport center. **Ctrl+0** restores the initial fit. Drag with the **middle mouse button**
to pan an enlarged image. Zoom ranges from 0.1× to 16× the initial fit; the status bar shows the to pan an enlarged image. Panning is limited to axes where the image exceeds the viewport;
an image that fits stays centered. Zoom ranges from 0.1× to 16× the initial fit; the status bar shows the
actual image scale. Zoom and pan do not change annotations, export resolution or undo history. actual image scale. Zoom and pan do not change annotations, export resolution or undo history.
The wheel without Ctrl still adjusts the object under the cursor. The wheel without Ctrl still adjusts the object under the cursor.
@@ -186,6 +192,9 @@ Every shape has a round rotation handle above its outline. Drag it to rotate aro
hold **Shift** for 15-degree steps. The handle works for toolbar shapes and recognized shapes, hold **Shift** for 15-degree steps. The handle works for toolbar shapes and recognized shapes,
including triangles. Resize handles and attached connectors follow the rotation; Ctrl+Z/Ctrl+Y including triangles. Resize handles and attached connectors follow the rotation; Ctrl+Z/Ctrl+Y
undo and redo it. The square handle still controls outline thickness. undo and redo it. The square handle still controls outline thickness.
From v0.6.34, endpoint, shape-corner and number-radius handles show the four-direction move cursor.
Thickness handles retain directional resize cursors; rotation retains the hand cursor. Dragging,
snapping and shape constraints are unchanged.
**Triangle (G)** is also available directly on the toolbar: drag to create a triangle. **Triangle (G)** is also available directly on the toolbar: drag to create a triangle.
Both drawn and recognized triangles have three independent vertex handles instead of bounding-box Both drawn and recognized triangles have three independent vertex handles instead of bounding-box
resize handles. Drag any vertex to reshape the triangle while keeping the other two fixed, even resize handles. Drag any vertex to reshape the triangle while keeping the other two fixed, even
Binary file not shown.
+7 -9
View File
@@ -1,19 +1,17 @@
# Latest Windows release # Latest Windows release
## v0.6.31 — 2026-11-03 ## v0.6.34 — 2026-11-04
## Changes ## Changes
- Fix conversion failures when an editor such as Viber omits trailing ASCII spaces from Ctrl+C. KSwitcher recovers those spaces only when Windows accessibility confirms the full selection in the target application's focused, non-password control. Exact recent-text and previous-result checks still apply. - Screenshot editing now uses the four-direction move cursor for both endpoints of lines/arrows, bounding-box shape handles and number-radius handles. The cursor no longer suggests that a horizontal or vertical line endpoint is restricted to one axis.
- Recheck the same control and full selection before pasting a recovered range. Read back the replacement even when Write log is disabled; an unknown or mismatching result does not seed repeat conversion or switch the layout. - Object bodies, arrow labels and triangle vertices continue to use the same four-direction cursor. Thickness handles retain directional resize cursors perpendicular to the line; rotation handles retain the hand cursor.
- The shared copy path supports manual conversion, automatic dictionary correction, repeat conversion and Undo. No shortcut, setting or default has changed. - This changes cursor feedback only. Dragging, snapping, circle proportions, undo/redo, shortcuts, defaults and dialog access-key behavior are unchanged. The v0.6.33 unsaved-settings warning and visible menu access letters remain included.
## Validation ## Validation
Live Viber validation converted `Ghbdsn ` to `Привіт ` and confirmed the retained trailing space without sending a message. Final isolated conversion and typo integration runs passed, including real clipboard-space stripping, two spaces within surrounding text, repeat conversion, Undo, missing/unrelated selection rejection, and blocked-paste state clearing with logging disabled. 483 native, 541 input/UI, 105 sync and 48 updater checks passed, as did wheel batching, screenshot PNG saving, screenshot registration and updater progress checks. Earlier integration attempts encountered clipboard contention/restoration and log-rotation assertions; the isolated final rerun passed. The unchanged screenshot UI suite still fails at `middle drag pans enlarged canvas`; screenshot implementation and test source are unchanged from v0.6.30. All three packaged executable versions and all 12 manifest file sizes/hashes were verified; no personal data is packaged. The full `build.ps1 -Stage -Test` run passed: 105 sync checks, shared wheel batching, 483 native regressions, 551 input/UI checks at 96 DPI, screenshot editor UI checks (including free-movement cursor feedback across drawing tools and horizontal/vertical thickness cursors), PNG saving, isolated screenshot registration, 48 updater regressions and updater progress UI checks. All three executable versions and all 12 package manifest file sizes/SHA-256 hashes were verified; the package contains no personal data.
## Limitations ## Upgrade
Recovery requires a non-empty clipboard prefix and a single readable accessibility selection whose only additional characters are trailing ordinary spaces. Tabs, line breaks, missing letters, ambiguous or inaccessible selections are not reconstructed. Accessibility and editor timing can still prevent a conversion. This is not a general fix for all asynchronous editor failures. Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
**Upgrade:** choose Update in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
For manual installation, exit KSwitcher, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries. For manual installation, exit KSwitcher, close screenshot editors and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep all three executables together. The package contains no personal settings, credentials or clipboard history. Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep all three executables together. The package contains no personal settings, credentials or clipboard history.
+1 -1
View File
@@ -1 +1 @@
c02b422a6a7af97f7ffc64d7a526a9fae523583493fcddeb4b80c9aee254667a KSwitcher-win-x64.zip c7f004ecd9d0f1032356060191ca3ca9fca458df434d3e5908c5d9b038f95acc KSwitcher-win-x64.zip
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest" 1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1" 201 RCDATA "update.ps1"
1 VERSIONINFO 1 VERSIONINFO
FILEVERSION 0,6,31,0 FILEVERSION 0,6,34,0
PRODUCTVERSION 0,6,31,0 PRODUCTVERSION 0,6,34,0
FILEOS VOS_NT_WINDOWS32 FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP FILETYPE VFT_APP
BEGIN BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility" VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher" VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.31" VALUE "FileVersion", "0.6.34"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+1
View File
@@ -1654,6 +1654,7 @@ void App::trayMenu()
GetCursorPos(&pt); GetCursorPos(&pt);
pasteTarget = GetForegroundWindow(); pasteTarget = GetForegroundWindow();
SetForegroundWindow(window); SetForegroundWindow(window);
showMenuAccessKeys(menu);
int cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, pt.x, pt.y, 0, window, nullptr); int cmd = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, pt.x, pt.y, 0, window, nullptr);
DestroyMenu(menu); DestroyMenu(menu);
PostMessageW(window, WM_NULL, 0, 0); PostMessageW(window, WM_NULL, 0, 0);
+25
View File
@@ -159,6 +159,31 @@ void showRequestedWindow(HWND window)
if (!IsWindowVisible(window)) if (!IsWindowVisible(window))
ShowWindow(window, SW_SHOW); ShowWindow(window, SW_SHOW);
} }
void showMenuAccessKeys(HMENU menu)
{
// Mouse-opened popups ignore the owner's keyboard-cue state.
// Preserve the mnemonic and expose it in the right-hand hint column.
for (int i = 0; i < GetMenuItemCount(menu); ++i)
{
if (auto child = GetSubMenu(menu, i)) showMenuAccessKeys(child);
int length = GetMenuStringW(menu, i, nullptr, 0, MF_BYPOSITION);
std::wstring text(length + 1, L'\0');
GetMenuStringW(menu, i, text.data(), (int)text.size(), MF_BYPOSITION);
text.resize(length);
if (text.find(L'\t') != text.npos) continue;
for (size_t p = 0; p + 1 < text.size(); ++p)
{
if (text[p] != L'&') continue;
if (text[p + 1] == L'&') { ++p; continue; }
text += L"\t" + upper(text.substr(p + 1, 1));
MENUITEMINFOW item{sizeof(item)};
item.fMask = MIIM_STRING;
item.dwTypeData = text.data();
SetMenuItemInfoW(menu, i, TRUE, &item);
break;
}
}
}
bool activateWindow(HWND window) bool activateWindow(HWND window)
{ {
if (!IsWindow(window)) if (!IsWindow(window))
+1
View File
@@ -38,6 +38,7 @@ HKL windowLayout(HWND);
bool switchWindowLayout(HWND, HKL, const std::function<bool()> &valid); bool switchWindowLayout(HWND, HKL, const std::function<bool()> &valid);
bool activateWindow(HWND); bool activateWindow(HWND);
void showRequestedWindow(HWND); void showRequestedWindow(HWND);
void showMenuAccessKeys(HMENU);
bool ownsClipboard(HWND); bool ownsClipboard(HWND);
bool webViewDescendant(DWORD owner, DWORD host, bool webViewDescendant(DWORD owner, DWORD host,
const std::function<std::optional<std::pair<DWORD, std::wstring>>(DWORD)> &read); const std::function<std::optional<std::pair<DWORD, std::wstring>>(DWORD)> &read);
+5 -4
View File
@@ -783,12 +783,13 @@ HCURSOR actionCursor(const Capture &c,std::pair<int,MarkPart> hit,bool inside)
if(hit.second==MarkPart::Body || hit.second==MarkPart::Label) cursor=IDC_SIZEALL; if(hit.second==MarkPart::Body || hit.second==MarkPart::Label) cursor=IDC_SIZEALL;
else if(hit.second==MarkPart::Rotation) cursor=IDC_HAND; else if(hit.second==MarkPart::Rotation) cursor=IDC_HAND;
else if(hit.second>=MarkPart::Vertex0 && hit.second<=MarkPart::Vertex2) cursor=IDC_SIZEALL; else if(hit.second>=MarkPart::Vertex0 && hit.second<=MarkPart::Vertex2) cursor=IDC_SIZEALL;
else if(hit.second==MarkPart::Radius) cursor=IDC_SIZEWE; else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Radius)
else if(hit.second==MarkPart::Start || hit.second==MarkPart::End || hit.second==MarkPart::Thickness) { cursor=IDC_SIZEALL;
else if(hit.second==MarkPart::Thickness) {
const auto &mark=c.marks[hit.first]; const auto &mark=c.marks[hit.first];
auto [from,to]=hit.second==MarkPart::Thickness?thicknessSegment(mark):std::pair{mark.from,mark.to}; auto [from,to]=thicknessSegment(mark);
double angle=std::atan2(double(to.y-from.y),double(to.x-from.x)); double angle=std::atan2(double(to.y-from.y),double(to.x-from.x));
if(hit.second==MarkPart::Thickness) angle+=3.141592653589793/2; angle+=3.141592653589793/2;
int axis=(int(std::lround(angle/(3.141592653589793/4)))+8)%4; int axis=(int(std::lround(angle/(3.141592653589793/4)))+8)%4;
const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW}; const LPCWSTR cursors[]{IDC_SIZEWE,IDC_SIZENWSE,IDC_SIZENS,IDC_SIZENESW};
cursor=cursors[axis]; cursor=cursors[axis];
+43 -2
View File
@@ -867,6 +867,7 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
AppendMenuW(menu, MF_STRING, 113, L"&Delete"); AppendMenuW(menu, MF_STRING, 113, L"&Delete");
AppendMenuW(menu, MF_STRING, 114, selectedImage(w,v)?L"&Edit image in screenshot editor":L"&Open full item"); AppendMenuW(menu, MF_STRING, 114, selectedImage(w,v)?L"&Edit image in screenshot editor":L"&Open full item");
AppendMenuW(menu, MF_STRING, 115, L"&Copy text from image (OCR)"); AppendMenuW(menu, MF_STRING, 115, L"&Copy text from image (OCR)");
showMenuAccessKeys(menu);
int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr); int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr);
DestroyMenu(menu); DestroyMenu(menu);
if (cmd) if (cmd)
@@ -1338,6 +1339,46 @@ void showOcrOptions(HWND parent, SettingsView *v)
showRequestedWindow(w); SetForegroundWindow(w); showRequestedWindow(w); SetForegroundWindow(w);
} }
#include "ui_sync.inl" #include "ui_sync.inl"
bool settingsChanged(HWND w, SettingsView *v)
{
try
{
return encodeSettings(collectSettings(w, v)) != encodeSettings(v->app->settings);
}
catch (const std::exception &)
{
// Invalid edits are unsaved changes too. Allow discarding them without
// requiring the user to repair the fields first.
return true;
}
}
void closeSettings(HWND w, SettingsView *v)
{
if (settingsChanged(w, v))
{
int choice = MessageBoxW(w,
L"You have unsaved settings changes. Save them before closing?\n\n"
L"Yes: save and close.\nNo: discard changes.\nCancel: keep editing.",
L"KSwitcher — Unsaved settings", MB_YESNOCANCEL | MB_ICONWARNING | MB_DEFBUTTON3);
if (choice != IDYES && choice != IDNO)
return;
if (choice == IDYES)
{
try
{
if (v->app->busy)
throw std::runtime_error("Wait for the current text operation to finish");
v->app->applySettings(collectSettings(w, v));
}
catch (const std::exception &e)
{
error(w, e);
return;
}
}
}
DestroyWindow(w);
}
LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{ {
if (msg == WM_DPICHANGED) if (msg == WM_DPICHANGED)
@@ -1403,7 +1444,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
} }
if (id == IDCANCEL) if (id == IDCANCEL)
{ {
DestroyWindow(w); closeSettings(w, v);
return 0; return 0;
} }
if (id == 250) if (id == 250)
@@ -1454,7 +1495,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
break; break;
} }
case WM_CLOSE: case WM_CLOSE:
DestroyWindow(w); closeSettings(w, v);
return 0; return 0;
case WM_NCDESTROY: case WM_NCDESTROY:
disposeFont(w); disposeFont(w);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h" #include "platform.h"
namespace ks namespace ks
{ {
inline constexpr wchar_t AppVersion[] = L"0.6.31"; inline constexpr wchar_t AppVersion[] = L"0.6.34";
inline constexpr UINT UpdateMessage = WM_APP + 7; inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000; inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+83
View File
@@ -5,6 +5,19 @@
#include <commctrl.h> #include <commctrl.h>
#include <iostream> #include <iostream>
#include <cmath> #include <cmath>
static int settingsCloseChoice = IDCANCEL;
static int settingsClosePrompts = 0;
static void CALLBACK answerSettingsClose(HWND, UINT, UINT_PTR, DWORD)
{
HWND dialog = FindWindowW(L"#32770", L"KSwitcher — Unsaved settings");
DWORD process{};
if (dialog && GetWindowThreadProcessId(dialog, &process) == GetCurrentThreadId() &&
process == GetCurrentProcessId())
{
++settingsClosePrompts;
SendMessageW(dialog, WM_COMMAND, settingsCloseChoice, 0);
}
}
LRESULT CALLBACK countControlMoves(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data) LRESULT CALLBACK countControlMoves(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data)
{ {
if (message == WM_WINDOWPOSCHANGING) ++*reinterpret_cast<int *>(data); if (message == WM_WINDOWPOSCHANGING) ++*reinterpret_cast<int *>(data);
@@ -80,6 +93,25 @@ int main(int argc, char **argv) {
try { try {
INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES|ICC_LISTVIEW_CLASSES}; INITCOMMONCONTROLSEX controls{sizeof(controls),ICC_STANDARD_CLASSES|ICC_LISTVIEW_CLASSES};
InitCommonControlsEx(&controls); InitCommonControlsEx(&controls);
{
HMENU menu = CreatePopupMenu(), child = CreatePopupMenu();
AppendMenuW(menu,MF_STRING | MF_CHECKED,1,L"&Settings…");
AppendMenuW(menu,MF_STRING,2,L"Copy\tCtrl+C");
AppendMenuW(child,MF_STRING,3,L"Rock && &Roll");
AppendMenuW(menu,MF_POPUP,(UINT_PTR)child,L"&Pause");
ks::showMenuAccessKeys(menu);
ks::showMenuAccessKeys(menu);
wchar_t label[128]{};
GetMenuStringW(menu,1,label,128,MF_BYCOMMAND);
require(std::wstring(label)==L"&Settings…\tS" &&
(GetMenuState(menu,1,MF_BYCOMMAND)&MF_CHECKED),
"visible menu mnemonic is idempotent and preserves item state");
GetMenuStringW(menu,2,label,128,MF_BYCOMMAND);
require(std::wstring(label)==L"Copy\tCtrl+C","existing menu shortcut hints are retained");
GetMenuStringW(child,3,label,128,MF_BYCOMMAND);
require(std::wstring(label)==L"Rock && &Roll\tR","submenu hints skip escaped ampersands");
DestroyMenu(menu);
}
{ {
ks::App app(GetModuleHandleW(nullptr),true,temp); ks::App app(GetModuleHandleW(nullptr),true,temp);
ks::InputRegressionAccess::setup(app); ks::InputRegressionAccess::setup(app);
@@ -629,6 +661,49 @@ int main(int argc, char **argv) {
fitted=ks::fitWindowRect({2000,2000,2600,2400},{-1920,0,0,1080}); fitted=ks::fitWindowRect({2000,2000,2600,2400},{-1920,0,0,1080});
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported"); require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
ks::showSettings(app,false); ks::showSettings(app,false);
{
auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose);
require(timer != 0, "settings close prompt responder");
SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0);
require(!app.settingsWindow && settingsClosePrompts == 0,
"unchanged settings close without prompting");
ks::showSettings(app,false);
auto settingsWindow = app.settingsWindow;
auto initial = SendDlgItemMessageW(settingsWindow,205,BM_GETCHECK,0,0);
SendDlgItemMessageW(settingsWindow,205,BM_SETCHECK,!initial,0);
SendMessageW(settingsWindow, WM_CLOSE, 0, 0);
require(IsWindow(settingsWindow) && settingsClosePrompts == 1,
"cancel close preserves changed settings");
SendDlgItemMessageW(settingsWindow,205,BM_SETCHECK,initial,0);
SendMessageW(settingsWindow, WM_CLOSE, 0, 0);
require(!app.settingsWindow && settingsClosePrompts == 1,
"reverted settings close without prompting");
ks::showSettings(app,false);
SetDlgItemTextW(app.settingsWindow,240,L"invalid");
settingsCloseChoice = IDNO;
SendMessageW(app.settingsWindow,WM_COMMAND,IDCANCEL,0);
require(!app.settingsWindow && settingsClosePrompts == 2,
"Cancel button allows discarding invalid edits");
auto originalSettings = app.settings;
// The isolated harness has no keyboard hook; save without shortcuts.
app.settings.toggle = app.settings.casing = app.settings.translit =
app.settings.undo = app.settings.clipboard = app.settings.screenshot = L"";
app.settings.direct.assign(app.layouts.size(), L"");
ks::showSettings(app,false);
SendDlgItemMessageW(app.settingsWindow,208,BM_SETCHECK,
originalSettings.writeLog ? BST_UNCHECKED : BST_CHECKED,0);
settingsCloseChoice = IDYES;
SendMessageW(app.settingsWindow,WM_CLOSE,0,0);
require(!app.settingsWindow && settingsClosePrompts == 3 &&
app.settings.writeLog != originalSettings.writeLog &&
ks::loadSettings(app.data / L"settings.json",app.layouts).writeLog == app.settings.writeLog,
"Yes saves settings to disk and applies them before closing");
app.settings = originalSettings;
ks::setLoggingEnabled(originalSettings.writeLog);
KillTimer(nullptr,timer);
settingsCloseChoice = IDCANCEL;
ks::showSettings(app,false);
}
require(GetDlgItem(app.settingsWindow,258)!=nullptr,"sync available without local opt-in"); require(GetDlgItem(app.settingsWindow,258)!=nullptr,"sync available without local opt-in");
// Mnemonics only target visible controls; show without taking foreground focus. // Mnemonics only target visible controls; show without taking foreground focus.
ShowWindow(app.settingsWindow,SW_SHOWNOACTIVATE); ShowWindow(app.settingsWindow,SW_SHOWNOACTIVATE);
@@ -774,6 +849,14 @@ int main(int argc, char **argv) {
SendDlgItemMessageW(autoOptions,415,BM_SETCHECK,BST_CHECKED,0); SendDlgItemMessageW(autoOptions,415,BM_SETCHECK,BST_CHECKED,0);
SendMessageW(autoOptions,WM_COMMAND,IDOK,0); SendMessageW(autoOptions,WM_COMMAND,IDOK,0);
require(!IsWindow(autoOptions),"personal words accepted without file editing"); require(!IsWindow(autoOptions),"personal words accepted without file editing");
{
auto timer = SetTimer(nullptr,0,10,answerSettingsClose);
auto before = settingsClosePrompts;
SendMessageW(app.settingsWindow,WM_CLOSE,0,0);
KillTimer(nullptr,timer);
require(app.settingsWindow && settingsClosePrompts == before + 1,
"accepted personal dictionary edits trigger unsaved settings warning");
}
SendMessageW(app.settingsWindow,WM_COMMAND,255,0); SendMessageW(app.settingsWindow,WM_COMMAND,255,0);
autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr); autoOptions=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
ShowWindow(autoOptions,SW_HIDE); ShowWindow(autoOptions,SW_HIDE);
+20 -1
View File
@@ -225,6 +225,17 @@ int main(int argc, char **argv)
require(c.zoom>zoom && std::abs(before.x-after.x)<=1 && std::abs(before.y-after.y)<=1, require(c.zoom>zoom && std::abs(before.x-after.x)<=1 && std::abs(before.y-after.y)<=1,
"Ctrl+wheel zoom keeps image point under cursor"); "Ctrl+wheel zoom keeps image point under cursor");
require(!c.history.canUndo(),"zoom does not add image edits to undo history"); require(!c.history.canUndo(),"zoom does not add image edits to undo history");
// A fixed zoom can still fit inside a high-DPI editor. Size the
// fixture from the actual viewport, with room for the tested drag
// on both axes, then start from a centered view.
const double panZoom=std::max({1.0,
double(c.canvas.right-c.canvas.left+128)/(c.view.right-c.view.left),
double(c.canvas.bottom-c.canvas.top+128)/(c.view.bottom-c.view.top)});
ks::zoomCanvas(window,c,panZoom,center);
c.panX=c.panY=0; ks::layout(window,c);
require(c.view.left+15<c.canvas.left && c.view.top+10<c.canvas.top &&
c.view.right>c.canvas.right && c.view.bottom>c.canvas.bottom,
"pan fixture has drag clearance on both axes within the supported zoom range");
auto view=c.view; auto view=c.view;
SendMessageW(window,WM_MBUTTONDOWN,MK_MBUTTON,MAKELPARAM(center.x,center.y)); SendMessageW(window,WM_MBUTTONDOWN,MK_MBUTTON,MAKELPARAM(center.x,center.y));
SendMessageW(window,WM_MOUSEMOVE,MK_MBUTTON,MAKELPARAM(center.x+15,center.y+10)); SendMessageW(window,WM_MOUSEMOVE,MK_MBUTTON,MAKELPARAM(center.x+15,center.y+10));
@@ -443,8 +454,16 @@ int main(int argc, char **argv)
c.marks={{{20,20},{120,20},0}}; c.marks={{{20,20},{120,20},0}};
require(ks::actionCursor(c,{0,ks::MarkPart::Body},true)==LoadCursorW(nullptr,IDC_SIZEALL),"move cursor overrides drawing tool"); require(ks::actionCursor(c,{0,ks::MarkPart::Body},true)==LoadCursorW(nullptr,IDC_SIZEALL),"move cursor overrides drawing tool");
require(ks::actionCursor(c,{0,ks::MarkPart::Rotation},true)==LoadCursorW(nullptr,IDC_HAND),"rotation cursor overrides drawing tool"); require(ks::actionCursor(c,{0,ks::MarkPart::Rotation},true)==LoadCursorW(nullptr,IDC_HAND),"rotation cursor overrides drawing tool");
require(ks::actionCursor(c,{0,ks::MarkPart::End},true)==LoadCursorW(nullptr,IDC_SIZEWE),"endpoint resize overrides drawing tool"); for(auto part:{ks::MarkPart::Start,ks::MarkPart::End,ks::MarkPart::Radius,
ks::MarkPart::Label,ks::MarkPart::Vertex0,ks::MarkPart::Vertex1,ks::MarkPart::Vertex2})
require(ks::actionCursor(c,{0,part},true)==LoadCursorW(nullptr,IDC_SIZEALL),
"freely movable handles use four-direction cursor for every drawing tool");
require(ks::actionCursor(c,{0,ks::MarkPart::Thickness},true)==LoadCursorW(nullptr,IDC_SIZENS),"thickness resize is perpendicular to line"); require(ks::actionCursor(c,{0,ks::MarkPart::Thickness},true)==LoadCursorW(nullptr,IDC_SIZENS),"thickness resize is perpendicular to line");
c.marks[0].to={20,120};
require(ks::actionCursor(c,{0,ks::MarkPart::End},true)==LoadCursorW(nullptr,IDC_SIZEALL),
"vertical line endpoint still moves in both axes");
require(ks::actionCursor(c,{0,ks::MarkPart::Thickness},true)==LoadCursorW(nullptr,IDC_SIZEWE),
"vertical line thickness retains horizontal resize cursor");
} }
c.marks.clear(); SendMessageW(window,WM_COMMAND,27,0); c.marks.clear(); SendMessageW(window,WM_COMMAND,27,0);
wchar_t windowTitle[128]{}; GetWindowTextW(window,windowTitle,128); wchar_t windowTitle[128]{}; GetWindowTextW(window,windowTitle,128);