Release v0.6.2: configurable screenshot editor and DPI fixes
This commit is contained in:
@@ -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"([{«“‘\"";
|
||||
|
||||
+9
-4
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user