Release v0.6.2: configurable screenshot editor and DPI fixes
This commit is contained in:
@@ -75,9 +75,13 @@ 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 Windows shows a notification with a thumbnail.
|
||||
Click the notification to open the editor, including after the screenshot process has exited.
|
||||
If notifications are disabled or cannot be sent, the already-copied screenshot opens in the editor.
|
||||
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 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.
|
||||
Click the notification to reopen the original screenshot, including after the screenshot process has exited.
|
||||
Originals in the local notification cache are DPAPI-encrypted. Thumbnail PNGs live in a separate
|
||||
per-user cache under ProgramData, readable only by that user and SYSTEM, so the Windows notification
|
||||
broker can load them even when the profile directory is inaccessible to it.
|
||||
@@ -173,6 +177,8 @@ controls and fonts. Settings has a fixed form size, without manual resizing or m
|
||||
utility windows remain resizable. Windows stay within a remaining monitor's work area after display
|
||||
changes and provide scrollbars when their content does not fit. Tab navigation scrolls the focused
|
||||
field into view; Shift+wheel scrolls horizontally.
|
||||
Clipboard history also updates its preview row heights when DPI changes, keeping text,
|
||||
thumbnails and source labels aligned with the scaled fonts and spacing.
|
||||
|
||||
## Implemented
|
||||
|
||||
|
||||
Binary file not shown.
+19
-21
@@ -1,28 +1,26 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.4.1 — 2026-10-28
|
||||
## v0.6.2 — 2026-10-29
|
||||
|
||||
- Fix Settings spacing around Screen capture, automatic layout correction and layout controls.
|
||||
- Verify update checksums through .NET SHA-256 without requiring the PowerShell Get-FileHash command. This fixes updates from processes launched with a conflicting PowerShell module environment.
|
||||
- Capture a rectangular screen region with Win+Shift+S (configurable), with dimmed selection and independent editor windows.
|
||||
- Annotate screenshots with numbered markers, arrows, straight/freehand lines, highlights, rectangles, circles, ellipses and inline text.
|
||||
- Edit color, line style, arrowheads, thickness, font and size. Link arrow endpoints to markers and attach captions.
|
||||
- Captions follow line angle and keep their relative offset until manually rotated. Text rotation supports Shift snapping in 15-degree steps.
|
||||
- Move objects one pixel with arrow keys; clone with Ctrl+D; undo/redo edits. Numbered markers reuse gaps.
|
||||
- Compact Lucide toolbar, contextual status bar, pencil cursor and confirmation before discarding an uncopied screenshot. Escape no longer closes the editor.
|
||||
- Preserve the separate on-demand configurator and configurable capture shortcut.
|
||||
- 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.
|
||||
|
||||
Verified: 173 native checks, 379 input/rendering checks, screenshot editor UI integration checks,
|
||||
29 updater checks, including missing Get-FileHash coverage, and package validation.
|
||||
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.
|
||||
|
||||
**Upgrade from v0.3.0 or v0.4.0:** use Update in the tray menu. Both executables are supported.
|
||||
If an older updater reports missing `Get-FileHash`, restart KSwitcher normally from Explorer
|
||||
(outside a PowerShell 7 session) or install this ZIP manually once. v0.4.1 removes that dependency.
|
||||
**Upgrade from v0.2.3 or earlier:** install manually once because those updaters reject the additional configurator EXE.
|
||||
Exit KSwitcher and extract the entire ZIP into its installation folder. Keep personal `data/` and customized dictionaries.
|
||||
**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.
|
||||
For manual installation, exit KSwitcher and extract the entire ZIP into its installation folder.
|
||||
Keep personal `data/` and customized dictionaries.
|
||||
|
||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains both executables,
|
||||
offline dictionaries, documentation and license notices; no personal settings or clipboard history.
|
||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains the resident,
|
||||
configurator and screenshot executables, a release manifest, offline dictionaries, documentation
|
||||
and license notices; no personal settings or clipboard history.
|
||||
|
||||
Build and package:
|
||||
|
||||
@@ -31,5 +29,5 @@ Build and package:
|
||||
.\package.ps1 -PublishRelease
|
||||
```
|
||||
|
||||
Commit the archive, checksum and sources together. Publish both assets on a stable release whose
|
||||
tag matches the executable version.
|
||||
Commit the archive, checksum and sources together. Publish both assets on a stable
|
||||
`release-vX.Y.Z` tag matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||
|
||||
@@ -1 +1 @@
|
||||
9b2e2708eda40531659748c97d8c2d10f59cc98954bad7517b86e824fd889019 KSwitcher-win-x64.zip
|
||||
e0c5a7b4f225ee32790f01ec916ae4706783b7e50b0bf6816c38ae2a6685d43d KSwitcher-win-x64.zip
|
||||
|
||||
@@ -51,6 +51,14 @@ try {
|
||||
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
|
||||
& .\build\KSwitcherScreenshotUITests.exe
|
||||
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
|
||||
$registrationTest = Join-Path (Split-Path $executablePath) 'KSwitcherRegistrationTests.exe'
|
||||
try {
|
||||
& $compiler @common tests/screenshot_registration.cpp build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
|
||||
if ($LASTEXITCODE) { throw 'Screenshot registration test build failed' }
|
||||
& $registrationTest
|
||||
if ($LASTEXITCODE) { throw 'Screenshot registration tests failed' }
|
||||
} finally { Remove-Item -LiteralPath $registrationTest -ErrorAction SilentlyContinue }
|
||||
|
||||
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update.ps1 -Executable $executablePath
|
||||
if ($LASTEXITCODE) { throw 'Updater tests failed' }
|
||||
}
|
||||
|
||||
+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,1,0
|
||||
PRODUCTVERSION 0,6,1,0
|
||||
FILEVERSION 0,6,2,0
|
||||
PRODUCTVERSION 0,6,2,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.1"
|
||||
VALUE "FileVersion", "0.6.2"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -1453,6 +1453,8 @@ int App::execute()
|
||||
confirmUpdatedStartup(updateStartupEvent);
|
||||
if (startCommand)
|
||||
PostMessageW(window, WM_HOTKEY, startCommand, 0);
|
||||
try { registerScreenshotNotifications(); }
|
||||
catch(const std::exception &e) { log(L"Screenshot notification registration: "+wide(e.what())); }
|
||||
auto screenshotActivation=startScreenshotActivationListener();
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
|
||||
|
||||
@@ -48,6 +48,7 @@ struct Settings
|
||||
bool elevatedAutostart = false;
|
||||
bool autoCorrect = false;
|
||||
bool writeLog = true;
|
||||
bool screenshotAutoOpenEditor = true;
|
||||
int autoMaxRank = 0;
|
||||
int autoMinLength = 3;
|
||||
std::wstring autoPrefixChars = L"([{«“‘\"";
|
||||
|
||||
+8
-3
@@ -127,6 +127,7 @@ struct Capture
|
||||
std::unique_ptr<Surface> image;
|
||||
std::unique_ptr<Surface> dimmed, frame;
|
||||
bool editing{}, dragging{}, arrow{}, owned{}, copied{};
|
||||
bool autoOpenEditor;
|
||||
bool highlight{};
|
||||
bool freehand=true;
|
||||
bool straight{}, textTool{};
|
||||
@@ -161,7 +162,7 @@ struct Capture
|
||||
int hoveredTool=0;
|
||||
MarkPart hoveredPart=MarkPart::None;
|
||||
std::wstring statusText;
|
||||
explicit Capture(ScreenshotHost &a) : app(a) {}
|
||||
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
|
||||
~Capture() {
|
||||
if(pencilCursor) DestroyCursor(pencilCursor);
|
||||
if (font) DeleteObject(font);
|
||||
@@ -812,10 +813,14 @@ void finishSelection(HWND w,Capture &c,DWORD (*publish)(HWND,const Clip &)=write
|
||||
auto clip=screenshotClip(cropped.dc,cropped.width,cropped.height,{});
|
||||
publish(c.app.window,clip);
|
||||
c.copied=true;
|
||||
ShowWindow(w,SW_HIDE);
|
||||
if(c.app.notify(clip)) DestroyWindow(w);
|
||||
// A successful toast submission does not guarantee that Windows shows a banner.
|
||||
if(c.autoOpenEditor) edit(w,c);
|
||||
bool notified=c.app.notify(clip);
|
||||
if(!c.autoOpenEditor) {
|
||||
if(notified) DestroyWindow(w);
|
||||
else edit(w,c);
|
||||
}
|
||||
}
|
||||
LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto c = reinterpret_cast<Capture *>(GetWindowLongPtrW(w,GWLP_USERDATA));
|
||||
|
||||
@@ -8,6 +8,7 @@ struct ScreenshotHost {
|
||||
HWND window{};
|
||||
unsigned windows{};
|
||||
bool (*notify)(const Clip &){};
|
||||
bool autoOpenEditor = true;
|
||||
};
|
||||
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
|
||||
void showScreenshot(ScreenshotHost &);
|
||||
|
||||
@@ -1,10 +1,55 @@
|
||||
#include "screenshot_notifications.h"
|
||||
#include <atomic>
|
||||
#include <shlobj.h>
|
||||
#include <propkey.h>
|
||||
#include <propvarutil.h>
|
||||
#include <wrl/client.h>
|
||||
namespace ks {
|
||||
namespace {
|
||||
constexpr wchar_t AppId[]=L"HrynCo.KSwitcher.Screenshot";
|
||||
constexpr GUID ActivatorId{0xe3f78047,0xf0ad,0x49b6,{0x90,0x74,0x41,0x19,0x31,0x70,0x61,0x67}};
|
||||
}
|
||||
namespace {
|
||||
using Microsoft::WRL::ComPtr;
|
||||
void check(HRESULT result) {
|
||||
if(FAILED(result)) throw std::runtime_error("Screenshot registration failed: " + std::to_string((unsigned long)result));
|
||||
}
|
||||
void registryString(const std::wstring &path,const wchar_t *name,const std::wstring &value)
|
||||
{
|
||||
HKEY key{};
|
||||
if(RegCreateKeyExW(HKEY_CURRENT_USER,path.c_str(),0,nullptr,0,KEY_SET_VALUE,nullptr,&key,nullptr)!=ERROR_SUCCESS)
|
||||
throw std::runtime_error("Cannot register screenshot notifications");
|
||||
auto result=RegSetValueExW(key,name,0,REG_SZ,(const BYTE *)value.c_str(),DWORD((value.size()+1)*sizeof(wchar_t)));
|
||||
RegCloseKey(key);
|
||||
if(result!=ERROR_SUCCESS) throw std::runtime_error("Cannot register screenshot notification activation");
|
||||
}
|
||||
}
|
||||
void registerScreenshotNotifications()
|
||||
{
|
||||
auto exe=(std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe").wstring();
|
||||
if(!std::filesystem::exists(exe)) throw std::runtime_error("Screenshot helper is missing");
|
||||
registryString(L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32",nullptr,L"\""+exe+L"\" --toast-activated");
|
||||
registryString(L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32",L"ServerExecutable",exe);
|
||||
auto identity=std::wstring(L"Software\\Classes\\AppUserModelId\\")+AppId;
|
||||
registryString(identity,L"DisplayName",L"KSwitcher Screenshot");
|
||||
registryString(identity,L"IconUri",exe);
|
||||
registryString(identity,L"CustomActivator",L"{e3f78047-f0ad-49b6-9074-411931706167}");
|
||||
// A shortcut supplies desktop notification identity and its COM activation callback.
|
||||
ComPtr<IShellLinkW> link; check(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)));
|
||||
check(link->SetPath(exe.c_str())); check(link->SetIconLocation(exe.c_str(),0));
|
||||
ComPtr<IPropertyStore> properties; check(link.As(&properties));
|
||||
PROPVARIANT id{}; check(InitPropVariantFromString(AppId,&id));
|
||||
auto result=properties->SetValue(PKEY_AppUserModel_ID,id); PropVariantClear(&id); check(result);
|
||||
GUID activator=ActivatorId;
|
||||
PROPVARIANT clsid{}; clsid.vt=VT_CLSID; clsid.puuid=&activator;
|
||||
check(properties->SetValue(PKEY_AppUserModel_ToastActivatorCLSID,clsid));
|
||||
check(properties->Commit());
|
||||
PWSTR programs{}; check(SHGetKnownFolderPath(FOLDERID_Programs,0,nullptr,&programs));
|
||||
auto shortcut=std::filesystem::path(programs)/L"KSwitcher Screenshot.lnk"; CoTaskMemFree(programs);
|
||||
ComPtr<IPersistFile> file; check(link.As(&file)); check(file->Save(shortcut.c_str(),TRUE));
|
||||
// Notify the shell that notification identity/activation registration changed.
|
||||
SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,nullptr,nullptr);
|
||||
}
|
||||
bool validScreenshotToken(const std::wstring &id)
|
||||
{
|
||||
if(id.size()!=36) return false;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
#include "app.h"
|
||||
|
||||
namespace ks {
|
||||
void launchScreenshot(App &)
|
||||
void launchScreenshot(App &app)
|
||||
{
|
||||
auto path=std::filesystem::path(executable()).parent_path()/L"KSwitcher.Screenshot.exe";
|
||||
if (!std::filesystem::exists(path))
|
||||
throw std::runtime_error("KSwitcher.Screenshot.exe is missing. Extract the complete application package.");
|
||||
auto command=L"\""+path.wstring()+L"\"";
|
||||
auto command=L"\""+path.wstring()+L"\" --auto-open-editor="+
|
||||
(app.settings.screenshotAutoOpenEditor?L"1":L"0");
|
||||
if(app.settings.writeLog) command+=L" --write-log";
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
// Grant foreground rights before the child's first selection window opens.
|
||||
|
||||
+13
-3
@@ -14,6 +14,8 @@ LRESULT CALLBACK controller(HWND window,UINT message,WPARAM wp,LPARAM lp)
|
||||
SetWindowLongPtrW(window,GWLP_USERDATA,(LONG_PTR)host);
|
||||
}
|
||||
if(host && message==ks::ScreenshotRequest) {
|
||||
host->autoOpenEditor=(wp&1)!=0;
|
||||
ks::setLoggingEnabled((wp&2)!=0);
|
||||
try { ks::showScreenshot(*host); }
|
||||
catch(const std::exception &e) {
|
||||
MessageBoxW(window,ks::wide(e.what()).c_str(),L"Screen capture",MB_OK|MB_ICONERROR);
|
||||
@@ -45,7 +47,12 @@ 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;
|
||||
int argc{}; auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
||||
if(argc>1 && std::wstring(argv[argc-1])==L"--write-log") {
|
||||
writeLog=true; --argc;
|
||||
ks::setLoggingEnabled(true);
|
||||
}
|
||||
if(argc>=2 && std::wstring(argv[1])==L"--toast-activated") {
|
||||
LocalFree(argv); return ks::runScreenshotToastActivator();
|
||||
}
|
||||
@@ -54,7 +61,10 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||
int result=0; try { ks::registerScreenshotNotifications(); } catch(...) { result=1; }
|
||||
CoUninitialize(); return result;
|
||||
}
|
||||
if(argc>1) {
|
||||
if(argc==2 && (std::wstring(argv[1])==L"--auto-open-editor=0" ||
|
||||
std::wstring(argv[1])==L"--auto-open-editor=1")) {
|
||||
autoOpenEditor=std::wstring(argv[1])==L"--auto-open-editor=1";
|
||||
} else if(argc>1) {
|
||||
if(argc==3 && std::wstring(argv[1])==L"--open") {
|
||||
std::wstring uri=argv[2]; const std::wstring prefix=L"kswitcher-screenshot:";
|
||||
if(uri.starts_with(prefix)) token=uri.substr(prefix.size());
|
||||
@@ -74,7 +84,7 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||
DWORD pid{}; GetWindowThreadProcessId(existing,&pid);
|
||||
AllowSetForegroundWindow(pid);
|
||||
DWORD_PTR accepted{};
|
||||
if(SendMessageTimeoutW(existing,token.empty()?ks::ScreenshotRequest:WM_COPYDATA,0,
|
||||
if(SendMessageTimeoutW(existing,token.empty()?ks::ScreenshotRequest:WM_COPYDATA,token.empty()?(autoOpenEditor?1:0)|(writeLog?2:0):0,
|
||||
token.empty()?0:(LPARAM)&open,SMTO_ABORTIFHUNG,2000,&accepted) && accepted) {
|
||||
CloseHandle(mutex); return 0;
|
||||
}
|
||||
@@ -95,7 +105,7 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
|
||||
// Toast protocol activation may run unelevated while the editor was launched by an elevated resident.
|
||||
// WM_COPYDATA accepts only a validated cache GUID, never a path or executable command.
|
||||
ChangeWindowMessageFilterEx(host.window,WM_COPYDATA,MSGFLT_ALLOW,nullptr);
|
||||
if(token.empty()) PostMessageW(host.window,ks::ScreenshotRequest,0,0);
|
||||
if(token.empty()) PostMessageW(host.window,ks::ScreenshotRequest,(autoOpenEditor?1:0)|(writeLog?2:0),0);
|
||||
else SendMessageW(host.window,WM_COPYDATA,0,(LPARAM)&open);
|
||||
MSG message{};
|
||||
while((result=GetMessageW(&message,nullptr,0,0))>0) {
|
||||
|
||||
@@ -26,7 +26,6 @@ using Microsoft::WRL::ComPtr;
|
||||
namespace Notifications=ABI::Windows::UI::Notifications;
|
||||
namespace Xml=ABI::Windows::Data::Xml::Dom;
|
||||
constexpr wchar_t AppId[]=L"HrynCo.KSwitcher.Screenshot";
|
||||
constexpr GUID ActivatorId{0xe3f78047,0xf0ad,0x49b6,{0x90,0x74,0x41,0x19,0x31,0x70,0x61,0x67}};
|
||||
std::string notificationError;
|
||||
void check(HRESULT result) { if(FAILED(result)) { char message[100]; snprintf(message,sizeof(message),"Windows screenshot notification failed (0x%08lx)",(unsigned long)result); throw std::runtime_error(message); } }
|
||||
struct String {
|
||||
@@ -91,41 +90,6 @@ void cleanCache(const std::filesystem::path &path)
|
||||
}
|
||||
}
|
||||
}
|
||||
void registryString(const std::wstring &path,const wchar_t *name,const std::wstring &value)
|
||||
{
|
||||
HKEY key{};
|
||||
if(RegCreateKeyExW(HKEY_CURRENT_USER,path.c_str(),0,nullptr,0,KEY_SET_VALUE,nullptr,&key,nullptr)!=ERROR_SUCCESS)
|
||||
throw std::runtime_error("Cannot register screenshot notifications");
|
||||
auto result=RegSetValueExW(key,name,0,REG_SZ,(const BYTE *)value.c_str(),DWORD((value.size()+1)*sizeof(wchar_t)));
|
||||
RegCloseKey(key);
|
||||
if(result!=ERROR_SUCCESS) throw std::runtime_error("Cannot register screenshot notification activation");
|
||||
}
|
||||
void registerNotifications()
|
||||
{
|
||||
auto exe=executable();
|
||||
registryString(L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32",nullptr,L"\""+exe+L"\" --toast-activated");
|
||||
registryString(L"Software\\Classes\\CLSID\\{e3f78047-f0ad-49b6-9074-411931706167}\\LocalServer32",L"ServerExecutable",exe);
|
||||
auto identity=std::wstring(L"Software\\Classes\\AppUserModelId\\")+AppId;
|
||||
registryString(identity,L"DisplayName",L"KSwitcher Screenshot");
|
||||
registryString(identity,L"IconUri",exe);
|
||||
registryString(identity,L"CustomActivator",L"{e3f78047-f0ad-49b6-9074-411931706167}");
|
||||
// A shortcut supplies desktop notification identity and its COM activation callback.
|
||||
ComPtr<IShellLinkW> link; check(CoCreateInstance(CLSID_ShellLink,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&link)));
|
||||
check(link->SetPath(exe.c_str())); check(link->SetIconLocation(exe.c_str(),0));
|
||||
ComPtr<IPropertyStore> properties; check(link.As(&properties));
|
||||
PROPVARIANT id{}; check(InitPropVariantFromString(AppId,&id));
|
||||
auto result=properties->SetValue(PKEY_AppUserModel_ID,id); PropVariantClear(&id); check(result);
|
||||
GUID activator=ActivatorId;
|
||||
PROPVARIANT clsid{}; clsid.vt=VT_CLSID; clsid.puuid=&activator;
|
||||
check(properties->SetValue(PKEY_AppUserModel_ToastActivatorCLSID,clsid));
|
||||
check(properties->Commit());
|
||||
PWSTR programs{}; check(SHGetKnownFolderPath(FOLDERID_Programs,0,nullptr,&programs));
|
||||
auto shortcut=std::filesystem::path(programs)/L"KSwitcher Screenshot.lnk"; CoTaskMemFree(programs);
|
||||
ComPtr<IPersistFile> file; check(link.As(&file)); check(file->Save(shortcut.c_str(),TRUE));
|
||||
// The notification broker caches protocol handlers independently of ShellExecute.
|
||||
SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,nullptr,nullptr);
|
||||
check(SetCurrentProcessExplicitAppUserModelID(AppId));
|
||||
}
|
||||
void savePreview(const Clip &clip,const std::filesystem::path &path)
|
||||
{
|
||||
auto pixels=decodeImage(clip,360,220);
|
||||
@@ -161,7 +125,8 @@ bool notifyScreenshot(const Clip &clip)
|
||||
notificationError.clear();
|
||||
const char *stage="registration";
|
||||
try {
|
||||
registerNotifications();
|
||||
registerScreenshotNotifications();
|
||||
check(SetCurrentProcessExplicitAppUserModelID(AppId));
|
||||
stage="notification manager";
|
||||
ComPtr<Notifications::IToastNotificationManagerStatics> manager;
|
||||
String managerClass(L"Windows.UI.Notifications.ToastNotificationManager");
|
||||
@@ -212,9 +177,9 @@ bool notifyScreenshot(const Clip &clip)
|
||||
stage="show";
|
||||
check(notifier->Show(toast.Get()));
|
||||
return true;
|
||||
} catch(const std::exception &e) { notificationError=std::string(stage)+": "+e.what(); return false; } // Clipboard is already populated; fall back to the editor.
|
||||
} catch(const std::exception &e) { notificationError=std::string(stage)+": "+e.what(); log(wide(notificationError)); return false; } // Clipboard is already populated; fall back to the editor.
|
||||
}
|
||||
std::string screenshotNotificationError() { return notificationError; }
|
||||
void registerScreenshotNotifications() { registerNotifications(); }
|
||||
|
||||
|
||||
}
|
||||
|
||||
+3
-1
@@ -151,6 +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);
|
||||
if (j.contains("layouts"))
|
||||
{
|
||||
s.cycle.clear();
|
||||
@@ -205,7 +206,8 @@ Bytes encodeSettings(const Settings &s)
|
||||
{"translitHotkey", utf8(s.translit)},
|
||||
{"undoHotkey", utf8(s.undo)},
|
||||
{"clipboardHotkey", utf8(s.clipboard)},
|
||||
{"screenshotHotkey", utf8(s.screenshot)}};
|
||||
{"screenshotHotkey", utf8(s.screenshot)},
|
||||
{"screenshotAutoOpenEditor", s.screenshotAutoOpenEditor}};
|
||||
j["layouts"] = Json::array();
|
||||
for (auto &id : s.cycle)
|
||||
j["layouts"].push_back(utf8(id));
|
||||
|
||||
+6
-6
@@ -360,6 +360,9 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next);
|
||||
if (old)
|
||||
DeleteObject(old);
|
||||
// layoutProc consumes WM_DPICHANGED, so owner-drawn rows must be resized here.
|
||||
if (layout && layout->history)
|
||||
SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88));
|
||||
fitToMonitor(w, *(RECT *)lp);
|
||||
arrangeWindow(w);
|
||||
}
|
||||
@@ -747,12 +750,6 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
v->pending.clear();
|
||||
++v->generation;
|
||||
return 0;
|
||||
case WM_DPICHANGED:
|
||||
{
|
||||
resizeForDpi(w, wp, lp);
|
||||
SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88));
|
||||
return 0;
|
||||
}
|
||||
case WM_NCDESTROY:
|
||||
disposeFont(w);
|
||||
v->app->historyWindow = nullptr;
|
||||
@@ -822,6 +819,7 @@ void fillSettings(HWND w, SettingsView *v)
|
||||
check(w, 201, s.recent);
|
||||
check(w, 202, s.switchLayout);
|
||||
check(w, 208, s.writeLog);
|
||||
check(w, 209, s.screenshotAutoOpenEditor);
|
||||
check(w, 203, s.history);
|
||||
check(w, 204, s.autostart);
|
||||
check(w, 206, s.elevatedAutostart);
|
||||
@@ -872,6 +870,7 @@ Settings collectSettings(HWND w, SettingsView *v)
|
||||
s.recent = checked(w, 201);
|
||||
s.switchLayout = checked(w, 202);
|
||||
s.writeLog = checked(w, 208);
|
||||
s.screenshotAutoOpenEditor = checked(w, 209);
|
||||
s.history = checked(w, 203);
|
||||
s.autostart = checked(w, 204);
|
||||
s.elevatedAutostart = checked(w, 206);
|
||||
@@ -1662,6 +1661,7 @@ void showSettings(App &app, bool visible)
|
||||
label(w, names[i], 420, 48 + i * 30, 115);
|
||||
hotkeyEdit(app, w, 210 + i, 540, 44 + i * 30, 220);
|
||||
}
|
||||
checkbox(w, L"Open editor after screen capture", 209, 420, 224, 340);
|
||||
checkbox(w, L"Automatically correct layout (experimental)", 207, 18, 210, 395);
|
||||
label(w, L"Layout cycle — top to bottom", 18, 240, 400);
|
||||
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER, 220,
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.1";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.2";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
struct UpdateResult
|
||||
{
|
||||
|
||||
+11
-1
@@ -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");
|
||||
|
||||
@@ -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.' }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+20
-2
@@ -164,7 +164,15 @@ 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; };
|
||||
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);
|
||||
@@ -175,7 +183,17 @@ int main()
|
||||
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");
|
||||
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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user