diff --git a/README.md b/README.md index b5a14b6..8738632 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,11 @@ physical key positions and therefore reset recent-word tracking; selection conve Press **Win+Shift+S**, choose **Capture screen region…** in the tray menu, or run `KSwitcher.exe --capture`. Drag a rectangle on the dimmed, frozen desktop; the selected area stays at normal brightness. Escape cancels. Selection and annotation frames are buffered to avoid flicker. -The selected area is copied immediately and opens in the editor, even when Windows notifications -are disabled, suppressed or cannot be sent. Disable **Open editor after screen capture** in settings -to open it only by clicking the notification; an immediate notification error still opens the editor. +The selected area is copied immediately, the fullscreen selection is hidden, and a Windows notification +is sent. Click the notification to open the editor. Enable **Open editor after screen capture** in settings +to open it automatically; this option is off by default. An immediate notification error still opens the editor. +Existing saved preferences are preserved. Windows Do Not Disturb can send notifications directly to the +notification center without showing a banner. The preference applies to each new capture, including while other editors are open. KSwitcher also sends a notification with a thumbnail. Notification identity and activation are registered at resident startup and refreshed before sending; registration/submission errors are written to the log. @@ -158,7 +160,9 @@ Captures use ordinary clipboard history settings and limits. Protected video and Select **Update** in the tray menu to check the stable Releases of [this repository](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/releases). -A startup check also reports an available version, but never installs it automatically. +A background check runs shortly after startup and every 6 hours while KSwitcher is running. +It reports each available version once per session and never installs automatically. +Network failures stay in diagnostics; the next scheduled check retries. You can also check manually at any time. Confirm the offered version to download and install it. The updater uses the existing `KSwitcher-win-x64.zip` and `SHA256SUMS.txt` assets, checks the checksum, package contents, x64 executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen diff --git a/Release/KSwitcher-win-x64.zip b/Release/KSwitcher-win-x64.zip index b30a73a..435336e 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 6c97e73..5bb6c51 100644 --- a/Release/README.md +++ b/Release/README.md @@ -1,17 +1,17 @@ # Latest Windows release -## v0.6.2 — 2026-10-29 +## v0.6.3 — 2026-10-29 -- Add **Open editor after screen capture**, enabled by default. Disable it to use notification-first capture; an immediate notification error still opens the editor. -- Save the preference and apply it to each capture, including requests sent to an already running screenshot process. -- Register screenshot notification identity, shortcut and COM activation during resident startup, as well as before sending a notification. -- Log notification registration and submission errors when logging is enabled. -- Refresh clipboard history preview row heights when display DPI changes. +- Hide the fullscreen capture selection before submitting a notification when automatic editor opening is disabled. Previously the selection remained visible during submission, which could suppress the banner under Windows fullscreen notification rules. +- Default to capture → notification → click → editor. **Open editor after screen capture** is now off by default; explicitly saved preferences are preserved. +- Retain the editor fallback for immediate notification errors. +- Check for updates in the background every 6 hours after the startup check. Notify once per version per session, retry after network failures at the next interval, and install only after confirmation. -Verified: 175 native checks, 397 input/layout checks at 240 DPI, screenshot editor UI integration, -isolated notification registration checks, and 45 updater checks. A physical transition between -monitors with different DPI has not been tested. Successful notification submission does not -guarantee a visible Windows banner; asynchronous toast failures are not yet monitored. +Verified: 176 native checks, 403 input/layout checks at 240 DPI, screenshot editor UI integration, +isolated notification registration checks, and 45 updater checks. The new UI regression test failed +before the fix and passes afterward: the selection is hidden at notification submission. +Banner behavior still needs confirmation on the affected laptop. Windows Do Not Disturb can +send notifications directly to the notification center; asynchronous toast failures are not yet monitored. **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. diff --git a/Release/SHA256SUMS.txt b/Release/SHA256SUMS.txt index 7320f04..c93b28b 100644 --- a/Release/SHA256SUMS.txt +++ b/Release/SHA256SUMS.txt @@ -1 +1 @@ -e0c5a7b4f225ee32790f01ec916ae4706783b7e50b0bf6816c38ae2a6685d43d KSwitcher-win-x64.zip +a43786b06878da0f069dbad0fa29587a736dd2d8904c7bc991f5cf0c0570a16f KSwitcher-win-x64.zip diff --git a/resources/app.rc b/resources/app.rc index 79af45a..0a4192d 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,2,0 - PRODUCTVERSION 0,6,2,0 + FILEVERSION 0,6,3,0 + PRODUCTVERSION 0,6,3,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.2" + VALUE "FileVersion", "0.6.3" END END BLOCK "VarFileInfo" diff --git a/src/app.cpp b/src/app.cpp index d0aab56..c4ca01b 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1254,8 +1254,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES) app->checkUpdate(true, true, result->version); } - else + else if (app->notifiedUpdate != result->version) { + app->notifiedUpdate = result->version; NOTIFYICONDATAW icon{sizeof(icon)}; icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO; wcscpy_s(icon.szInfoTitle, L"KSwitcher update available"); @@ -1274,7 +1275,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) else if (result->interactive) MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR); else - diagnosticEvent(L"Startup update check failed: " + result->error); + diagnosticEvent(L"Background update check failed: " + result->error); return 0; } case WM_HOTKEY: @@ -1338,7 +1339,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) case WM_TIMER: if (wp == 5) { - KillTimer(hwnd, 5); + // After the initial 5-second check, repeat while the resident stays open. + // Rearm even when a check is already running or the previous one failed. + SetTimer(hwnd, 5, UpdateCheckIntervalMs, nullptr); app->checkUpdate(false); return 0; } diff --git a/src/app.h b/src/app.h index af82796..58b0362 100644 --- a/src/app.h +++ b/src/app.h @@ -85,7 +85,7 @@ class App std::thread worker; std::thread updateWorker; bool updateRunning{}, installingUpdate{}; - std::wstring availableUpdate; + std::wstring availableUpdate, notifiedUpdate; bool stop{}; UINT taskbarCreated{}; uint64_t pendingAutoVersion{}; diff --git a/src/core.h b/src/core.h index 2eec8a2..bcc7a53 100644 --- a/src/core.h +++ b/src/core.h @@ -48,7 +48,7 @@ struct Settings bool elevatedAutostart = false; bool autoCorrect = false; bool writeLog = true; - bool screenshotAutoOpenEditor = true; + bool screenshotAutoOpenEditor = false; int autoMaxRank = 0; int autoMinLength = 3; std::wstring autoPrefixChars = L"([{«“‘\""; diff --git a/src/screenshot.cpp b/src/screenshot.cpp index 4b0c0a3..3ec1056 100644 --- a/src/screenshot.cpp +++ b/src/screenshot.cpp @@ -815,6 +815,9 @@ void finishSelection(HWND w,Capture &c,DWORD (*publish)(HWND,const Clip &)=write c.copied=true; // A successful toast submission does not guarantee that Windows shows a banner. if(c.autoOpenEditor) edit(w,c); + // Leave fullscreen capture before submitting the toast: Windows may suppress + // banners while a fullscreen application is visible. + else ShowWindow(w,SW_HIDE); bool notified=c.app.notify(clip); if(!c.autoOpenEditor) { if(notified) DestroyWindow(w); diff --git a/src/screenshot.h b/src/screenshot.h index 3ba7bb7..6152e8a 100644 --- a/src/screenshot.h +++ b/src/screenshot.h @@ -8,7 +8,7 @@ struct ScreenshotHost { HWND window{}; unsigned windows{}; bool (*notify)(const Clip &){}; - bool autoOpenEditor = true; + bool autoOpenEditor = false; }; inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72; void showScreenshot(ScreenshotHost &); diff --git a/src/screenshot_main.cpp b/src/screenshot_main.cpp index b55af1e..5d38a68 100644 --- a/src/screenshot_main.cpp +++ b/src/screenshot_main.cpp @@ -47,7 +47,7 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp) int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int) { std::wstring token; - bool autoOpenEditor=true, writeLog=false; + bool autoOpenEditor=false, writeLog=false; int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc); if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") { writeLog=true; --argc; diff --git a/src/storage.cpp b/src/storage.cpp index 928908e..f839d3a 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -151,7 +151,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector &layouts) s.undo = str(j, "undoHotkey", s.undo); s.clipboard = str(j, "clipboardHotkey", s.clipboard); s.screenshot = str(j, "screenshotHotkey", s.screenshot); - s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", true); + s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", false); if (j.contains("layouts")) { s.cycle.clear(); diff --git a/src/update.h b/src/update.h index e9f0bb2..6164310 100644 --- a/src/update.h +++ b/src/update.h @@ -2,8 +2,9 @@ #include "platform.h" namespace ks { -inline constexpr wchar_t AppVersion[] = L"0.6.2"; +inline constexpr wchar_t AppVersion[] = L"0.6.3"; inline constexpr UINT UpdateMessage = WM_APP + 7; +inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000; struct UpdateResult { std::wstring state, version, error; diff --git a/tests/input.cpp b/tests/input.cpp index 66d6075..ee1b9e9 100644 --- a/tests/input.cpp +++ b/tests/input.cpp @@ -7,6 +7,20 @@ namespace ks { struct InputRegressionAccess { static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; } + static bool updateTimer(App &a) { + a.updateRunning=true; // Exercise timer scheduling without launching a network request. + App::windowProc(a.window,WM_TIMER,5,0); + bool first=KillTimer(a.window,5)!=FALSE; + App::windowProc(a.window,WM_TIMER,5,0); + bool repeated=KillTimer(a.window,5)!=FALSE; + a.updateRunning=false; + return first && repeated && !a.updateWorker.joinable(); + } + static std::wstring updateResult(App &a,const wchar_t *state,const wchar_t *version) { + auto result=new UpdateResult; result->state=state; result->version=version; + App::windowProc(a.window,UpdateMessage,0,(LPARAM)result); + return a.notifiedUpdate; + } static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); } }; } @@ -31,6 +45,12 @@ int main(int argc, char **argv) { // Message-only window: no hooks, synthetic input, clipboard changes or visible editor. app.window=CreateWindowExW(0,L"STATIC",L"Input tests",0,0,0,0,0,HWND_MESSAGE,nullptr,nullptr,nullptr); require(app.window!=nullptr,"message-only test window"); + require(ks::UpdateCheckIntervalMs==21600000,"automatic update interval is six hours"); + require(ks::InputRegressionAccess::updateTimer(app),"update timer rearms and avoids overlapping checks"); + require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","background update records announced version"); + require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.0")==L"99.0.0","same available version remains deduplicated"); + require(ks::InputRegressionAccess::updateResult(app,L"error",L"")==L"99.0.0","network error preserves announced version"); + require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.1")==L"99.0.1","new version can be announced after an error"); for(int variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon) for(int size:{22,44,55}) { auto handle=(HICON)LoadImageW(app.instance,MAKEINTRESOURCEW(icon<9?300+variant*10+icon:400+variant*10+icon-9),IMAGE_ICON,size,size,0); @@ -509,7 +529,7 @@ int main(int argc, char **argv) { require(EqualRect(&listBefore,&(listAfter=childRect(102))),"DPI refresh preserves history list geometry"); } auto settings=app.settingsWindow; - require(SendDlgItemMessageW(settings,209,BM_GETCHECK,0,0)==BST_CHECKED,"automatic screenshot editor checkbox defaults on"); + require(SendDlgItemMessageW(settings,209,BM_GETCHECK,0,0)==BST_UNCHECKED,"automatic screenshot editor checkbox defaults off"); require(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus"); for(auto pair:{std::pair{209,215},std::pair{209,207},std::pair{209,220},std::pair{209,232},std::pair{207,215},std::pair{207,232},std::pair{207,220},std::pair{232,230},std::pair{230,231},std::pair{231,253}}) { RECT first{},second{},overlap{}; diff --git a/tests/screenshot_ui.cpp b/tests/screenshot_ui.cpp index 53516b3..c609774 100644 --- a/tests/screenshot_ui.cpp +++ b/tests/screenshot_ui.cpp @@ -165,8 +165,12 @@ int main() require(!IsWindow(window),"copied screenshot closes without confirmation"); static bool published=false; static bool notificationAccepted=false; + static HWND notificationWindow{}; + static bool expectHidden=false; app.notify=[](const ks::Clip &) { if(!published) throw std::runtime_error("notification preceded clipboard publication"); + if(expectHidden && IsWindowVisible(notificationWindow)) + throw std::runtime_error("selection overlay is still visible when notification is sent"); return notificationAccepted; }; for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) { @@ -178,6 +182,8 @@ int main() PatBlt(selection.image->dc,0,0,100,100,WHITENESS); selection.start={10,20}; selection.end={70,60}; auto overlay=CreateWindowW(cls.lpszClassName,L"Selection test",WS_POPUP,0,0,100,100,app.window,nullptr,app.instance,&selection); + notificationWindow=overlay; expectHidden=!autoOpen; + ShowWindow(overlay,SW_SHOWNOACTIVATE); ks::finishSelection(overlay,selection,[](HWND,const ks::Clip &clip)->DWORD { auto pixels=ks::decodeImage(clip); if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection"); diff --git a/tests/tests.cpp b/tests/tests.cpp index ce0e5b5..98633c0 100644 --- a/tests/tests.cpp +++ b/tests/tests.cpp @@ -430,7 +430,10 @@ int main(int argc, char **argv) auto invalid = settings; auto captureSettings = settings; captureSettings.screenshot = L"Ctrl+Shift+F9"; - require(captureSettings.screenshotAutoOpenEditor,"editor opens by default"); + require(!captureSettings.screenshotAutoOpenEditor,"notification-first capture is the default"); + captureSettings.screenshotAutoOpenEditor=true; + require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor, + "explicit automatic editor preference survives settings roundtrip"); captureSettings.screenshotAutoOpenEditor=false; require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor, "notification-first preference survives settings roundtrip");