diff --git a/CMakeLists.txt b/CMakeLists.txt index 618e675..42a32a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ 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) message(FATAL_ERROR "KSwitcher uses native Windows APIs") endif() diff --git a/README.md b/README.md index 343316c..027038c 100644 --- a/README.md +++ b/README.md @@ -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 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. +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. 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 diff --git a/Release/KSwitcher-win-x64.zip b/Release/KSwitcher-win-x64.zip index 2148c07..3cfa7c9 100644 Binary files a/Release/KSwitcher-win-x64.zip and b/Release/KSwitcher-win-x64.zip differ diff --git a/Release/README.md b/Release/README.md index 879d50c..d8bdbfa 100644 --- a/Release/README.md +++ b/Release/README.md @@ -1,17 +1,15 @@ # Latest Windows release -## v0.6.26 — 2026-11-02 +## v0.6.27 — 2026-11-02 ## 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. -- Synchronize at startup, approximately two seconds after saving settings, and every 10 minutes in the background. Local operation continues offline. -- 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. -- 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. -- 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. +- Tray and clipboard context menus support underlined access keys. Press the letter while a menu is open (S for Settings, X for Exit). +- 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. +- Clipboard history supports Alt+letter for its actions and search while retaining Enter, Shift+Enter and Ctrl+digit shortcuts. +- 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. ## 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 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. diff --git a/Release/SHA256SUMS.txt b/Release/SHA256SUMS.txt index 168edf3..565e551 100644 --- a/Release/SHA256SUMS.txt +++ b/Release/SHA256SUMS.txt @@ -1 +1 @@ -99a863ddb82d3ec868d874ba5986d6552ab8e5b2fd61d0e227577b35f2657383 KSwitcher-win-x64.zip +e6597d11fc388979e972c8572e2471a00dec6c3782414a22d396072508fe7329 KSwitcher-win-x64.zip diff --git a/resources/app.rc b/resources/app.rc index 2c665d9..54b8adc 100644 --- a/resources/app.rc +++ b/resources/app.rc @@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur" 1 RT_MANIFEST "app.manifest" 201 RCDATA "update.ps1" 1 VERSIONINFO - FILEVERSION 0,6,26,0 - PRODUCTVERSION 0,6,26,0 + FILEVERSION 0,6,27,0 + PRODUCTVERSION 0,6,27,0 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP BEGIN @@ -20,7 +20,7 @@ BEGIN BEGIN VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility" VALUE "ProductName", "KSwitcher" - VALUE "FileVersion", "0.6.26" + VALUE "FileVersion", "0.6.27" END END BLOCK "VarFileInfo" diff --git a/src/app.cpp b/src/app.cpp index 5c5275c..55cc7d5 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1453,28 +1453,28 @@ void App::recognizeEntry(std::wstring id) void App::trayMenu() { HMENU menu = CreatePopupMenu(); - AppendMenuW(menu, MF_STRING, 10, L"Clipboard history"); - AppendMenuW(menu, MF_STRING, 24, L"Capture screen region…"); - AppendMenuW(menu, MF_STRING, 11, L"Settings…"); + AppendMenuW(menu, MF_STRING, 10, L"Clipboard &history"); + AppendMenuW(menu, MF_STRING, 24, L"&Capture screen region…"); + AppendMenuW(menu, MF_STRING, 11, L"&Settings…"); SetMenuDefaultItem(menu, 11, FALSE); - AppendMenuW(menu, MF_STRING, 12, L"Snippets…"); - AppendMenuW(menu, MF_STRING, 13, L"Keyboard shortcuts…"); + AppendMenuW(menu, MF_STRING, 12, L"S&nippets…"); + AppendMenuW(menu, MF_STRING, 13, L"&Keyboard shortcuts…"); 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(); - AppendMenuW(pause, MF_STRING, 20, L"Resume"); - AppendMenuW(pause, MF_STRING, 21, L"10 minutes"); - AppendMenuW(pause, MF_STRING, 22, L"1 hour"); - AppendMenuW(pause, MF_STRING, 23, L"Until restart"); - AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"Pause clipboard history"); - AppendMenuW(menu, MF_STRING, 15, L"Open data folder"); - AppendMenuW(menu, MF_STRING, 18, L"View log…"); + AppendMenuW(pause, MF_STRING, 20, L"&Resume"); + AppendMenuW(pause, MF_STRING, 21, L"&10 minutes"); + AppendMenuW(pause, MF_STRING, 22, L"1 &hour"); + AppendMenuW(pause, MF_STRING, 23, L"&Until restart"); + AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"&Pause clipboard history"); + AppendMenuW(menu, MF_STRING, 15, L"&Open data folder"); + AppendMenuW(menu, MF_STRING, 18, L"View &log…"); AppendMenuW(menu, MF_STRING | (updateRunning ? MF_GRAYED : 0), 19, - updateRunning ? L"Update (checking / preparing…)" : - availableUpdate.empty() ? L"Update" : L"Update (new version available)"); + updateRunning ? L"&Update (checking / preparing…)" : + availableUpdate.empty() ? L"&Update" : L"&Update (new version available)"); AppendMenuW(menu, MF_SEPARATOR, 0, nullptr); - AppendMenuW(menu, MF_STRING, 16, L"About KSwitcher"); - AppendMenuW(menu, MF_STRING, 17, L"Exit"); + AppendMenuW(menu, MF_STRING, 16, L"&About KSwitcher"); + AppendMenuW(menu, MF_STRING, 17, L"E&xit"); POINT pt; GetCursorPos(&pt); pasteTarget = GetForegroundWindow(); @@ -1837,6 +1837,9 @@ int App::execute() if (popup != settingsWindow && IsDialogMessageW(popup, &msg)) 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)) continue; if (snippetsWindow && IsDialogMessageW(snippetsWindow, &msg)) diff --git a/src/ui.cpp b/src/ui.cpp index 2fea01a..3c31fac 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -493,7 +493,7 @@ bool selectedImage(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) { @@ -854,12 +854,12 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp) p={row.left,row.top}; ClientToScreen(list,&p); } HMENU menu = CreatePopupMenu(); - AppendMenuW(menu, MF_STRING, 110, L"Paste"); - AppendMenuW(menu, MF_STRING, 111, L"Paste plain text"); - AppendMenuW(menu, MF_STRING, 112, L"Pin / unpin"); - 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, 115, L"Copy text from image (OCR)"); + AppendMenuW(menu, MF_STRING, 110, L"&Paste"); + AppendMenuW(menu, MF_STRING, 111, L"Paste plain &text"); + AppendMenuW(menu, MF_STRING, 112, L"P&in / unpin"); + 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, 115, L"&Copy text from image (OCR)"); int cmd = TrackPopupMenu(menu, TPM_RETURNCMD, p.x, p.y, 0, w, nullptr); DestroyMenu(menu); if (cmd) @@ -1159,9 +1159,9 @@ void showAutoOptions(HWND parent, SettingsView *v) SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); 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); - checkbox(w, L"English", 401, 18, 48, 150); - checkbox(w, L"Ukrainian", 402, 188, 48, 150); - checkbox(w, L"Russian", 403, 358, 48, 150); + checkbox(w, L"&English", 401, 18, 48, 150); + checkbox(w, L"&Ukrainian", 402, 188, 48, 150); + checkbox(w, L"&Russian", 403, 358, 48, 150); for (auto &lang : v->draft.autoLanguages) check(w, lang == L"en" ? 401 : lang == L"uk" ? 402 : 403, true); 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"}}) { 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); auto words = v->draft.autoWords.find(language); 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, 409, v->draft.autoSuffixChars); 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); - button(w, L"OK", IDOK, 326, 632, 100); - button(w, L"Cancel", IDCANCEL, 440, 632, 100); + button(w, L"&OK", IDOK, 326, 632, 100); + button(w, L"&Cancel", IDCANCEL, 440, 632, 100); RECT initial{}; GetWindowRect(w, &initial); 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_USERDATA, (LONG_PTR)new OcrOptionsView{parent, v}); 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); label(w, L"Tesseract executable (blank = detect installed location)", 18, 90, 600); edit(w, 502, 18, 118, 484); 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); edit(w, 503, 18, 188, 602); 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); - 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"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); - button(w, L"OK", IDOK, 406, 390, 100); - button(w, L"Cancel", IDCANCEL, 520, 390, 100); + button(w, L"&OK", IDOK, 406, 390, 100); + button(w, L"&Cancel", IDCANCEL, 520, 390, 100); EnableWindow(parent, FALSE); showRequestedWindow(w); SetForegroundWindow(w); } @@ -1606,18 +1606,18 @@ void showHistory(App &app, bool visible) app.historyWindow = w; windowLayoutState(w)->history = true; ((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); auto list = control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS, 102, 16, 76, 728, 360); SendMessageW(list, LB_SETITEMHEIGHT, 0, scale(w, 88)); - button(w, L"Paste", 110, 16, 452, 90); - button(w, L"Plain text", 111, 114, 452, 100); - button(w, L"Pin / unpin", 112, 222, 452, 110); - button(w, L"Delete", 113, 340, 452, 90); - button(w, L"Open item", 114, 438, 452, 100); + button(w, L"&Paste", 110, 16, 452, 90); + button(w, L"Plain &text", 111, 114, 452, 100); + button(w, L"P&in / unpin", 112, 222, 452, 110); + button(w, L"&Delete", 113, 340, 452, 90); + button(w, L"&Open item", 114, 438, 452, 100); SetWindowSubclass(list, 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); app.settingsWindow = w; 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"Switch Windows input layout after conversion", 202, 18, 70, 390); - checkbox(w, L"Record clipboard history", 203, 18, 106); - checkbox(w, L"Start with Windows", 204, 18, 133); - checkbox(w, L"Start with Windows as administrator", 206, 18, 160, 395); - checkbox(w, L"Keep screen awake", 205, 18, 184, 210); - checkbox(w, L"Write log", 208, 240, 184, 160); + 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"Record clip&board history", 203, 18, 106); + checkbox(w, L"Start with &Windows", 204, 18, 133); + 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"Write lo&g", 208, 240, 184, 160); 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", 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); 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"Automatically correct layout (experimental)", 207, 18, 210, 395); + checkbox(w, L"Open editor after screen capt&ure", 209, 420, 224, 340); + checkbox(w, L"Automatically correct layout (e&xperimental)", 207, 18, 210, 395); 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, 18, 262, 470, 172); - button(w, L"Move up", 232, 504, 262, 120); - button(w, L"Move down", 233, 632, 262, 128); - checkbox(w, L"Include in cycle", 230, 504, 300, 240); + button(w, L"Move u&p", 232, 504, 262, 120); + button(w, L"Move &down", 233, 632, 262, 128); + checkbox(w, L"&Include in cycle", 230, 504, 300, 240); label(w, L"Direct layout shortcut", 504, 332, 240); hotkeyEdit(app, w, 231, 504, 356, 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"Items", 18, 478, 50); edit(w, 240, 70, 474, 90); @@ -1690,18 +1690,18 @@ void showSettings(App &app, bool visible) edit(w, 241, 260, 474, 90); label(w, L"Per item MiB", 370, 478, 100); 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); edit(w, 243, 18, 544, 742); - button(w, L"Auto-correction options…", 255, 18, 575, 240); - button(w, L"View log…", 256, 278, 575, 220); - button(w, L"OCR options…", 257, 518, 575, 242); - button(w, L"Import settings…", 250, 18, 606, 145); - button(w, L"Export settings…", 251, 173, 606, 145); - button(w, L"Snippets…", 252, 328, 606, 110); - button(w, L"Sync...", 258, 448, 606, 90); - button(w, L"Save", IDOK, 548, 606, 100); - button(w, L"Cancel", IDCANCEL, 660, 606, 100); + button(w, L"&Auto-correction options…", 255, 18, 575, 240); + button(w, L"View l&og…", 256, 278, 575, 220); + button(w, L"OCR op&tions…", 257, 518, 575, 242); + button(w, L"Impo&rt settings…", 250, 18, 606, 145); + button(w, L"&Export settings…", 251, 173, 606, 145); + button(w, L"S&nippets…", 252, 328, 606, 110); + button(w, L"S&ync...", 258, 448, 606, 90); + button(w, L"&Save", IDOK, 548, 606, 100); + button(w, L"&Cancel", IDCANCEL, 660, 606, 100); fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA)); if (visible) { @@ -1721,17 +1721,17 @@ void showSnippets(App &app) label(w, L"Triggers", 16, 16); control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 301, 16, 44, 200, 380); - button(w, L"Add", 310, 16, 438, 90); - button(w, L"Delete", 311, 116, 438, 100); - label(w, L"Trigger (for example ;addr)", 236, 16, 470); + button(w, L"&Add", 310, 16, 438, 90); + button(w, L"&Delete", 311, 116, 438, 100); + label(w, L"&Trigger (for example ;addr)", 236, 16, 470); 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); 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 Enter marker", 313, 436, 430, 190); - button(w, L"Save", IDOK, 510, 478, 100); - button(w, L"Cancel", IDCANCEL, 624, 478, 100); + button(w, L"Insert cursor &marker", 312, 236, 430, 190); + button(w, L"Insert &Enter marker", 313, 436, 430, 190); + button(w, L"&Save", IDOK, 510, 478, 100); + button(w, L"&Cancel", IDCANCEL, 624, 478, 100); fillSnippets(w, (SnippetView *)GetWindowLongPtrW(w, GWLP_USERDATA)); showRequestedWindow(w); SetForegroundWindow(w); diff --git a/src/ui_log.inl b/src/ui_log.inl index d3da456..4912580 100644 --- a/src/ui_log.inl +++ b/src/ui_log.inl @@ -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"Previous log — kswitcher.log.1"); SendMessageW(choice, CB_SETCURSEL, 0, 0); - button(w, L"Refresh", LogRefresh, 310, 14, 100); - button(w, L"Copy selected", LogCopy, 424, 14, 140); - button(w, L"Clear log", LogClear, 578, 14, 100); + button(w, L"&Refresh", LogRefresh, 310, 14, 100); + button(w, L"&Copy selected", LogCopy, 424, 14, 140); + button(w, L"Clear &log", LogClear, 578, 14, 100); 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); - 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); - 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); - checkbox(w, L"Only problems", LogProblems, 752, 102, 166); - button(w, L"Reset filters", LogReset, 938, 98, 146); + checkbox(w, L"&Only problems", LogProblems, 752, 102, 166); + button(w, L"Reset &filters", LogReset, 938, 98, 146); INITCOMMONCONTROLSEX common{sizeof(common), ICC_LISTVIEW_CLASSES}; InitCommonControlsEx(&common); 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", L"Click a column to sort. Select an operation for details. Ctrl+C copies selected operations.", 0, 0, 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); layout->stretch = list; layout->details = details; diff --git a/src/ui_sync.inl b/src/ui_sync.inl index 084d73a..567d128 100644 --- a/src/ui_sync.inl +++ b/src/ui_sync.inl @@ -83,25 +83,25 @@ void showSyncOptions(HWND parent, App &app) HWND w = makeWindow(app, L"KSwitcher.Sync", L"Settings synchronization - OpenBao", syncProc, 660, 640); SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent); 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); - label(w, L"Server (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"Password", 18, 140, 160); edit(w, 604, 190, 136, 450, 25, ES_AUTOHSCROLL | ES_PASSWORD); + label(w, L"Se&rver (HTTPS)", 18, 60, 160); edit(w, 602, 190, 56, 450); value(w, 602, c.server); + 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"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"Profile path", 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"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount); + 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"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); value(w, 608, app.syncCommand()); - button(w, L"Save / Sign in / Sync", IDOK, 18, 452, 200); - button(w, L"Download remote", 610, 230, 452, 195); - button(w, L"Upload local", 611, 437, 452, 203); - button(w, L"Resolve individual conflicts...", 612, 18, 498, 300); + button(w, L"&Save / Sign in / Sync", IDOK, 18, 452, 200); + button(w, L"&Download remote", 610, 230, 452, 195); + button(w, L"&Upload local", 611, 437, 452, 203); + button(w, L"Resolve &individual conflicts...", 612, 18, 498, 300); label(w, L"Background check: every 10 minutes", 330, 502, 310); - button(w, L"Disconnect / forget password", 609, 18, 550, 290); - button(w, L"Close", IDCANCEL, 540, 550, 100); + button(w, L"Disconnect / &forget password", 609, 18, 550, 290); + button(w, L"&Close", IDCANCEL, 540, 550, 100); EnableWindow(parent, FALSE); SetTimer(w, 1, 1000, nullptr); showRequestedWindow(w); SetForegroundWindow(w); diff --git a/src/ui_sync_conflicts.inl b/src/ui_sync_conflicts.inl index a91ab29..bbb5c0f 100644 --- a/src/ui_sync_conflicts.inl +++ b/src/ui_sync_conflicts.inl @@ -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); 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); - 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 this &PC for selected setting", 623, 18, 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); - button(w, L"Apply choices", IDOK, 526, 488, 140); - button(w, L"Cancel", IDCANCEL, 678, 488, 100); + button(w, L"&Apply choices", IDOK, 526, 488, 140); + button(w, L"&Cancel", IDCANCEL, 678, 488, 100); SendDlgItemMessageW(w, 620, LB_SETCURSEL, 0, 0); showConflictValues(w, v); EnableWindow(parent, FALSE); showRequestedWindow(w); SetForegroundWindow(w); } diff --git a/src/update.h b/src/update.h index 0a7fcfd..35eeeb3 100644 --- a/src/update.h +++ b/src/update.h @@ -2,7 +2,7 @@ #include "platform.h" 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 UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000; diff --git a/tests/input.cpp b/tests/input.cpp index 5484bed..09e3334 100644 --- a/tests/input.cpp +++ b/tests/input.cpp @@ -627,6 +627,24 @@ int main(int argc, char **argv) { require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported"); ks::showSettings(app,false); 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); ks::saveSyncConfig(app.data,ks::SyncConfig{}); ks::showSettings(app,false);