Release v0.6.9: notify about updates and fix Settings scroll flicker
This commit is contained in:
+14
-7
@@ -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")
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,¬ifier));
|
||||
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
@@ -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, ¤t);
|
||||
MapWindowPoints(nullptr, w, reinterpret_cast<POINT *>(¤t), 2);
|
||||
if (!EqualRect(¤t, &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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user