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
+39
View File
@@ -4,8 +4,17 @@
#include <commctrl.h>
#include <iostream>
#include <cmath>
LRESULT CALLBACK countControlMoves(HWND w, UINT message, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR data)
{
if (message == WM_WINDOWPOSCHANGING) ++*reinterpret_cast<int *>(data);
return DefSubclassProc(w, message, wp, lp);
}
namespace ks {
struct InputRegressionAccess {
static inline bool delivery = true;
static inline int notifications = 0;
static bool notify(const std::wstring &) { ++notifications; return delivery; }
static void notificationSetup(App &a) { a.updateNotifier = notify; }
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.
@@ -42,6 +51,7 @@ int main(int argc, char **argv) {
{
ks::App app(GetModuleHandleW(nullptr),true,temp);
ks::InputRegressionAccess::setup(app);
ks::InputRegressionAccess::notificationSetup(app);
// 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");
@@ -49,8 +59,18 @@ int main(int argc, char **argv) {
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::notifications==1,"successful background notification is submitted only once");
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");
ks::InputRegressionAccess::delivery=false;
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.2")==L"99.0.1","failed notification is not marked delivered");
require(KillTimer(app.window,5)!=FALSE,"failed notification schedules a retry");
ks::InputRegressionAccess::delivery=true;
require(ks::InputRegressionAccess::updateResult(app,L"available",L"99.0.2")==L"99.0.2","notification can succeed on retry");
auto toast=ks::updateToastXml(L"1&2<3\"");
require(toast.find(L"1&amp;2&lt;3&quot;")!=toast.npos,"update version is XML escaped");
require(toast.find(L"launch=\"check-update\"")!=toast.npos && toast.find(L"arguments=\"check-update\"")!=toast.npos,
"toast body and action both review the update instead of installing automatically");
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);
@@ -640,8 +660,27 @@ int main(int argc, char **argv) {
require(scroll.nMax+1>(int)scroll.nPage,"small settings window has vertical scrolling");
GetScrollInfo(settings,SB_HORZ,&scroll);
require(scroll.nMax+1>(int)scroll.nPage,"small settings window has horizontal scrolling");
int controlMoves = 0;
auto trackedControl = GetDlgItem(settings,201);
SetWindowSubclass(trackedControl,countControlMoves,99,(DWORD_PTR)&controlMoves);
SendMessageW(settings,WM_VSCROLL,SB_TOP,0);
SendMessageW(settings,WM_HSCROLL,SB_LEFT,0);
controlMoves=0;
ValidateRect(settings,nullptr);
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WHEEL_DELTA),0);
require(controlMoves==0 && !GetUpdateRect(settings,nullptr,FALSE),"wheel at top does not move controls or erase the window");
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WORD(short(-WHEEL_DELTA/2))),0);
require(controlMoves==0,"partial wheel step does not rearrange controls");
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,WORD(short(-WHEEL_DELTA/2))),0);
require(controlMoves==1,"wheel scroll batches each control movement once");
SendMessageW(settings,WM_VSCROLL,SB_BOTTOM,0);
SendMessageW(settings,WM_HSCROLL,SB_RIGHT,0);
controlMoves=0;
SendMessageW(settings,WM_MOUSEWHEEL,MAKEWPARAM(0,-WHEEL_DELTA),0);
SendMessageW(settings,WM_MOUSEHWHEEL,MAKEWPARAM(0,WHEEL_DELTA),0);
require(controlMoves==0,"wheel at bottom and right boundaries does not rearrange controls");
require((GetWindowLongPtrW(settings,GWL_STYLE)&WS_CLIPCHILDREN)!=0,"parent background excludes child controls");
RemoveWindowSubclass(trackedControl,countControlMoves,99);
GetClientRect(settings,&client);
GetWindowRect(GetDlgItem(settings,IDCANCEL),&cancel);
MapWindowPoints(nullptr,settings,(POINT*)&cancel,2);
+22
View File
@@ -3,6 +3,14 @@
#include <shlobj.h>
#include <iostream>
static std::wstring programs;
static std::wstring activationExecutable, activationCommand;
static int activationCalls{};
static BOOL WINAPI testCreateProcess(LPCWSTR path, LPWSTR command, LPSECURITY_ATTRIBUTES,
LPSECURITY_ATTRIBUTES, BOOL, DWORD, LPVOID, LPCWSTR, LPSTARTUPINFOW, LPPROCESS_INFORMATION)
{
++activationCalls; activationExecutable=path; activationCommand=command;
SetLastError(ERROR_CANCELLED); return FALSE; // Inspect activation without starting any program.
}
static HRESULT testKnownFolder(REFKNOWNFOLDERID, DWORD, HANDLE, PWSTR *out)
{
*out=(PWSTR)CoTaskMemAlloc((programs.size()+1)*sizeof(wchar_t));
@@ -10,8 +18,10 @@ static HRESULT testKnownFolder(REFKNOWNFOLDERID, DWORD, HANDLE, PWSTR *out)
wcscpy(*out,programs.c_str()); return S_OK;
}
#define SHGetKnownFolderPath testKnownFolder
#define CreateProcessW testCreateProcess
#include "../src/screenshot_activation.cpp"
#undef SHGetKnownFolderPath
#undef CreateProcessW
int main()
{
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
@@ -45,6 +55,18 @@ int main()
valid=id.vt==VT_CLSID && *id.puuid==ks::ActivatorId; PropVariantClear(&id);
if(!valid) throw std::runtime_error("shortcut activator missing");
ks::registerScreenshotNotifications(); // Registration is safe to repeat.
ks::Activator activator(helper.wstring(),false);
activator.Activate(L"HrynCo.KSwitcher.Screenshot",L"check-update",nullptr,0);
auto main=(helper.parent_path()/L"KSwitcher.exe").wstring();
if(activationCalls!=1 || activationExecutable!=main || activationCommand!=L"\""+main+L"\" --check-update")
throw std::runtime_error("update toast must open review, never install automatically");
if(activator.Activate(L"other-app",L"check-update",nullptr,0)!=E_INVALIDARG ||
activator.Activate(L"HrynCo.KSwitcher.Screenshot",L"--install",nullptr,0)!=E_INVALIDARG || activationCalls!=1)
throw std::runtime_error("invalid notification activation must not launch a process");
const wchar_t *token=L"12345678-1234-1234-1234-123456789abc";
activator.Activate(L"HrynCo.KSwitcher.Screenshot",token,nullptr,0);
if(activationCalls!=2 || activationExecutable!=helper.wstring() || activationCommand.find(token)==activationCommand.npos)
throw std::runtime_error("screenshot activation must retain its existing route");
std::cout<<"Passed isolated screenshot registration checks\n";
} catch(const std::exception &e) { std::cerr<<e.what()<<'\n'; result=1; }
RegOverridePredefKey(HKEY_CURRENT_USER,nullptr);