Release v0.6.3: fix capture banner ordering and check updates periodically

This commit is contained in:
2026-10-29 13:06:58 +02:00
parent 6b180798f5
commit d28e28c03c
16 changed files with 69 additions and 29 deletions
+8 -4
View File
@@ -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 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 `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. 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 The selected area is copied immediately, the fullscreen selection is hidden, and a Windows notification
are disabled, suppressed or cannot be sent. Disable **Open editor after screen capture** in settings is sent. Click the notification to open the editor. Enable **Open editor after screen capture** in settings
to open it only by clicking the notification; an immediate notification error still opens the editor. 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. 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 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. 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 Select **Update** in the tray menu to check the stable Releases of
[this repository](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/releases). [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 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 `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 executable identity and version, then closes and restarts KSwitcher. Downloads and staging happen
Binary file not shown.
+10 -10
View File
@@ -1,17 +1,17 @@
# Latest Windows release # 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. - 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.
- Save the preference and apply it to each capture, including requests sent to an already running screenshot process. - Default to capture → notification → click → editor. **Open editor after screen capture** is now off by default; explicitly saved preferences are preserved.
- Register screenshot notification identity, shortcut and COM activation during resident startup, as well as before sending a notification. - Retain the editor fallback for immediate notification errors.
- Log notification registration and submission errors when logging is enabled. - 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.
- Refresh clipboard history preview row heights when display DPI changes.
Verified: 175 native checks, 397 input/layout checks at 240 DPI, screenshot editor UI integration, Verified: 176 native checks, 403 input/layout checks at 240 DPI, screenshot editor UI integration,
isolated notification registration checks, and 45 updater checks. A physical transition between isolated notification registration checks, and 45 updater checks. The new UI regression test failed
monitors with different DPI has not been tested. Successful notification submission does not before the fix and passes afterward: the selection is hidden at notification submission.
guarantee a visible Windows banner; asynchronous toast failures are not yet monitored. 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 **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. v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
+1 -1
View File
@@ -1 +1 @@
e0c5a7b4f225ee32790f01ec916ae4706783b7e50b0bf6816c38ae2a6685d43d KSwitcher-win-x64.zip a43786b06878da0f069dbad0fa29587a736dd2d8904c7bc991f5cf0c0570a16f 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,2,0 FILEVERSION 0,6,3,0
PRODUCTVERSION 0,6,2,0 PRODUCTVERSION 0,6,3,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.2" VALUE "FileVersion", "0.6.3"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"
+6 -3
View File
@@ -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) if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES)
app->checkUpdate(true, true, result->version); app->checkUpdate(true, true, result->version);
} }
else else if (app->notifiedUpdate != result->version)
{ {
app->notifiedUpdate = result->version;
NOTIFYICONDATAW icon{sizeof(icon)}; NOTIFYICONDATAW icon{sizeof(icon)};
icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO; icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO;
wcscpy_s(icon.szInfoTitle, L"KSwitcher update available"); 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) else if (result->interactive)
MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR); MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR);
else else
diagnosticEvent(L"Startup update check failed: " + result->error); diagnosticEvent(L"Background update check failed: " + result->error);
return 0; return 0;
} }
case WM_HOTKEY: case WM_HOTKEY:
@@ -1338,7 +1339,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_TIMER: case WM_TIMER:
if (wp == 5) 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); app->checkUpdate(false);
return 0; return 0;
} }
+1 -1
View File
@@ -85,7 +85,7 @@ class App
std::thread worker; std::thread worker;
std::thread updateWorker; std::thread updateWorker;
bool updateRunning{}, installingUpdate{}; bool updateRunning{}, installingUpdate{};
std::wstring availableUpdate; std::wstring availableUpdate, notifiedUpdate;
bool stop{}; bool stop{};
UINT taskbarCreated{}; UINT taskbarCreated{};
uint64_t pendingAutoVersion{}; uint64_t pendingAutoVersion{};
+1 -1
View File
@@ -48,7 +48,7 @@ struct Settings
bool elevatedAutostart = false; bool elevatedAutostart = false;
bool autoCorrect = false; bool autoCorrect = false;
bool writeLog = true; bool writeLog = true;
bool screenshotAutoOpenEditor = true; bool screenshotAutoOpenEditor = false;
int autoMaxRank = 0; int autoMaxRank = 0;
int autoMinLength = 3; int autoMinLength = 3;
std::wstring autoPrefixChars = L"([{«“‘\""; std::wstring autoPrefixChars = L"([{«“‘\"";
+3
View File
@@ -815,6 +815,9 @@ void finishSelection(HWND w,Capture &c,DWORD (*publish)(HWND,const Clip &)=write
c.copied=true; c.copied=true;
// A successful toast submission does not guarantee that Windows shows a banner. // A successful toast submission does not guarantee that Windows shows a banner.
if(c.autoOpenEditor) edit(w,c); 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); bool notified=c.app.notify(clip);
if(!c.autoOpenEditor) { if(!c.autoOpenEditor) {
if(notified) DestroyWindow(w); if(notified) DestroyWindow(w);
+1 -1
View File
@@ -8,7 +8,7 @@ struct ScreenshotHost {
HWND window{}; HWND window{};
unsigned windows{}; unsigned windows{};
bool (*notify)(const Clip &){}; bool (*notify)(const Clip &){};
bool autoOpenEditor = true; bool autoOpenEditor = false;
}; };
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72; inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
void showScreenshot(ScreenshotHost &); void showScreenshot(ScreenshotHost &);
+1 -1
View File
@@ -47,7 +47,7 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp)
int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int) int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
{ {
std::wstring token; std::wstring token;
bool autoOpenEditor=true, writeLog=false; bool autoOpenEditor=false, writeLog=false;
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc); int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") { if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") {
writeLog=true; --argc; writeLog=true; --argc;
+1 -1
View File
@@ -151,7 +151,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
s.undo = str(j, "undoHotkey", s.undo); s.undo = str(j, "undoHotkey", s.undo);
s.clipboard = str(j, "clipboardHotkey", s.clipboard); s.clipboard = str(j, "clipboardHotkey", s.clipboard);
s.screenshot = str(j, "screenshotHotkey", s.screenshot); s.screenshot = str(j, "screenshotHotkey", s.screenshot);
s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", true); s.screenshotAutoOpenEditor = j.value("screenshotAutoOpenEditor", false);
if (j.contains("layouts")) if (j.contains("layouts"))
{ {
s.cycle.clear(); s.cycle.clear();
+2 -1
View File
@@ -2,8 +2,9 @@
#include "platform.h" #include "platform.h"
namespace ks 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 UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
struct UpdateResult struct UpdateResult
{ {
std::wstring state, version, error; std::wstring state, version, error;
+21 -1
View File
@@ -7,6 +7,20 @@
namespace ks { namespace ks {
struct InputRegressionAccess { struct InputRegressionAccess {
static void setup(App &a) { a.hookBindings = {{Toggle,{MOD_CONTROL,VK_F12}}, {Case,{MOD_CONTROL|MOD_SHIFT,'U'}}}; } 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); } 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. // 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); 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(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 variant=0;variant<3;++variant) for(int icon=0;icon<15;++icon)
for(int size:{22,44,55}) { 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); 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"); require(EqualRect(&listBefore,&(listAfter=childRect(102))),"DPI refresh preserves history list geometry");
} }
auto settings=app.settingsWindow; 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"); 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}}) { 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{}; RECT first{},second{},overlap{};
+6
View File
@@ -165,8 +165,12 @@ int main()
require(!IsWindow(window),"copied screenshot closes without confirmation"); require(!IsWindow(window),"copied screenshot closes without confirmation");
static bool published=false; static bool published=false;
static bool notificationAccepted=false; static bool notificationAccepted=false;
static HWND notificationWindow{};
static bool expectHidden=false;
app.notify=[](const ks::Clip &) { app.notify=[](const ks::Clip &) {
if(!published) throw std::runtime_error("notification preceded clipboard publication"); 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; return notificationAccepted;
}; };
for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) { 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); PatBlt(selection.image->dc,0,0,100,100,WHITENESS);
selection.start={10,20}; selection.end={70,60}; 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); 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 { ks::finishSelection(overlay,selection,[](HWND,const ks::Clip &clip)->DWORD {
auto pixels=ks::decodeImage(clip); auto pixels=ks::decodeImage(clip);
if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection"); if(pixels.width!=60 || pixels.height!=40) throw std::runtime_error("automatic copy did not crop selection");
+4 -1
View File
@@ -430,7 +430,10 @@ int main(int argc, char **argv)
auto invalid = settings; auto invalid = settings;
auto captureSettings = settings; auto captureSettings = settings;
captureSettings.screenshot = L"Ctrl+Shift+F9"; 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; captureSettings.screenshotAutoOpenEditor=false;
require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor, require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
"notification-first preference survives settings roundtrip"); "notification-first preference survives settings roundtrip");