Release v0.6.9: notify about updates and fix Settings scroll flicker

This commit is contained in:
2026-10-29 22:15:30 +02:00
parent 107d2f8852
commit 5c786e12a9
16 changed files with 196 additions and 44 deletions
+14 -7
View File
@@ -233,6 +233,7 @@ class Transaction
} // namespace
App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instance(h), smoke(test)
{
updateNotifier = notifyUpdateAvailable;
current = this;
data = dataRoot.empty() ? dataDirectory() : dataRoot;
std::filesystem::create_directories(data);
@@ -674,6 +675,7 @@ void App::dispatch(int command)
return;
try
{
if (command == 23) { checkUpdate(true); return; }
if (command == Screenshot)
{
if (!busy) { resetInput(); launchScreenshot(*this); }
@@ -1328,6 +1330,8 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
if (app->updateWorker.joinable()) app->updateWorker.join();
app->updateRunning = false;
app->installingUpdate = false;
log(L"Update check: " + result->state + (result->version.empty() ? L"" : L" " + result->version) +
(result->error.empty() ? L"" : L"; " + result->error));
if (result->state == L"prepared")
{
// The helper has verified and staged the package; it waits for process exit.
@@ -1345,13 +1349,16 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
}
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");
auto text = result->version + L" is available. Select Update in the tray menu to install.";
wcsncpy_s(icon.szInfo, text.c_str(), _TRUNCATE);
Shell_NotifyIconW(NIM_MODIFY, &icon);
if (app->updateNotifier(result->version))
{
app->notifiedUpdate = result->version;
log(L"Update notification submitted to Windows: " + result->version);
}
else
{
log(L"Update notification failed; retrying in 5 minutes: " + result->version);
SetTimer(hwnd, 5, UpdateNotificationRetryMs, nullptr);
}
}
}
else if (result->state == L"current")
+1
View File
@@ -95,6 +95,7 @@ class App
std::thread updateWorker;
bool updateRunning{}, installingUpdate{};
std::wstring availableUpdate, notifiedUpdate;
bool (*updateNotifier)(const std::wstring &){};
bool stop{};
UINT taskbarCreated{};
uint64_t pendingAutoVersion{};
+3
View File
@@ -45,12 +45,15 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
cmd = 21;
else if (arg == L"--exit")
cmd = 99;
else if (arg == L"--check-update")
cmd = 23;
LocalFree(argv);
if (existing)
{
HWND window = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
if (window)
{
if (cmd == 23) { DWORD pid{}; GetWindowThreadProcessId(window, &pid); AllowSetForegroundWindow(pid); }
COPYDATASTRUCT data{1, sizeof(cmd), &cmd};
DWORD_PTR result{};
SendMessageTimeoutW(window, WM_COPYDATA, 0, (LPARAM)&data, SMTO_ABORTIFHUNG, 1000, &result);
+6 -2
View File
@@ -83,8 +83,12 @@ public:
ULONG STDMETHODCALLTYPE AddRef() override { return ++references; }
ULONG STDMETHODCALLTYPE Release() override { auto count=--references; if(!count) delete this; return count; }
HRESULT STDMETHODCALLTYPE Activate(LPCWSTR app,LPCWSTR args,const NotificationInput *,ULONG) override {
if(!app || std::wstring(app)!=AppId || !args || !validScreenshotToken(args)) return E_INVALIDARG;
auto path=target; auto command=L"\""+path+L"\" --open \"kswitcher-screenshot:"+args+L"\"";
if(!app || std::wstring(app)!=AppId || !args) return E_INVALIDARG;
const bool update=std::wstring(args)==L"check-update";
if(!update && !validScreenshotToken(args)) return E_INVALIDARG;
auto path=update ? (std::filesystem::path(target).parent_path()/L"KSwitcher.exe").wstring() : target;
auto command=update ? L"\""+path+L"\" --check-update" :
L"\""+path+L"\" --open \"kswitcher-screenshot:"+args+L"\"";
STARTUPINFOW startup{sizeof(startup)}; PROCESS_INFORMATION process{};
auto ok=CreateProcessW(path.c_str(),command.data(),nullptr,nullptr,FALSE,CREATE_SUSPENDED,nullptr,nullptr,&startup,&process);
auto result=ok?S_OK:HRESULT_FROM_WIN32(GetLastError());
+39
View File
@@ -181,5 +181,44 @@ bool notifyScreenshot(const Clip &clip)
}
std::string screenshotNotificationError() { return notificationError; }
std::wstring updateToastXml(const std::wstring &version)
{
return L"<toast launch=\"check-update\"><visual><binding template=\"ToastGeneric\">"
L"<text>KSwitcher update available</text><text>Version " + escapeXml(version) +
L" is available. Click to review and install.</text></binding></visual>"
L"<actions><action content=\"Review update\" arguments=\"check-update\" activationType=\"foreground\"/>"
L"<action content=\"Later\" arguments=\"dismiss\" activationType=\"system\"/></actions></toast>";
}
bool notifyUpdateAvailable(const std::wstring &version)
{
try {
// Reuse the screenshot notification identity, Windows settings and COM activator.
registerScreenshotNotifications();
ComPtr<Notifications::IToastNotificationManagerStatics> manager;
String managerClass(L"Windows.UI.Notifications.ToastNotificationManager");
check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager)));
ComPtr<Notifications::IToastNotifier> notifier; String appId(AppId);
check(manager->CreateToastNotifierWithId(appId.value,&notifier));
Notifications::NotificationSetting setting{};
auto settingsResult=notifier->get_Setting(&setting);
if(settingsResult!=HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) check(settingsResult);
if(setting!=Notifications::NotificationSetting_Enabled)
throw std::runtime_error("Windows notifications for KSwitcher Screenshot are disabled");
String documentClass(L"Windows.Data.Xml.Dom.XmlDocument"); ComPtr<IInspectable> instance;
check(RoActivateInstance(documentClass.value,&instance));
ComPtr<Xml::IXmlDocument> document; check(instance.As(&document));
ComPtr<Xml::IXmlDocumentIO> io; check(document.As(&io));
String content(updateToastXml(version)); check(io->LoadXml(content.value));
ComPtr<Notifications::IToastNotificationFactory> factory; String toastClass(L"Windows.UI.Notifications.ToastNotification");
check(RoGetActivationFactory(toastClass.value,IID_PPV_ARGS(&factory)));
ComPtr<Notifications::IToastNotification> toast; check(factory->CreateToastNotification(document.Get(),&toast));
// Repeated submissions replace the existing update card in Notification Center.
ComPtr<Notifications::IToastNotification2> tagged; check(toast.As(&tagged));
String tag(L"update"),group(L"KSwitcher"); check(tagged->put_Tag(tag.value)); check(tagged->put_Group(group.value));
check(notifier->Show(toast.Get()));
return true;
} catch(const std::exception &e) { log(L"Update toast: "+wide(e.what())); return false; }
}
}
+43 -11
View File
@@ -71,17 +71,48 @@ void arrangeWindow(HWND w)
RECT client{}; GetClientRect(w, &client);
int extraWidth = layout->history ? std::max(0, int(client.right) - scale(w, layout->width)) : 0;
int extraHeight = layout->history ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
struct Move { HWND child; RECT rect; };
std::vector<Move> moves;
for (const auto &[child, r] : layout->controls)
{
int id = GetDlgCtrlID(child);
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x,
scale(w, r.top) - layout->y + (id >= 110 && id <= 114 ? extraHeight : 0),
scale(w, r.right - r.left) + (id == 101 || id == 102 ? extraWidth : 0),
scale(w, r.bottom - r.top) + (id == 102 ? extraHeight : 0),
SWP_NOACTIVATE | SWP_NOZORDER);
RECT next{scale(w, r.left) - layout->x,
scale(w, r.top) - layout->y + (id >= 110 && id <= 114 ? extraHeight : 0)};
next.right = next.left + scale(w, r.right - r.left) + (id == 101 || id == 102 ? extraWidth : 0);
next.bottom = next.top + scale(w, r.bottom - r.top) + (id == 102 ? extraHeight : 0);
RECT current{}; GetWindowRect(child, &current);
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(&current), 2);
if (!EqualRect(&current, &next)) moves.push_back({child, next});
}
if (!moves.empty())
{
auto batch = BeginDeferWindowPos((int)moves.size());
for (const auto &move : moves)
{
if (!batch) break;
auto r = move.rect;
batch = DeferWindowPos(batch, move.child, nullptr, r.left, r.top, r.right-r.left, r.bottom-r.top,
SWP_NOACTIVATE | SWP_NOZORDER);
}
if (!batch || !EndDeferWindowPos(batch))
for (const auto &move : moves)
{
auto r = move.rect;
SetWindowPos(move.child, nullptr, r.left, r.top, r.right-r.left, r.bottom-r.top,
SWP_NOACTIVATE | SWP_NOZORDER);
}
}
layout->arranging = false;
InvalidateRect(w, nullptr, TRUE);
}
void scrollWindow(HWND w, WindowLayout *layout, bool horizontal, int requested)
{
SCROLLINFO info{sizeof(info), SIF_RANGE | SIF_PAGE};
GetScrollInfo(w, horizontal ? SB_HORZ : SB_VERT, &info);
int next = std::clamp(requested, info.nMin, std::max(info.nMin, info.nMax - std::max(0, (int)info.nPage - 1)));
int &position = horizontal ? layout->x : layout->y;
if (position == next) return;
position = next;
arrangeWindow(w);
}
void revealControl(HWND child)
{
@@ -127,7 +158,7 @@ LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
bool vertical = msg == WM_VSCROLL;
SCROLLINFO info{sizeof(info), SIF_ALL};
GetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, &info);
int &position = vertical ? layout->y : layout->x;
int position = vertical ? layout->y : layout->x;
switch (LOWORD(wp))
{
case SB_LINEUP: position -= scale(w, 32); break;
@@ -138,7 +169,7 @@ LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
case SB_BOTTOM: position = info.nMax; break;
case SB_THUMBTRACK: case SB_THUMBPOSITION: position = info.nTrackPos; break;
}
arrangeWindow(w);
scrollWindow(w, layout, !vertical, position);
return 0;
}
if (layout->scrolling && (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL))
@@ -148,8 +179,9 @@ LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
remainder += GET_WHEEL_DELTA_WPARAM(wp);
int steps = remainder / WHEEL_DELTA;
remainder %= WHEEL_DELTA;
(horizontal ? layout->x : layout->y) += steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1);
arrangeWindow(w);
if (steps)
scrollWindow(w, layout, horizontal, (horizontal ? layout->x : layout->y) +
steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1));
return 0;
}
if (msg == WM_NCDESTROY)
@@ -276,7 +308,7 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
RegisterClassW(&wc);
UINT dpi = GetDpiForSystem();
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | extra;
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN | extra;
if (resizable) style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
POINT cursor{};
+4 -1
View File
@@ -2,9 +2,12 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.8";
inline constexpr wchar_t AppVersion[] = L"0.6.9";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
bool notifyUpdateAvailable(const std::wstring &version);
std::wstring updateToastXml(const std::wstring &version);
struct UpdateResult
{
std::wstring state, version, error;