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
+2
View File
@@ -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)
+1
View File
@@ -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"([{«“‘\"";
+9 -4
View File
@@ -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,9 +813,13 @@ 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);
else edit(w,c);
// 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)
{
+1
View File
@@ -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 &);
+45
View File
@@ -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;
+4 -2
View File
@@ -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
View File
@@ -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) {
+4 -39
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
{