Release v0.6.27: menu and Alt access keys

This commit is contained in:
2026-11-02 01:23:33 +02:00
parent 22edece7cf
commit 738c81dce2
13 changed files with 138 additions and 114 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.26 LANGUAGES CXX RC) project(KSwitcher VERSION 0.6.27 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()
+5
View File
@@ -16,6 +16,11 @@ 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.
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
to activate a button, toggle a checkbox or focus a labeled field. Press Alt to reveal
the underlines when Windows hides them. Shortcut-capture fields still record Alt combinations;
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.
Saving applies changes immediately through the resident KSwitcher process. The configurator does not Saving applies changes immediately through the resident KSwitcher process. The configurator does not
Binary file not shown.
+6 -8
View File
@@ -1,17 +1,15 @@
# Latest Windows release # Latest Windows release
## v0.6.26 — 2026-11-02 ## v0.6.27 — 2026-11-02
## Changes ## Changes
- Settings → Sync… is available to everyone. Connect your own OpenBao KV v2 with a username and password; synchronization is disabled by default and no server is built in. - Tray and clipboard context menus support underlined access keys. Press the letter while a menu is open (S for Settings, X for Exit).
- Synchronize at startup, approximately two seconds after saving settings, and every 10 minutes in the background. Local operation continues offline. - Alt+letter activates buttons and checkboxes in Settings, Snippets, Logs, synchronization/conflict, OCR and dictionary dialogs. Selected field labels also move focus to their input.
- Merge independent settings changes and resolve conflicts individually, with local/server values shown side by side. Unresolved values stay pending while independent changes synchronize. Every server write uses CAS to protect concurrent changes. - Clipboard history supports Alt+letter for its actions and search while retaining Enter, Shift+Enter and Ctrl+digit shortcuts.
- Snippets have stable IDs and retained deletion records. Independent snippets, renames and text edits can merge; deletion versus editing and duplicate shortcuts/triggers receive explicit conflict handling. - Press Alt to reveal Windows' hidden underlines. Shortcut capture still records Alt combinations; press Tab to leave the capture field before using form access keys. Captions and access keys currently use English letters.
- Personal dictionaries, exclusions, shortcuts and layout preferences synchronize. Startup, history, local paths, connection details and credentials stay on the PC. Passwords use Windows Credential Manager; cached sync state uses DPAPI.
- Existing profiles migrate to sync envelope version 2. Update every participating PC before using the new synchronization behavior.
## Validation ## Validation
91 sync/protocol/conflict checks, 423 native regressions, 510 input/UI checks, screenshot editor and PNG save regressions, shared wheel handling, notification registration, 48 updater checks and updater progress checks. Initial upload and personal-dictionary synchronization were confirmed against the user's OpenBao. Two-physical-PC production testing is still pending. 91 sync/protocol/conflict checks, 423 native regressions, 513 input/UI checks (including Windows Alt+K dispatch and checkbox toggling), shared wheel handling, screenshot editor and PNG save regressions, notification registration, 48 updater checks and updater progress checks.
## Limitations ## Limitations
OpenBao synchronization is experimental and requires a reachable HTTPS server with a trusted certificate, KV v2 and a provisioned userpass account/policy. It does not provide account registration, MFA login, non-root namespaces or reverse-proxy URL prefixes. Ordered layout/language lists are resolved as whole settings. Deletion records are retained without expiry. Missing keyboard layouts block applying a profile. Download/Upload replace a whole profile and are intended for initial setup or deliberate recovery; use Resolve individual conflicts for normal conflicts. Existing browser-selection and clipboard/accessibility limitations remain unchanged. OpenBao synchronization is experimental and requires a reachable HTTPS server with a trusted certificate, KV v2 and a provisioned userpass account/policy. It does not provide account registration, MFA login, non-root namespaces or reverse-proxy URL prefixes. Ordered layout/language lists are resolved as whole settings. Deletion records are retained without expiry. Missing keyboard layouts block applying a profile. Download/Upload replace a whole profile and are intended for initial setup or deliberate recovery; use Resolve individual conflicts for normal conflicts. Existing browser-selection and clipboard/accessibility limitations remain unchanged.
+1 -1
View File
@@ -1 +1 @@
99a863ddb82d3ec868d874ba5986d6552ab8e5b2fd61d0e227577b35f2657383 KSwitcher-win-x64.zip e6597d11fc388979e972c8572e2471a00dec6c3782414a22d396072508fe7329 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,26,0 FILEVERSION 0,6,27,0
PRODUCTVERSION 0,6,26,0 PRODUCTVERSION 0,6,27,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.26" VALUE "FileVersion", "0.6.27"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+20 -17
View File
@@ -1453,28 +1453,28 @@ void App::recognizeEntry(std::wstring id)
void App::trayMenu() void App::trayMenu()
{ {
HMENU menu = CreatePopupMenu(); HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 10, L"Clipboard history"); AppendMenuW(menu, MF_STRING, 10, L"Clipboard &history");
AppendMenuW(menu, MF_STRING, 24, L"Capture screen region…"); AppendMenuW(menu, MF_STRING, 24, L"&Capture screen region…");
AppendMenuW(menu, MF_STRING, 11, L"Settings…"); AppendMenuW(menu, MF_STRING, 11, L"&Settings…");
SetMenuDefaultItem(menu, 11, FALSE); SetMenuDefaultItem(menu, 11, FALSE);
AppendMenuW(menu, MF_STRING, 12, L"Snippets…"); AppendMenuW(menu, MF_STRING, 12, L"S&nippets…");
AppendMenuW(menu, MF_STRING, 13, L"Keyboard shortcuts…"); AppendMenuW(menu, MF_STRING, 13, L"&Keyboard shortcuts…");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING | (settings.awake ? MF_CHECKED : 0), 14, L"Keep screen awake"); AppendMenuW(menu, MF_STRING | (settings.awake ? MF_CHECKED : 0), 14, L"Keep screen a&wake");
HMENU pause = CreatePopupMenu(); HMENU pause = CreatePopupMenu();
AppendMenuW(pause, MF_STRING, 20, L"Resume"); AppendMenuW(pause, MF_STRING, 20, L"&Resume");
AppendMenuW(pause, MF_STRING, 21, L"10 minutes"); AppendMenuW(pause, MF_STRING, 21, L"&10 minutes");
AppendMenuW(pause, MF_STRING, 22, L"1 hour"); AppendMenuW(pause, MF_STRING, 22, L"1 &hour");
AppendMenuW(pause, MF_STRING, 23, L"Until restart"); AppendMenuW(pause, MF_STRING, 23, L"&Until restart");
AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"Pause clipboard history"); AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"&Pause clipboard history");
AppendMenuW(menu, MF_STRING, 15, L"Open data folder"); AppendMenuW(menu, MF_STRING, 15, L"&Open data folder");
AppendMenuW(menu, MF_STRING, 18, L"View log…"); AppendMenuW(menu, MF_STRING, 18, L"View &log…");
AppendMenuW(menu, MF_STRING | (updateRunning ? MF_GRAYED : 0), 19, AppendMenuW(menu, MF_STRING | (updateRunning ? MF_GRAYED : 0), 19,
updateRunning ? L"Update (checking / preparing…)" : updateRunning ? L"&Update (checking / preparing…)" :
availableUpdate.empty() ? L"Update" : L"Update (new version available)"); availableUpdate.empty() ? L"&Update" : L"&Update (new version available)");
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
AppendMenuW(menu, MF_STRING, 16, L"About KSwitcher"); AppendMenuW(menu, MF_STRING, 16, L"&About KSwitcher");
AppendMenuW(menu, MF_STRING, 17, L"Exit"); AppendMenuW(menu, MF_STRING, 17, L"E&xit");
POINT pt; POINT pt;
GetCursorPos(&pt); GetCursorPos(&pt);
pasteTarget = GetForegroundWindow(); pasteTarget = GetForegroundWindow();
@@ -1837,6 +1837,9 @@ int App::execute()
if (popup != settingsWindow && IsDialogMessageW(popup, &msg)) if (popup != settingsWindow && IsDialogMessageW(popup, &msg))
continue; continue;
} }
// Preserve history-specific Enter/Shift+Enter and Ctrl+digit handling.
if (historyWindow && msg.message == WM_SYSCHAR && IsDialogMessageW(historyWindow, &msg))
continue;
if (settingsWindow && IsDialogMessageW(settingsWindow, &msg)) if (settingsWindow && IsDialogMessageW(settingsWindow, &msg))
continue; continue;
if (snippetsWindow && IsDialogMessageW(snippetsWindow, &msg)) if (snippetsWindow && IsDialogMessageW(snippetsWindow, &msg))
+57 -57
View File
@@ -493,7 +493,7 @@ bool selectedImage(HWND w, HistoryView *v)
} }
void updateHistoryActions(HWND w, HistoryView *v) void updateHistoryActions(HWND w, HistoryView *v)
{ {
SetDlgItemTextW(w,114,selectedImage(w,v)?L"Edit image":L"Open item"); SetDlgItemTextW(w,114,selectedImage(w,v)?L"&Edit image":L"&Open item");
} }
std::wstring selected(HWND w, HistoryView *v) std::wstring selected(HWND w, HistoryView *v)
{ {
@@ -854,12 +854,12 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
p={row.left,row.top}; ClientToScreen(list,&p); p={row.left,row.top}; ClientToScreen(list,&p);
} }
HMENU menu = CreatePopupMenu(); HMENU menu = CreatePopupMenu();
AppendMenuW(menu, MF_STRING, 110, L"Paste"); AppendMenuW(menu, MF_STRING, 110, L"&Paste");
AppendMenuW(menu, MF_STRING, 111, L"Paste plain text"); AppendMenuW(menu, MF_STRING, 111, L"Paste plain &text");
AppendMenuW(menu, MF_STRING, 112, L"Pin / unpin"); AppendMenuW(menu, MF_STRING, 112, L"P&in / unpin");
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)");
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)
@@ -1159,9 +1159,9 @@ void showAutoOptions(HWND parent, SettingsView *v)
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v}); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR) new AutoOptionsView{parent, v});
label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525); label(w, L"Offline dictionaries (only layouts in your cycle are used)", 18, 16, 525);
checkbox(w, L"English", 401, 18, 48, 150); checkbox(w, L"&English", 401, 18, 48, 150);
checkbox(w, L"Ukrainian", 402, 188, 48, 150); checkbox(w, L"&Ukrainian", 402, 188, 48, 150);
checkbox(w, L"Russian", 403, 358, 48, 150); checkbox(w, L"&Russian", 403, 358, 48, 150);
for (auto &lang : v->draft.autoLanguages) for (auto &lang : v->draft.autoLanguages)
check(w, lang == L"en" ? 401 : lang == L"uk" ? 402 : 403, true); check(w, lang == L"en" ? 401 : lang == L"uk" ? 402 : 403, true);
label(w, L"Excluded applications (process names, comma-separated)", 18, 94, 525); label(w, L"Excluded applications (process names, comma-separated)", 18, 94, 525);
@@ -1174,7 +1174,7 @@ void showAutoOptions(HWND parent, SettingsView *v)
for (auto [id, language] : {std::pair{411, L"en"}, {412, L"uk"}, {413, L"ru"}}) for (auto [id, language] : {std::pair{411, L"en"}, {412, L"uk"}, {413, L"ru"}})
{ {
int x = 18 + (id - 411) * 178; int x = 18 + (id - 411) * 178;
label(w, id == 411 ? L"English" : id == 412 ? L"Ukrainian" : L"Russian", x, 302, 168); label(w, id == 411 ? L"En&glish" : id == 412 ? L"U&krainian" : L"Ru&ssian", x, 302, 168);
edit(w, id, x, 328, 168, 56, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN); edit(w, id, x, 328, 168, 56, ES_MULTILINE | WS_VSCROLL | ES_AUTOVSCROLL | ES_WANTRETURN);
auto words = v->draft.autoWords.find(language); auto words = v->draft.autoWords.find(language);
if (words != v->draft.autoWords.end()) value(w, id, commaList(words->second)); if (words != v->draft.autoWords.end()) value(w, id, commaList(words->second));
@@ -1192,10 +1192,10 @@ void showAutoOptions(HWND parent, SettingsView *v)
value(w, 408, v->draft.autoPrefixChars); value(w, 408, v->draft.autoPrefixChars);
value(w, 409, v->draft.autoSuffixChars); value(w, 409, v->draft.autoSuffixChars);
label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525); label(w, L"Enter punctuation without spaces or separators. These characters are kept.", 18, 530, 525);
button(w, L"Open dictionary folder", 407, 18, 562, 220); button(w, L"Open dictionary &folder", 407, 18, 562, 220);
label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525); label(w, L"After editing files, click OK here, then Save in Settings to reload.", 18, 600, 525);
button(w, L"OK", IDOK, 326, 632, 100); button(w, L"&OK", IDOK, 326, 632, 100);
button(w, L"Cancel", IDCANCEL, 440, 632, 100); button(w, L"&Cancel", IDCANCEL, 440, 632, 100);
RECT initial{}; RECT initial{};
GetWindowRect(w, &initial); GetWindowRect(w, &initial);
initial.right += scale(w, 200); initial.right += scale(w, 200);
@@ -1278,23 +1278,23 @@ void showOcrOptions(HWND parent, SettingsView *v)
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)new OcrOptionsView{parent, v}); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)new OcrOptionsView{parent, v});
label(w, L"Windows OCR is used by default. Recognition stays on this computer.", 18, 16, 600); label(w, L"Windows OCR is used by default. Recognition stays on this computer.", 18, 16, 600);
checkbox(w, L"Use optional Tesseract 5 instead of Windows OCR", 501, 18, 48, 600); checkbox(w, L"&Use optional Tesseract 5 instead of Windows OCR", 501, 18, 48, 600);
check(w, 501, v->draft.useTesseract); check(w, 501, v->draft.useTesseract);
label(w, L"Tesseract executable (blank = detect installed location)", 18, 90, 600); label(w, L"Tesseract executable (blank = detect installed location)", 18, 90, 600);
edit(w, 502, 18, 118, 484); edit(w, 502, 18, 118, 484);
value(w, 502, v->draft.tesseractPath); value(w, 502, v->draft.tesseractPath);
button(w, L"Browse…", 504, 514, 117, 106); button(w, L"&Browse…", 504, 514, 117, 106);
label(w, L"Languages used together (for example ukr+eng+rus)", 18, 160, 600); label(w, L"Languages used together (for example ukr+eng+rus)", 18, 160, 600);
edit(w, 503, 18, 188, 602); edit(w, 503, 18, 188, 602);
value(w, 503, v->draft.tesseractLanguages); value(w, 503, v->draft.tesseractLanguages);
button(w, L"Check installation", 505, 18, 228, 180); button(w, L"Check &installation", 505, 18, 228, 180);
control(w, L"STATIC", L"", 0, 506, 212, 232, 408, 24); control(w, L"STATIC", L"", 0, 506, 212, 232, 408, 24);
button(w, L"Download Tesseract…", 507, 18, 266, 180); button(w, L"&Download Tesseract…", 507, 18, 266, 180);
label(w, L"Opens the Windows installer page in your browser.", 212, 270, 408); label(w, L"Opens the Windows installer page in your browser.", 212, 270, 408);
label(w, L"Changing the engine or languages reprocesses saved images in the background.", 18, 314, 602); label(w, L"Changing the engine or languages reprocesses saved images in the background.", 18, 314, 602);
label(w, L"Click OK here, then Save in Settings to apply. No downloads are automatic.", 18, 344, 602); label(w, L"Click OK here, then Save in Settings to apply. No downloads are automatic.", 18, 344, 602);
button(w, L"OK", IDOK, 406, 390, 100); button(w, L"&OK", IDOK, 406, 390, 100);
button(w, L"Cancel", IDCANCEL, 520, 390, 100); button(w, L"&Cancel", IDCANCEL, 520, 390, 100);
EnableWindow(parent, FALSE); EnableWindow(parent, FALSE);
showRequestedWindow(w); SetForegroundWindow(w); showRequestedWindow(w); SetForegroundWindow(w);
} }
@@ -1606,18 +1606,18 @@ void showHistory(App &app, bool visible)
app.historyWindow = w; app.historyWindow = w;
windowLayoutState(w)->history = true; windowLayoutState(w)->history = true;
((HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA))->rich = createRichText(w, 105, false); ((HistoryView *)GetWindowLongPtrW(w, GWLP_USERDATA))->rich = createRichText(w, 105, false);
label(w, L"Search clipboard history", 16, 12, 500); label(w, L"&Search clipboard history", 16, 12, 500);
edit(w, 101, 16, 38, 728); edit(w, 101, 16, 38, 728);
auto list = control(w, L"LISTBOX", L"", auto list = control(w, L"LISTBOX", L"",
WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER | WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER |
LBS_OWNERDRAWFIXED | LBS_HASSTRINGS, LBS_OWNERDRAWFIXED | LBS_HASSTRINGS,
102, 16, 76, 728, 360); 102, 16, 76, 728, 360);
SendMessageW(list, LB_SETITEMHEIGHT, 0, scale(w, 88)); SendMessageW(list, LB_SETITEMHEIGHT, 0, scale(w, 88));
button(w, L"Paste", 110, 16, 452, 90); button(w, L"&Paste", 110, 16, 452, 90);
button(w, L"Plain text", 111, 114, 452, 100); button(w, L"Plain &text", 111, 114, 452, 100);
button(w, L"Pin / unpin", 112, 222, 452, 110); button(w, L"P&in / unpin", 112, 222, 452, 110);
button(w, L"Delete", 113, 340, 452, 90); button(w, L"&Delete", 113, 340, 452, 90);
button(w, L"Open item", 114, 438, 452, 100); button(w, L"&Open item", 114, 438, 452, 100);
SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w); SetWindowSubclass(list, historyKey, 1, (DWORD_PTR)w);
SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w); SetWindowSubclass(GetDlgItem(w, 101), historyKey, 1, (DWORD_PTR)w);
} }
@@ -1656,13 +1656,13 @@ void showSettings(App &app, bool visible)
HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660, 0, false); HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 780, 660, 0, false);
app.settingsWindow = w; app.settingsWindow = w;
label(w, L"Text conversion", 18, 16); label(w, L"Text conversion", 18, 16);
checkbox(w, L"Convert recently typed text when nothing is selected", 201, 18, 43, 390); checkbox(w, L"Con&vert recently typed text when nothing is selected", 201, 18, 43, 390);
checkbox(w, L"Switch Windows input layout after conversion", 202, 18, 70, 390); checkbox(w, L"Switch Windows input &layout after conversion", 202, 18, 70, 390);
checkbox(w, L"Record clipboard history", 203, 18, 106); checkbox(w, L"Record clip&board history", 203, 18, 106);
checkbox(w, L"Start with Windows", 204, 18, 133); checkbox(w, L"Start with &Windows", 204, 18, 133);
checkbox(w, L"Start with Windows as administrator", 206, 18, 160, 395); checkbox(w, L"Start with Windows as ad&ministrator", 206, 18, 160, 395);
checkbox(w, L"Keep screen awake", 205, 18, 184, 210); checkbox(w, L"&Keep screen awake", 205, 18, 184, 210);
checkbox(w, L"Write log", 208, 240, 184, 160); checkbox(w, L"Write lo&g", 208, 240, 184, 160);
label(w, L"Click a shortcut and press the desired keys", 420, 16, 350); label(w, L"Click a shortcut and press the desired keys", 420, 16, 350);
const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo", const wchar_t *names[] = {L"Next layout", L"Change case", L"Transliterate", L"Undo",
L"Clipboard history", L"Screen capture"}; L"Clipboard history", L"Screen capture"};
@@ -1671,18 +1671,18 @@ void showSettings(App &app, bool visible)
label(w, names[i], 420, 48 + i * 30, 115); label(w, names[i], 420, 48 + i * 30, 115);
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220); hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
} }
checkbox(w, L"Open editor after screen capture", 209, 420, 224, 340); checkbox(w, L"Open editor after screen capt&ure", 209, 420, 224, 340);
checkbox(w, L"Automatically correct layout (experimental)", 207, 18, 210, 395); checkbox(w, L"Automatically correct layout (e&xperimental)", 207, 18, 210, 395);
label(w, L"Layout cycle — top to bottom", 18, 240, 400); label(w, L"Layout cycle — top to bottom", 18, 240, 400);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220, control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220,
18, 262, 470, 172); 18, 262, 470, 172);
button(w, L"Move up", 232, 504, 262, 120); button(w, L"Move u&p", 232, 504, 262, 120);
button(w, L"Move down", 233, 632, 262, 128); button(w, L"Move &down", 233, 632, 262, 128);
checkbox(w, L"Include in cycle", 230, 504, 300, 240); checkbox(w, L"&Include in cycle", 230, 504, 300, 240);
label(w, L"Direct layout shortcut", 504, 332, 240); label(w, L"Direct layout shortcut", 504, 332, 240);
hotkeyEdit(app, w, 231, 504, 356, 256); hotkeyEdit(app, w, 231, 504, 356, 256);
label(w, L"Backspace / Delete clears a shortcut", 504, 382, 256); label(w, L"Backspace / Delete clears a shortcut", 504, 382, 256);
button(w, L"Refresh layouts", 253, 504, 408, 150); button(w, L"Re&fresh layouts", 253, 504, 408, 150);
label(w, L"History limits", 18, 444); label(w, L"History limits", 18, 444);
label(w, L"Items", 18, 478, 50); label(w, L"Items", 18, 478, 50);
edit(w, 240, 70, 474, 90); edit(w, 240, 70, 474, 90);
@@ -1690,18 +1690,18 @@ void showSettings(App &app, bool visible)
edit(w, 241, 260, 474, 90); edit(w, 241, 260, 474, 90);
label(w, L"Per item MiB", 370, 478, 100); label(w, L"Per item MiB", 370, 478, 100);
edit(w, 242, 475, 474, 80); edit(w, 242, 475, 474, 80);
button(w, L"Clear history…", 254, 580, 474, 180); button(w, L"Clear &history…", 254, 580, 474, 180);
label(w, L"Exclude applications (process names, comma-separated)", 18, 516, 700); label(w, L"Exclude applications (process names, comma-separated)", 18, 516, 700);
edit(w, 243, 18, 544, 742); edit(w, 243, 18, 544, 742);
button(w, L"Auto-correction options…", 255, 18, 575, 240); button(w, L"&Auto-correction options…", 255, 18, 575, 240);
button(w, L"View log…", 256, 278, 575, 220); button(w, L"View l&og…", 256, 278, 575, 220);
button(w, L"OCR options…", 257, 518, 575, 242); button(w, L"OCR op&tions…", 257, 518, 575, 242);
button(w, L"Import settings…", 250, 18, 606, 145); button(w, L"Impo&rt settings…", 250, 18, 606, 145);
button(w, L"Export settings…", 251, 173, 606, 145); button(w, L"&Export settings…", 251, 173, 606, 145);
button(w, L"Snippets…", 252, 328, 606, 110); button(w, L"S&nippets…", 252, 328, 606, 110);
button(w, L"Sync...", 258, 448, 606, 90); button(w, L"S&ync...", 258, 448, 606, 90);
button(w, L"Save", IDOK, 548, 606, 100); button(w, L"&Save", IDOK, 548, 606, 100);
button(w, L"Cancel", IDCANCEL, 660, 606, 100); button(w, L"&Cancel", IDCANCEL, 660, 606, 100);
fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA)); fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA));
if (visible) if (visible)
{ {
@@ -1721,17 +1721,17 @@ void showSnippets(App &app)
label(w, L"Triggers", 16, 16); label(w, L"Triggers", 16, 16);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 301, control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 301,
16, 44, 200, 380); 16, 44, 200, 380);
button(w, L"Add", 310, 16, 438, 90); button(w, L"&Add", 310, 16, 438, 90);
button(w, L"Delete", 311, 116, 438, 100); button(w, L"&Delete", 311, 116, 438, 100);
label(w, L"Trigger (for example ;addr)", 236, 16, 470); label(w, L"&Trigger (for example ;addr)", 236, 16, 470);
edit(w, 302, 236, 44, 488); edit(w, 302, 236, 44, 488);
label(w, L"Replacement text", 236, 82, 480); label(w, L"&Replacement text", 236, 82, 480);
edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL); edit(w, 303, 236, 110, 488, 270, ES_MULTILINE | ES_WANTRETURN | ES_AUTOVSCROLL | WS_VSCROLL);
control(w, L"STATIC", L"{cursor}: caret; {enter}: press Enter after insertion.\nUse {{cursor}} / {{enter}} for literal markers.", 0, 0, 236, 392, 488, 36); control(w, L"STATIC", L"{cursor}: caret; {enter}: press Enter after insertion.\nUse {{cursor}} / {{enter}} for literal markers.", 0, 0, 236, 392, 488, 36);
button(w, L"Insert cursor marker", 312, 236, 430, 190); button(w, L"Insert cursor &marker", 312, 236, 430, 190);
button(w, L"Insert Enter marker", 313, 436, 430, 190); button(w, L"Insert &Enter marker", 313, 436, 430, 190);
button(w, L"Save", IDOK, 510, 478, 100); button(w, L"&Save", IDOK, 510, 478, 100);
button(w, L"Cancel", IDCANCEL, 624, 478, 100); button(w, L"&Cancel", IDCANCEL, 624, 478, 100);
fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA)); fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA));
showRequestedWindow(w); showRequestedWindow(w);
SetForegroundWindow(w); SetForegroundWindow(w);
+9 -9
View File
@@ -309,18 +309,18 @@ void showLogs(App &app)
SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Current log — kswitcher.log"); SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Current log — kswitcher.log");
SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Previous log — kswitcher.log.1"); SendMessageW(choice, CB_ADDSTRING, 0, (LPARAM)L"Previous log — kswitcher.log.1");
SendMessageW(choice, CB_SETCURSEL, 0, 0); SendMessageW(choice, CB_SETCURSEL, 0, 0);
button(w, L"Refresh", LogRefresh, 310, 14, 100); button(w, L"&Refresh", LogRefresh, 310, 14, 100);
button(w, L"Copy selected", LogCopy, 424, 14, 140); button(w, L"&Copy selected", LogCopy, 424, 14, 140);
button(w, L"Clear log", LogClear, 578, 14, 100); button(w, L"Clear &log", LogClear, 578, 14, 100);
control(w, L"STATIC", L"", 0, LogStatus, 16, 49, 1068, 22); control(w, L"STATIC", L"", 0, LogStatus, 16, 49, 1068, 22);
label(w, L"Program", 16, 78, 170); label(w, L"&Program", 16, 78, 170);
control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogProgram, 16, 101, 174, 300); control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogProgram, 16, 101, 174, 300);
label(w, L"Command", 206, 78, 200); label(w, L"Co&mmand", 206, 78, 200);
control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogCommand, 206, 101, 204, 300); control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogCommand, 206, 101, 204, 300);
label(w, L"Result", 426, 78, 300); label(w, L"Res&ult", 426, 78, 300);
control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogResult, 426, 101, 310, 300); control(w, L"COMBOBOX", L"", WS_TABSTOP | CBS_DROPDOWNLIST | WS_VSCROLL, LogResult, 426, 101, 310, 300);
checkbox(w, L"Only problems", LogProblems, 752, 102, 166); checkbox(w, L"&Only problems", LogProblems, 752, 102, 166);
button(w, L"Reset filters", LogReset, 938, 98, 146); button(w, L"Reset &filters", LogReset, 938, 98, 146);
INITCOMMONCONTROLSEX common{sizeof(common), ICC_LISTVIEW_CLASSES}; INITCOMMONCONTROLSEX common{sizeof(common), ICC_LISTVIEW_CLASSES};
InitCommonControlsEx(&common); InitCommonControlsEx(&common);
auto list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SHOWSELALWAYS, auto list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SHOWSELALWAYS,
@@ -344,7 +344,7 @@ void showLogs(App &app)
w, L"STATIC", w, L"STATIC",
L"Click a column to sort. Select an operation for details. Ctrl+C copies selected operations.", 0, 0, L"Click a column to sort. Select an operation for details. Ctrl+C copies selected operations.", 0, 0,
16, 712, 860, 22); 16, 712, 860, 22);
button(w, L"Close", IDCANCEL, 974, 708, 110); button(w, L"Clos&e", IDCANCEL, 974, 708, 110);
auto *layout = windowLayoutState(w); auto *layout = windowLayoutState(w);
layout->stretch = list; layout->stretch = list;
layout->details = details; layout->details = details;
+13 -13
View File
@@ -83,25 +83,25 @@ void showSyncOptions(HWND parent, App &app)
HWND w = makeWindow(app, L"KSwitcher.Sync", L"Settings synchronization - OpenBao", syncProc, 660, 640); HWND w = makeWindow(app, L"KSwitcher.Sync", L"Settings synchronization - OpenBao", syncProc, 660, 640);
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)new SyncView{parent, &app}); SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)new SyncView{parent, &app});
checkbox(w, L"Enable synchronization with your OpenBao (KV v2)", 601, 18, 18, 620); checkbox(w, L"&Enable synchronization with your OpenBao (KV v2)", 601, 18, 18, 620);
check(w, 601, c.enabled); check(w, 601, c.enabled);
label(w, L"Server (HTTPS)", 18, 60, 160); edit(w, 602, 190, 56, 450); value(w, 602, c.server); label(w, L"Se&rver (HTTPS)", 18, 60, 160); edit(w, 602, 190, 56, 450); value(w, 602, c.server);
label(w, L"Username", 18, 100, 160); edit(w, 603, 190, 96, 450); value(w, 603, c.username); label(w, L"User&name", 18, 100, 160); edit(w, 603, 190, 96, 450); value(w, 603, c.username);
label(w, L"Password", 18, 140, 160); edit(w, 604, 190, 136, 450, 25, ES_AUTOHSCROLL | ES_PASSWORD); label(w, L"&Password", 18, 140, 160); edit(w, 604, 190, 136, 450, 25, ES_AUTOHSCROLL | ES_PASSWORD);
label(w, L"Leave password blank to use the saved Windows credential.", 190, 166, 450); label(w, L"Leave password blank to use the saved Windows credential.", 190, 166, 450);
label(w, L"KV v2 mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount); label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount);
label(w, L"Profile path", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path); label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
label(w, L"Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount); label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
label(w, L"Sync uses saved settings. Close and reopen Settings after downloading changes.", 18, 330, 622); label(w, L"Sync uses saved settings. Close and reopen Settings after downloading changes.", 18, 330, 622);
control(w, L"STATIC", L"", 0, 608, 18, 362, 622, 76); control(w, L"STATIC", L"", 0, 608, 18, 362, 622, 76);
value(w, 608, app.syncCommand()); value(w, 608, app.syncCommand());
button(w, L"Save / Sign in / Sync", IDOK, 18, 452, 200); button(w, L"&Save / Sign in / Sync", IDOK, 18, 452, 200);
button(w, L"Download remote", 610, 230, 452, 195); button(w, L"&Download remote", 610, 230, 452, 195);
button(w, L"Upload local", 611, 437, 452, 203); button(w, L"&Upload local", 611, 437, 452, 203);
button(w, L"Resolve individual conflicts...", 612, 18, 498, 300); button(w, L"Resolve &individual conflicts...", 612, 18, 498, 300);
label(w, L"Background check: every 10 minutes", 330, 502, 310); label(w, L"Background check: every 10 minutes", 330, 502, 310);
button(w, L"Disconnect / forget password", 609, 18, 550, 290); button(w, L"Disconnect / &forget password", 609, 18, 550, 290);
button(w, L"Close", IDCANCEL, 540, 550, 100); button(w, L"&Close", IDCANCEL, 540, 550, 100);
EnableWindow(parent, FALSE); EnableWindow(parent, FALSE);
SetTimer(w, 1, 1000, nullptr); SetTimer(w, 1, 1000, nullptr);
showRequestedWindow(w); SetForegroundWindow(w); showRequestedWindow(w); SetForegroundWindow(w);
+4 -4
View File
@@ -70,11 +70,11 @@ void showSyncConflicts(HWND parent, App &app)
label(w, L"This PC", 18, 190, 360); label(w, L"Server", 410, 190, 368); label(w, L"This PC", 18, 190, 360); label(w, L"Server", 410, 190, 368);
edit(w, 621, 18, 220, 368, 180, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL); edit(w, 621, 18, 220, 368, 180, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL);
edit(w, 622, 410, 220, 368, 180, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL); edit(w, 622, 410, 220, 368, 180, ES_MULTILINE | ES_READONLY | ES_AUTOVSCROLL | WS_VSCROLL);
button(w, L"Use this PC for selected setting", 623, 18, 412, 368); button(w, L"Use this &PC for selected setting", 623, 18, 412, 368);
button(w, L"Use server for selected setting", 624, 410, 412, 368); button(w, L"Use &server for selected setting", 624, 410, 412, 368);
label(w, L"Unselected conflicts stay pending. Changed values will be checked again before saving.", 18, 454, 760); label(w, L"Unselected conflicts stay pending. Changed values will be checked again before saving.", 18, 454, 760);
button(w, L"Apply choices", IDOK, 526, 488, 140); button(w, L"&Apply choices", IDOK, 526, 488, 140);
button(w, L"Cancel", IDCANCEL, 678, 488, 100); button(w, L"&Cancel", IDCANCEL, 678, 488, 100);
SendDlgItemMessageW(w, 620, LB_SETCURSEL, 0, 0); showConflictValues(w, v); SendDlgItemMessageW(w, 620, LB_SETCURSEL, 0, 0); showConflictValues(w, v);
EnableWindow(parent, FALSE); showRequestedWindow(w); SetForegroundWindow(w); EnableWindow(parent, FALSE); showRequestedWindow(w); SetForegroundWindow(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.26"; inline constexpr wchar_t AppVersion[] = L"0.6.27";
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;
+18
View File
@@ -627,6 +627,24 @@ int main(int argc, char **argv) {
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);
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.
ShowWindow(app.settingsWindow,SW_SHOWNOACTIVATE);
auto previousActive=SetActiveWindow(app.settingsWindow);
auto awakeBefore=SendDlgItemMessageW(app.settingsWindow,205,BM_GETCHECK,0,0);
MSG accessKey{}; accessKey.hwnd=GetDlgItem(app.settingsWindow,201);
accessKey.message=WM_SYSCHAR; accessKey.wParam=L'k'; accessKey.lParam=1LL<<29;
require(IsDialogMessageW(app.settingsWindow,&accessKey),"Alt+K is handled as a dialog access key");
MSG dispatched{};
while(PeekMessageW(&dispatched,nullptr,0,0,PM_REMOVE)) { TranslateMessage(&dispatched); DispatchMessageW(&dispatched); }
require(SendDlgItemMessageW(app.settingsWindow,205,BM_GETCHECK,0,0)!=awakeBefore,
"Alt+K toggles Keep screen awake");
IsDialogMessageW(app.settingsWindow,&accessKey);
while(PeekMessageW(&dispatched,nullptr,0,0,PM_REMOVE)) { TranslateMessage(&dispatched); DispatchMessageW(&dispatched); }
require(SendDlgItemMessageW(app.settingsWindow,205,BM_GETCHECK,0,0)==awakeBefore,
"repeated access key restores checkbox state");
ShowWindow(app.settingsWindow,SW_HIDE);
SetActiveWindow(previousActive);
DestroyWindow(app.settingsWindow); DestroyWindow(app.settingsWindow);
ks::saveSyncConfig(app.data,ks::SyncConfig{}); ks::saveSyncConfig(app.data,ks::SyncConfig{});
ks::showSettings(app,false); ks::showSettings(app,false);