Release v0.6.3: fix capture banner ordering and check updates periodically
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
+10
-10
@@ -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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
e0c5a7b4f225ee32790f01ec916ae4706783b7e50b0bf6816c38ae2a6685d43d KSwitcher-win-x64.zip
|
||||
a43786b06878da0f069dbad0fa29587a736dd2d8904c7bc991f5cf0c0570a16f KSwitcher-win-x64.zip
|
||||
|
||||
+3
-3
@@ -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"
|
||||
|
||||
+6
-3
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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{};
|
||||
|
||||
+1
-1
@@ -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"([{«“‘\"";
|
||||
|
||||
@@ -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);
|
||||
|
||||
+1
-1
@@ -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 &);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &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();
|
||||
|
||||
+2
-1
@@ -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;
|
||||
|
||||
+21
-1
@@ -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{};
|
||||
|
||||
@@ -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");
|
||||
|
||||
+4
-1
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user