Release v0.6.2: configurable screenshot editor and DPI fixes

This commit is contained in:
2026-10-29 12:41:48 +02:00
parent 21a09e33be
commit 6b180798f5
22 changed files with 231 additions and 99 deletions
+11 -1
View File
@@ -499,9 +499,19 @@ int main(int argc, char **argv) {
require(buttonAfter.top-buttonBefore.top==180 && buttonAfter.bottom-listAfter.bottom==buttonBefore.bottom-listBefore.bottom,"history actions stay below list after resize");
SetWindowPos(history,nullptr,0,0,historyFrame.right-historyFrame.left,historyFrame.bottom-historyFrame.top,SWP_NOMOVE|SWP_NOZORDER|SWP_NOACTIVATE);
require(EqualRect(&listBefore,&(listAfter=childRect(102))),"history resize roundtrip restores geometry");
// Reproduce a row height left over from another DPI without changing desktop settings.
UINT historyDpi=GetDpiForWindow(history);
for (int staleDpi : {96, 144, 192, 240}) {
SendDlgItemMessageW(history,102,LB_SETITEMHEIGHT,0,MulDiv(88,staleDpi,96));
SendMessageW(history,WM_DPICHANGED,MAKELONG(historyDpi,historyDpi),(LPARAM)&historyFrame);
require(SendDlgItemMessageW(history,102,LB_GETITEMHEIGHT,0,0)==MulDiv(88,historyDpi,96),
"DPI change updates owner-drawn history row height through layout subclass");
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(settings&&!IsWindowVisible(settings),"settings layout tested without changing user focus");
for(auto pair:{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{};
GetWindowRect(GetDlgItem(settings,pair.first),&first); GetWindowRect(GetDlgItem(settings,pair.second),&second);
require(!IntersectRect(&overlap,&first,&second),"capture hotkey and layout controls do not overlap");
+2 -2
View File
@@ -46,7 +46,7 @@ function Latest-Token {
}
$process=$null
try {
$process=Start-Process -FilePath $Executable -WindowStyle Hidden -PassThru
$process=Start-Process -FilePath $Executable -ArgumentList @("--auto-open-editor=0") -WindowStyle Hidden -PassThru
Wait-For { (Capture-Windows).Count -eq 1 } 'First selection did not open.'
Select-Region (Capture-Windows)[0]
if (!$process.WaitForExit(5000)) { throw 'Process remained alive after notification.' }
@@ -63,7 +63,7 @@ try {
if ($process.HasExited) { throw 'Process exited with an editor open.' }
Close-Editor $second
if (!$process.WaitForExit(5000) -or $process.ExitCode -ne 0) { throw 'Last editor did not exit process.' }
$process=Start-Process -FilePath $Executable -WindowStyle Hidden -PassThru
$process=Start-Process -FilePath $Executable -ArgumentList @("--auto-open-editor=0") -WindowStyle Hidden -PassThru
Wait-For { (Capture-Windows).Count -eq 1 } 'New process did not open selection.'
Send (Capture-Windows)[0] 0x100 27 # Esc cancels a selection only.
if (!$process.WaitForExit(5000) -or $process.ExitCode -ne 0) { throw 'Cancelling selection did not exit process.' }
+55
View File
@@ -0,0 +1,55 @@
// Redirect HKCU and the shortcut destination; never replace the user's registration.
#include "../src/screenshot_notifications.h"
#include <shlobj.h>
#include <iostream>
static std::wstring programs;
static HRESULT testKnownFolder(REFKNOWNFOLDERID, DWORD, HANDLE, PWSTR *out)
{
*out=(PWSTR)CoTaskMemAlloc((programs.size()+1)*sizeof(wchar_t));
if(!*out) return E_OUTOFMEMORY;
wcscpy(*out,programs.c_str()); return S_OK;
}
#define SHGetKnownFolderPath testKnownFolder
#include "../src/screenshot_activation.cpp"
#undef SHGetKnownFolderPath
int main()
{
CoInitializeEx(nullptr,COINIT_APARTMENTTHREADED);
auto suffix=std::to_wstring(GetCurrentProcessId());
auto keyPath=L"Software\\KSwitcherRegistrationTest-"+suffix;
auto directory=std::filesystem::temp_directory_path()/(L"KSwitcherRegistrationTest-"+suffix);
std::filesystem::create_directories(directory); programs=directory.wstring();
HKEY isolated{}; int result=0;
try {
if(RegCreateKeyExW(HKEY_CURRENT_USER,keyPath.c_str(),0,nullptr,0,KEY_ALL_ACCESS,nullptr,&isolated,nullptr)!=ERROR_SUCCESS ||
RegOverridePredefKey(HKEY_CURRENT_USER,isolated)!=ERROR_SUCCESS) throw std::runtime_error("registry isolation failed");
ks::registerScreenshotNotifications();
wchar_t command[32768]{}; DWORD size=sizeof(command);
auto path=L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32";
if(RegGetValueW(HKEY_CURRENT_USER,path,nullptr,RRF_RT_REG_SZ,nullptr,command,&size)!=ERROR_SUCCESS)
throw std::runtime_error("COM server was not registered");
auto helper=std::filesystem::path(ks::executable()).parent_path()/L"KSwitcher.Screenshot.exe";
if(command!=L"\""+helper.wstring()+L"\" --toast-activated") throw std::runtime_error("COM server targets wrong executable");
Microsoft::WRL::ComPtr<IShellLinkW> link;
if(FAILED(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)))) throw std::runtime_error("read shortcut");
Microsoft::WRL::ComPtr<IPersistFile> file; link.As(&file);
if(FAILED(file->Load((directory/L"KSwitcher Screenshot.lnk").c_str(),STGM_READ))) throw std::runtime_error("shortcut missing");
wchar_t target[32768]{}; link->GetPath(target,32768,nullptr,SLGP_RAWPATH);
if(target!=helper.wstring()) throw std::runtime_error("shortcut targets wrong executable");
Microsoft::WRL::ComPtr<IPropertyStore> properties; link.As(&properties);
PROPVARIANT id{}; properties->GetValue(PKEY_AppUserModel_ID,&id);
bool valid=id.vt==VT_LPWSTR && std::wstring(id.pwszVal)==L"HrynCo.KSwitcher.Screenshot";
PropVariantClear(&id);
if(!valid) throw std::runtime_error("shortcut identity missing");
properties->GetValue(PKEY_AppUserModel_ToastActivatorCLSID,&id);
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.
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);
if(isolated) RegCloseKey(isolated);
RegDeleteTreeW(HKEY_CURRENT_USER,keyPath.c_str());
std::filesystem::remove_all(directory);
CoUninitialize(); return result;
}
+30 -12
View File
@@ -164,18 +164,36 @@ int main()
SendMessageW(window,WM_CLOSE,0,0);
require(!IsWindow(window),"copied screenshot closes without confirmation");
static bool published=false;
app.notify=[](const ks::Clip &) { return published; };
ks::Capture selection(app);
selection.image=std::make_unique<ks::Surface>(100,100);
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);
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");
published=true; return 0;
});
require(published && selection.copied && !IsWindow(overlay),"selection copies before notification and closes without editor");
static bool notificationAccepted=false;
app.notify=[](const ks::Clip &) {
if(!published) throw std::runtime_error("notification preceded clipboard publication");
return notificationAccepted;
};
for(bool autoOpen : {true,false}) for (bool accepted : {true, false}) {
app.autoOpenEditor=autoOpen;
published=false;
notificationAccepted=accepted;
ks::Capture selection(app);
selection.image=std::make_unique<ks::Surface>(100,100);
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);
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");
published=true; return 0;
});
if(!autoOpen && accepted) {
require(published && !IsWindow(overlay),"notification-first closes selection after successful submission");
continue;
}
require(published && selection.copied && IsWindowVisible(overlay) && selection.editing,
"selection opens editor whether notification is accepted or rejected");
require(selection.image->width==60 && selection.image->height==40,
"automatically opened editor contains the selected region");
SendMessageW(overlay,WM_CLOSE,0,0);
require(!IsWindow(overlay),"automatically copied screenshot closes without confirmation");
}
DestroyWindow(app.window); app.window=nullptr;
CoUninitialize();
std::cout<<"Passed screenshot editor UI integration checks\n";
+4
View File
@@ -430,6 +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");
captureSettings.screenshotAutoOpenEditor=false;
require(!ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshotAutoOpenEditor,
"notification-first preference survives settings roundtrip");
require(ks::decodeSettings(ks::encodeSettings(captureSettings), {}).screenshot == captureSettings.screenshot,
"screen capture shortcut roundtrip");
captureSettings.screenshot = captureSettings.clipboard;