Release v0.2.1: support remote input, safe updates and high-DPI windows
This commit is contained in:
+144
-58
@@ -256,6 +256,8 @@ App::App(HINSTANCE h, bool test, const std::filesystem::path &dataRoot) : instan
|
||||
}
|
||||
App::~App()
|
||||
{
|
||||
if (updateWorker.joinable())
|
||||
updateWorker.join();
|
||||
if (keyboardHook)
|
||||
UnhookWindowsHookEx(keyboardHook);
|
||||
if (mouseHook)
|
||||
@@ -413,23 +415,15 @@ void App::resetInput()
|
||||
run.reset();
|
||||
last.reset();
|
||||
}
|
||||
void App::keyboard(const KBDLLHOOKSTRUCT &k)
|
||||
void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
||||
{
|
||||
if (k.flags & LLKHF_INJECTED)
|
||||
if (ownKeyboardInput(k))
|
||||
return;
|
||||
DWORD vk = k.vkCode;
|
||||
if (vk == VK_LSHIFT || vk == VK_RSHIFT || vk == VK_LCONTROL || vk == VK_RCONTROL || vk == VK_LMENU ||
|
||||
if (vk == VK_SHIFT || vk == VK_CONTROL || vk == VK_MENU ||
|
||||
vk == VK_LSHIFT || vk == VK_RSHIFT || vk == VK_LCONTROL || vk == VK_RCONTROL || vk == VK_LMENU ||
|
||||
vk == VK_RMENU || vk == VK_LWIN || vk == VK_RWIN)
|
||||
return;
|
||||
UINT mods = 0;
|
||||
if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
|
||||
mods |= MOD_CONTROL;
|
||||
if (GetAsyncKeyState(VK_MENU) & 0x8000)
|
||||
mods |= MOD_ALT;
|
||||
if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
|
||||
mods |= MOD_SHIFT;
|
||||
if ((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000)
|
||||
mods |= MOD_WIN;
|
||||
std::lock_guard lock(stateMutex);
|
||||
if (isHotkey(vk, mods))
|
||||
return;
|
||||
@@ -448,7 +442,11 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k)
|
||||
return;
|
||||
}
|
||||
bool shift = (mods & MOD_SHIFT) != 0;
|
||||
wchar_t ch = vk == VK_SPACE ? L' ' : physicalKey(k.scanCode, shift);
|
||||
// VK_PACKET carries UTF-16 in scanCode, not a physical key position.
|
||||
DWORD scan = vk == VK_PACKET ? 0 : k.scanCode;
|
||||
if (!scan && vk != VK_PACKET)
|
||||
scan = MapVirtualKeyExW(vk, MAPVK_VK_TO_VSC_EX, layout);
|
||||
wchar_t ch = vk == VK_SPACE ? L' ' : physicalKey(scan, shift);
|
||||
if (ch && letter(ch) && (GetKeyState(VK_CAPITAL) & 1))
|
||||
ch = shift ? lower(std::wstring(1, ch))[0] : upper(std::wstring(1, ch))[0];
|
||||
if (!ch)
|
||||
@@ -492,85 +490,83 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k)
|
||||
}
|
||||
}
|
||||
}
|
||||
LRESULT CALLBACK App::keyProc(int code, WPARAM wp, LPARAM lp)
|
||||
bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
|
||||
{
|
||||
if (code == HC_ACTION && (((KBDLLHOOKSTRUCT *)lp)->flags & LLKHF_INJECTED) &&
|
||||
((KBDLLHOOKSTRUCT *)lp)->vkCode == 0xE8)
|
||||
return CallNextHookEx(nullptr, code, wp, lp); // Our shell-mask key is never recorded.
|
||||
if (code == HC_ACTION && current && current->hotkeyCapture &&
|
||||
GetForegroundWindow() == GetAncestor(current->hotkeyCapture, GA_ROOT))
|
||||
// Filter before capture, hotkey latching and typing history. External injected
|
||||
// down AND up events use exactly the same path as physical keyboard input.
|
||||
if (ownKeyboardInput(key) || installingUpdate)
|
||||
return false;
|
||||
if (hotkeyCapture && GetForegroundWindow() == GetAncestor(hotkeyCapture, GA_ROOT))
|
||||
{
|
||||
auto &key = *(KBDLLHOOKSTRUCT *)lp;
|
||||
bool modifier = key.vkCode == VK_LCONTROL || key.vkCode == VK_RCONTROL || key.vkCode == VK_LSHIFT ||
|
||||
bool modifier = key.vkCode == VK_CONTROL || key.vkCode == VK_SHIFT || key.vkCode == VK_MENU ||
|
||||
key.vkCode == VK_LCONTROL || key.vkCode == VK_RCONTROL || key.vkCode == VK_LSHIFT ||
|
||||
key.vkCode == VK_RSHIFT || key.vkCode == VK_LMENU || key.vkCode == VK_RMENU ||
|
||||
key.vkCode == VK_LWIN || key.vkCode == VK_RWIN;
|
||||
if (!modifier)
|
||||
{
|
||||
if (wp == WM_KEYUP || wp == WM_SYSKEYUP)
|
||||
current->hotkeyHook.update(key.vkCode, false, 0, {});
|
||||
if (wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN)
|
||||
if (!down)
|
||||
hotkeyHook.update(key.vkCode, false, 0, {});
|
||||
if (down)
|
||||
{
|
||||
UINT mods = 0;
|
||||
if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
|
||||
mods |= MOD_CONTROL;
|
||||
if (GetAsyncKeyState(VK_MENU) & 0x8000)
|
||||
mods |= MOD_ALT;
|
||||
if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
|
||||
mods |= MOD_SHIFT;
|
||||
if ((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000)
|
||||
mods |= MOD_WIN;
|
||||
if (mods & MOD_WIN)
|
||||
chord(0xE8);
|
||||
PostMessageW(current->hotkeyCapture, CaptureHotkeyMessage, key.vkCode, mods);
|
||||
PostMessageW(hotkeyCapture, CaptureHotkeyMessage, key.vkCode, mods);
|
||||
}
|
||||
return 1; // Record the chord without executing a global shortcut or typing into an editor.
|
||||
}
|
||||
}
|
||||
if (code == HC_ACTION && (((KBDLLHOOKSTRUCT *)lp)->flags & LLKHF_INJECTED))
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
if (code == HC_ACTION && (wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN) && current)
|
||||
if (down)
|
||||
{
|
||||
auto &key = *(KBDLLHOOKSTRUCT *)lp;
|
||||
UINT mods = 0;
|
||||
if (GetAsyncKeyState(VK_CONTROL) & 0x8000)
|
||||
mods |= MOD_CONTROL;
|
||||
if (GetAsyncKeyState(VK_MENU) & 0x8000)
|
||||
mods |= MOD_ALT;
|
||||
if (GetAsyncKeyState(VK_SHIFT) & 0x8000)
|
||||
mods |= MOD_SHIFT;
|
||||
if ((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000)
|
||||
mods |= MOD_WIN;
|
||||
auto action = current->hotkeyHook.update(key.vkCode, true, mods, current->hookBindings);
|
||||
auto action = hotkeyHook.update(key.vkCode, true, mods, hookBindings);
|
||||
if (action > 0)
|
||||
{
|
||||
// Tell the shell that Win participated in a chord, preventing Start
|
||||
// from opening on Win-up after we consumed V. 0xE8 is unassigned.
|
||||
if (mods & (MOD_WIN | MOD_ALT))
|
||||
chord(0xE8);
|
||||
PostMessageW(current->window, WM_HOTKEY, action, 0);
|
||||
PostMessageW(window, WM_HOTKEY, action, 0);
|
||||
}
|
||||
if (action != 0)
|
||||
return 1;
|
||||
current->keyboard(*(KBDLLHOOKSTRUCT *)lp);
|
||||
keyboard(key, mods);
|
||||
}
|
||||
else if (hotkeyHook.update(key.vkCode, false, 0, hookBindings) != 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
LRESULT CALLBACK App::keyProc(int code, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (code == HC_ACTION && current &&
|
||||
(wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN || wp == WM_KEYUP || wp == WM_SYSKEYUP))
|
||||
{
|
||||
// The current event has not updated asynchronous state yet; the preceding
|
||||
// modifier events (including MWB input) have. Own events must still reach Windows.
|
||||
UINT mods = 0;
|
||||
if (GetAsyncKeyState(VK_CONTROL) & 0x8000) mods |= MOD_CONTROL;
|
||||
if (GetAsyncKeyState(VK_MENU) & 0x8000) mods |= MOD_ALT;
|
||||
if (GetAsyncKeyState(VK_SHIFT) & 0x8000) mods |= MOD_SHIFT;
|
||||
if ((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000) mods |= MOD_WIN;
|
||||
if (current->handleKeyboard(*(KBDLLHOOKSTRUCT *)lp,
|
||||
wp == WM_KEYDOWN || wp == WM_SYSKEYDOWN, mods))
|
||||
return 1;
|
||||
}
|
||||
if (code == HC_ACTION && (wp == WM_KEYUP || wp == WM_SYSKEYUP) && current &&
|
||||
current->hotkeyHook.update(((KBDLLHOOKSTRUCT *)lp)->vkCode, false, 0, current->hookBindings) != 0)
|
||||
return 1;
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
}
|
||||
LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (code == HC_ACTION && current)
|
||||
{
|
||||
auto *e = (MSLLHOOKSTRUCT *)lp;
|
||||
if (!(e->flags & LLMHF_INJECTED) &&
|
||||
(wp == WM_LBUTTONDOWN || wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN || wp == WM_MOUSEWHEEL))
|
||||
// KSwitcher generates no mouse input. Remote clicks invalidate a typing
|
||||
// range just like local clicks, including while a transaction is pending.
|
||||
if (wp == WM_LBUTTONDOWN || wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN || wp == WM_MOUSEWHEEL)
|
||||
current->resetInput();
|
||||
}
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
}
|
||||
void App::dispatch(int command)
|
||||
{
|
||||
if (installingUpdate)
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (command == ShowHistory)
|
||||
@@ -602,6 +598,7 @@ void App::dispatch(int command)
|
||||
}
|
||||
void App::transform(int command, int snippet)
|
||||
{
|
||||
if (installingUpdate) return;
|
||||
const auto attempt = ++transformAttempt;
|
||||
const auto requestedAt = GetTickCount64();
|
||||
HWND target = GetForegroundWindow();
|
||||
@@ -957,6 +954,7 @@ void App::transform(int command, int snippet)
|
||||
}
|
||||
void App::pasteEntry(std::wstring id, bool plain)
|
||||
{
|
||||
if (installingUpdate) return;
|
||||
if (!history || busy.exchange(true))
|
||||
return;
|
||||
HWND target = pasteTarget;
|
||||
@@ -1095,6 +1093,9 @@ void App::trayMenu()
|
||||
AppendMenuW(menu, MF_POPUP, (UINT_PTR)pause, L"Pause clipboard history");
|
||||
AppendMenuW(menu, MF_STRING, 15, L"Open data folder");
|
||||
AppendMenuW(menu, MF_STRING, 18, L"View log…");
|
||||
AppendMenuW(menu, MF_STRING | (updateRunning ? MF_GRAYED : 0), 19,
|
||||
updateRunning ? L"Update (checking / preparing…)" :
|
||||
availableUpdate.empty() ? L"Update" : L"Update (new version available)");
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING, 16, L"About KSwitcher");
|
||||
AppendMenuW(menu, MF_STRING, 17, L"Exit");
|
||||
@@ -1133,7 +1134,7 @@ void App::trayMenu()
|
||||
break;
|
||||
case 16:
|
||||
MessageBoxW(window,
|
||||
L"KSwitcher 0.2.0\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
||||
L"KSwitcher 0.2.1\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
||||
L"Typlo by bitcodepro. MIT license.\nHistory is encrypted for your Windows "
|
||||
L"account.\nNo .NET, WebView or graphics framework.",
|
||||
L"About KSwitcher", MB_OK);
|
||||
@@ -1144,6 +1145,9 @@ void App::trayMenu()
|
||||
case 18:
|
||||
showLogs(*this);
|
||||
break;
|
||||
case 19:
|
||||
checkUpdate(true);
|
||||
break;
|
||||
case 20:
|
||||
pauseUntil = 0;
|
||||
break;
|
||||
@@ -1163,6 +1167,27 @@ void App::trayMenu()
|
||||
report(wide(e.what()));
|
||||
}
|
||||
}
|
||||
void App::checkUpdate(bool interactive, bool install, const std::wstring &version)
|
||||
{
|
||||
if (updateRunning) return;
|
||||
if (install && busy)
|
||||
{
|
||||
report(L"Wait for the current text operation to finish, then select Update again.");
|
||||
return;
|
||||
}
|
||||
if (updateWorker.joinable()) updateWorker.join();
|
||||
updateRunning = true;
|
||||
installingUpdate = install;
|
||||
updateWorker = std::thread([this, interactive, install, version]
|
||||
{
|
||||
auto result = std::make_unique<UpdateResult>();
|
||||
try { *result = runUpdate(install, version); }
|
||||
catch (const std::exception &e) { result->state = L"error"; result->error = wide(e.what()); }
|
||||
result->interactive = interactive;
|
||||
result->install = install;
|
||||
if (PostMessageW(window, UpdateMessage, 0, reinterpret_cast<LPARAM>(result.get()))) result.release();
|
||||
});
|
||||
}
|
||||
LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *app = current;
|
||||
@@ -1175,6 +1200,50 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
switch (msg)
|
||||
{
|
||||
case UpdateMessage:
|
||||
{
|
||||
std::unique_ptr<UpdateResult> result(reinterpret_cast<UpdateResult *>(lp));
|
||||
if (app->updateWorker.joinable()) app->updateWorker.join();
|
||||
app->updateRunning = false;
|
||||
app->installingUpdate = false;
|
||||
if (result->state == L"prepared")
|
||||
{
|
||||
// The helper has verified and staged the package; it waits for process exit.
|
||||
DestroyWindow(hwnd);
|
||||
}
|
||||
else if (result->state == L"available")
|
||||
{
|
||||
app->availableUpdate = result->version;
|
||||
if (result->interactive)
|
||||
{
|
||||
auto prompt = L"Install KSwitcher " + result->version +
|
||||
L"?\n\nThe program will close and restart. Settings, history and existing dictionaries are preserved.";
|
||||
if (MessageBoxW(hwnd, prompt.c_str(), L"KSwitcher Update", MB_YESNO | MB_ICONQUESTION) == IDYES)
|
||||
app->checkUpdate(true, true, result->version);
|
||||
}
|
||||
else
|
||||
{
|
||||
NOTIFYICONDATAW icon{sizeof(icon)};
|
||||
icon.hWnd = hwnd; icon.uID = 1; icon.uFlags = NIF_INFO; icon.dwInfoFlags = NIIF_INFO;
|
||||
wcscpy_s(icon.szInfoTitle, L"KSwitcher update available");
|
||||
auto text = result->version + L" is available. Select Update in the tray menu to install.";
|
||||
wcsncpy_s(icon.szInfo, text.c_str(), _TRUNCATE);
|
||||
Shell_NotifyIconW(NIM_MODIFY, &icon);
|
||||
}
|
||||
}
|
||||
else if (result->state == L"current")
|
||||
{
|
||||
app->availableUpdate.clear();
|
||||
if (result->interactive)
|
||||
MessageBoxW(hwnd, (std::wstring(L"No newer stable release is available.\nInstalled version: ") + AppVersion).c_str(),
|
||||
L"KSwitcher Update", MB_OK | MB_ICONINFORMATION);
|
||||
}
|
||||
else if (result->interactive)
|
||||
MessageBoxW(hwnd, (L"Update failed.\n\n" + result->error).c_str(), L"KSwitcher Update", MB_OK | MB_ICONERROR);
|
||||
else
|
||||
diagnosticEvent(L"Startup update check failed: " + result->error);
|
||||
return 0;
|
||||
}
|
||||
case WM_HOTKEY:
|
||||
app->dispatch((int)wp);
|
||||
return 0;
|
||||
@@ -1192,10 +1261,12 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
app->captureHistory();
|
||||
return 0;
|
||||
case AutoCorrectMessage:
|
||||
if (app->installingUpdate) return 0;
|
||||
app->pendingAutoVersion = (uint64_t)wp;
|
||||
SetTimer(hwnd, 3, 250, nullptr);
|
||||
return 0;
|
||||
case SnippetMessage:
|
||||
if (app->installingUpdate) return 0;
|
||||
app->transform(0, (int)wp);
|
||||
return 0;
|
||||
case HistoryMessage:
|
||||
@@ -1227,6 +1298,12 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
return TRUE;
|
||||
}
|
||||
case WM_TIMER:
|
||||
if (wp == 5)
|
||||
{
|
||||
KillTimer(hwnd, 5);
|
||||
app->checkUpdate(false);
|
||||
return 0;
|
||||
}
|
||||
if (wp == 4)
|
||||
{
|
||||
if (GetAsyncKeyState(VK_LBUTTON) & 0x8000)
|
||||
@@ -1238,7 +1315,7 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
if (wp == 3)
|
||||
{
|
||||
KillTimer(hwnd, 3);
|
||||
if (app->settings.autoCorrect && app->inputVersion.load() == app->pendingAutoVersion)
|
||||
if (!app->installingUpdate && app->settings.autoCorrect && app->inputVersion.load() == app->pendingAutoVersion)
|
||||
app->transform(AutoCorrect);
|
||||
return 0;
|
||||
}
|
||||
@@ -1255,6 +1332,11 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
return 0;
|
||||
case WM_CLOSE:
|
||||
if (app->updateRunning)
|
||||
{
|
||||
app->report(L"Wait for the update check or preparation to finish before exiting.");
|
||||
return 0;
|
||||
}
|
||||
if (app->busy)
|
||||
{
|
||||
app->report(L"Wait for the current text operation to finish before exiting.");
|
||||
@@ -1324,6 +1406,10 @@ int App::execute()
|
||||
report(registrationWarning);
|
||||
if (smoke)
|
||||
SetTimer(window, 2, 10000, nullptr);
|
||||
else
|
||||
SetTimer(window, 5, 5000, nullptr);
|
||||
if (keyboardHook && mouseHook)
|
||||
confirmUpdatedStartup(updateStartupEvent);
|
||||
if (startCommand)
|
||||
PostMessageW(window, WM_HOTKEY, startCommand, 0);
|
||||
MSG msg;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
#include "autocorrect.h"
|
||||
#include "storage.h"
|
||||
#include "update.h"
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <deque>
|
||||
@@ -49,6 +50,7 @@ class App
|
||||
std::wstring registrationWarning;
|
||||
bool smoke{};
|
||||
int startCommand{};
|
||||
std::wstring updateStartupEvent;
|
||||
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {});
|
||||
~App();
|
||||
int execute();
|
||||
@@ -65,16 +67,22 @@ class App
|
||||
void applySettings(const Settings &);
|
||||
void reloadLayouts();
|
||||
bool isHotkey(DWORD, UINT);
|
||||
void keyboard(const KBDLLHOOKSTRUCT &);
|
||||
void keyboard(const KBDLLHOOKSTRUCT &, UINT modifiers);
|
||||
bool handleKeyboard(const KBDLLHOOKSTRUCT &, bool down, UINT modifiers);
|
||||
void resetInput();
|
||||
void checkUpdate(bool interactive, bool install = false, const std::wstring &version = {});
|
||||
static App *current;
|
||||
|
||||
private:
|
||||
friend struct InputRegressionAccess;
|
||||
HHOOK keyboardHook{}, mouseHook{};
|
||||
std::mutex jobsMutex;
|
||||
std::condition_variable jobsReady;
|
||||
std::deque<std::function<void()>> jobs;
|
||||
std::thread worker;
|
||||
std::thread updateWorker;
|
||||
bool updateRunning{}, installingUpdate{};
|
||||
std::wstring availableUpdate;
|
||||
bool stop{};
|
||||
UINT taskbarCreated{};
|
||||
uint64_t pendingAutoVersion{};
|
||||
@@ -90,7 +98,7 @@ class App
|
||||
};
|
||||
void showHistory(App &);
|
||||
void refreshHistory(App &);
|
||||
void showSettings(App &);
|
||||
void showSettings(App &, bool visible = true);
|
||||
void showLogs(App &);
|
||||
void showSnippets(App &);
|
||||
void showShortcuts(App &);
|
||||
|
||||
+3
-1
@@ -67,7 +67,9 @@ int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
|
||||
try
|
||||
{
|
||||
ks::App app(instance, arg == L"--smoke-test");
|
||||
if (!arg.empty() && arg != L"--smoke-test")
|
||||
if (arg.starts_with(L"--update-ready-event="))
|
||||
app.updateStartupEvent = arg.substr(std::wstring(L"--update-ready-event=").size());
|
||||
else if (!arg.empty() && arg != L"--smoke-test")
|
||||
app.startCommand = cmd;
|
||||
result = app.execute();
|
||||
}
|
||||
|
||||
+21
-12
@@ -347,15 +347,30 @@ size_t Clip::size() const
|
||||
n += f.data.size();
|
||||
return n;
|
||||
}
|
||||
ULONG_PTR inputMarker()
|
||||
{
|
||||
static const char marker{};
|
||||
return reinterpret_cast<ULONG_PTR>(&marker);
|
||||
}
|
||||
bool ownKeyboardInput(const KBDLLHOOKSTRUCT &key)
|
||||
{
|
||||
return (key.flags & LLKHF_INJECTED) && key.dwExtraInfo == inputMarker();
|
||||
}
|
||||
INPUT keyboardInput(WORD key, DWORD flags)
|
||||
{
|
||||
INPUT input{};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = key;
|
||||
input.ki.dwFlags = flags;
|
||||
input.ki.dwExtraInfo = inputMarker();
|
||||
return input;
|
||||
}
|
||||
bool chord(WORD key, UINT mods)
|
||||
{
|
||||
std::vector<INPUT> inputs;
|
||||
auto add = [&](WORD k, bool up)
|
||||
{
|
||||
INPUT i{};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
i.ki.wVk = k;
|
||||
i.ki.dwFlags = up ? KEYEVENTF_KEYUP : 0;
|
||||
INPUT i = keyboardInput(k, up ? KEYEVENTF_KEYUP : 0);
|
||||
if (k == VK_LEFT || k == VK_RIGHT || k == VK_INSERT)
|
||||
i.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
inputs.push_back(i);
|
||||
@@ -406,10 +421,7 @@ bool selectPrevious(size_t count, const std::function<bool()> &valid)
|
||||
std::vector<INPUT> inputs;
|
||||
auto add = [&](WORD key, DWORD flags)
|
||||
{
|
||||
INPUT i{};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
i.ki.wVk = key;
|
||||
i.ki.dwFlags = flags;
|
||||
INPUT i = keyboardInput(key, flags);
|
||||
inputs.push_back(i);
|
||||
};
|
||||
add(VK_SHIFT, 0);
|
||||
@@ -437,10 +449,7 @@ void releaseModifiers()
|
||||
for (int k : {VK_LCONTROL, VK_RCONTROL, VK_LMENU, VK_RMENU, VK_LSHIFT, VK_RSHIFT, VK_LWIN, VK_RWIN})
|
||||
if (GetAsyncKeyState(k) & 0x8000)
|
||||
{
|
||||
INPUT i{};
|
||||
i.type = INPUT_KEYBOARD;
|
||||
i.ki.wVk = (WORD)k;
|
||||
i.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
INPUT i = keyboardInput((WORD)k, KEYEVENTF_KEYUP);
|
||||
inputs.push_back(i);
|
||||
}
|
||||
if (!inputs.empty())
|
||||
|
||||
@@ -36,6 +36,10 @@ Clip readClipboard(HWND, size_t limit = 32 * 1024 * 1024);
|
||||
DWORD writeClipboard(HWND, const Clip &);
|
||||
Clip textClip(const std::wstring &);
|
||||
bool chord(WORD key, UINT modifiers = 0);
|
||||
// Process-local marker, shared by every KSwitcher input producer (not an authentication token).
|
||||
ULONG_PTR inputMarker();
|
||||
bool ownKeyboardInput(const KBDLLHOOKSTRUCT &);
|
||||
INPUT keyboardInput(WORD key, DWORD flags = 0);
|
||||
bool needsSelectionDelay(HWND);
|
||||
bool selectPrevious(size_t count, const std::function<bool()> &valid);
|
||||
bool modifiersReleased();
|
||||
|
||||
+173
-8
@@ -2,6 +2,7 @@
|
||||
#include "app.h"
|
||||
#include "ocr.h"
|
||||
#include "richtext.h"
|
||||
#include "ui_geometry.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
#include <commdlg.h>
|
||||
@@ -26,6 +27,129 @@ HFONT font(HWND w)
|
||||
OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, DEFAULT_PITCH,
|
||||
L"Segoe UI");
|
||||
}
|
||||
struct WindowLayout
|
||||
{
|
||||
int width{}, height{}, x{}, y{};
|
||||
bool scrolling{}, arranging{};
|
||||
int wheelRemainder{}, horizontalWheelRemainder{};
|
||||
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
|
||||
};
|
||||
WindowLayout *windowLayoutState(HWND w)
|
||||
{
|
||||
return reinterpret_cast<WindowLayout *>(GetPropW(w, L"WindowLayout"));
|
||||
}
|
||||
RECT workArea(RECT rect)
|
||||
{
|
||||
MONITORINFO monitor{sizeof(monitor)};
|
||||
GetMonitorInfoW(MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST), &monitor);
|
||||
return monitor.rcWork;
|
||||
}
|
||||
void fitToMonitor(HWND w, RECT rect)
|
||||
{
|
||||
rect = fitWindowRect(rect, workArea(rect));
|
||||
SetWindowPos(w, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
|
||||
SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
}
|
||||
void arrangeWindow(HWND w)
|
||||
{
|
||||
auto *layout = windowLayoutState(w);
|
||||
if (!layout || !layout->scrolling || layout->arranging) return;
|
||||
layout->arranging = true;
|
||||
// Showing either scrollbar can make the other necessary. Let the client size settle.
|
||||
for (int pass = 0; pass < 3; ++pass)
|
||||
{
|
||||
RECT client{};
|
||||
GetClientRect(w, &client);
|
||||
SCROLLINFO horizontal{sizeof(horizontal), SIF_RANGE | SIF_PAGE | SIF_POS,
|
||||
0, scale(w, layout->width) - 1, (UINT)client.right, layout->x};
|
||||
SCROLLINFO vertical{sizeof(vertical), SIF_RANGE | SIF_PAGE | SIF_POS,
|
||||
0, scale(w, layout->height) - 1, (UINT)client.bottom, layout->y};
|
||||
layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE);
|
||||
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
|
||||
}
|
||||
for (const auto &[child, r] : layout->controls)
|
||||
SetWindowPos(child, nullptr, scale(w, r.left) - layout->x, scale(w, r.top) - layout->y,
|
||||
scale(w, r.right - r.left), scale(w, r.bottom - r.top), SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
layout->arranging = false;
|
||||
InvalidateRect(w, nullptr, TRUE);
|
||||
}
|
||||
void revealControl(HWND child)
|
||||
{
|
||||
HWND parent = GetParent(child);
|
||||
auto *layout = windowLayoutState(parent);
|
||||
if (!layout || !layout->scrolling) return;
|
||||
RECT r{}, client{};
|
||||
GetWindowRect(child, &r);
|
||||
MapWindowPoints(nullptr, parent, reinterpret_cast<POINT *>(&r), 2);
|
||||
GetClientRect(parent, &client);
|
||||
if (r.left < 0) layout->x += r.left;
|
||||
else if (r.right > client.right) layout->x += r.right - client.right;
|
||||
if (r.top < 0) layout->y += r.top;
|
||||
else if (r.bottom > client.bottom) layout->y += r.bottom - client.bottom;
|
||||
arrangeWindow(parent);
|
||||
}
|
||||
LRESULT CALLBACK scrollChildProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
if (msg == WM_SETFOCUS) revealControl(w);
|
||||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(w, scrollChildProc, 2);
|
||||
return DefSubclassProc(w, msg, wp, lp);
|
||||
}
|
||||
void resizeForDpi(HWND, WPARAM, LPARAM);
|
||||
LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DWORD_PTR)
|
||||
{
|
||||
auto *layout = windowLayoutState(w);
|
||||
if (!layout) return DefSubclassProc(w, msg, wp, lp);
|
||||
if (msg == WM_DPICHANGED)
|
||||
{
|
||||
resizeForDpi(w, wp, lp);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_SIZE) arrangeWindow(w);
|
||||
if (msg == WM_DISPLAYCHANGE || msg == WM_SETTINGCHANGE || (msg == WM_SHOWWINDOW && wp))
|
||||
{
|
||||
RECT rect{};
|
||||
GetWindowRect(w, &rect);
|
||||
fitToMonitor(w, rect);
|
||||
arrangeWindow(w);
|
||||
}
|
||||
if (layout->scrolling && (msg == WM_VSCROLL || msg == WM_HSCROLL))
|
||||
{
|
||||
bool vertical = msg == WM_VSCROLL;
|
||||
SCROLLINFO info{sizeof(info), SIF_ALL};
|
||||
GetScrollInfo(w, vertical ? SB_VERT : SB_HORZ, &info);
|
||||
int &position = vertical ? layout->y : layout->x;
|
||||
switch (LOWORD(wp))
|
||||
{
|
||||
case SB_LINEUP: position -= scale(w, 32); break;
|
||||
case SB_LINEDOWN: position += scale(w, 32); break;
|
||||
case SB_PAGEUP: position -= (int)info.nPage; break;
|
||||
case SB_PAGEDOWN: position += (int)info.nPage; break;
|
||||
case SB_TOP: position = 0; break;
|
||||
case SB_BOTTOM: position = info.nMax; break;
|
||||
case SB_THUMBTRACK: case SB_THUMBPOSITION: position = info.nTrackPos; break;
|
||||
}
|
||||
arrangeWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (layout->scrolling && (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL))
|
||||
{
|
||||
bool horizontal = msg == WM_MOUSEHWHEEL || (GET_KEYSTATE_WPARAM(wp) & MK_SHIFT);
|
||||
int &remainder = horizontal ? layout->horizontalWheelRemainder : layout->wheelRemainder;
|
||||
remainder += GET_WHEEL_DELTA_WPARAM(wp);
|
||||
int steps = remainder / WHEEL_DELTA;
|
||||
remainder %= WHEEL_DELTA;
|
||||
(horizontal ? layout->x : layout->y) += steps * scale(w, 48) * (msg == WM_MOUSEHWHEEL ? 1 : -1);
|
||||
arrangeWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY)
|
||||
{
|
||||
RemovePropW(w, L"WindowLayout");
|
||||
RemoveWindowSubclass(w, layoutProc, 2);
|
||||
delete layout;
|
||||
}
|
||||
return DefSubclassProc(w, msg, wp, lp);
|
||||
}
|
||||
HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style, int id, int x, int y,
|
||||
int width, int height)
|
||||
{
|
||||
@@ -34,6 +158,12 @@ HWND control(HWND parent, const wchar_t *cls, const wchar_t *title, DWORD style,
|
||||
scale(parent, width), scale(parent, height), parent, (HMENU)(INT_PTR)id,
|
||||
GetModuleHandleW(nullptr), nullptr);
|
||||
SendMessageW(w, WM_SETFONT, (WPARAM)GetPropW(parent, L"Font"), TRUE);
|
||||
if (auto *layout = windowLayoutState(parent); layout && layout->scrolling)
|
||||
{
|
||||
layout->controls.push_back({w, {x, y, x + width, y + height}});
|
||||
SetWindowSubclass(w, scrollChildProc, 2, 0);
|
||||
arrangeWindow(parent);
|
||||
}
|
||||
return w;
|
||||
}
|
||||
void label(HWND w, const wchar_t *text, int x, int y, int width = 200)
|
||||
@@ -136,13 +266,31 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
|
||||
RegisterClassW(&wc);
|
||||
UINT dpi = GetDpiForSystem();
|
||||
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | extra;
|
||||
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_THICKFRAME | WS_MAXIMIZEBOX | extra;
|
||||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||||
POINT cursor{};
|
||||
GetCursorPos(&cursor);
|
||||
MONITORINFO monitor{sizeof(monitor)};
|
||||
GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor);
|
||||
HWND w =
|
||||
CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
CreateWindowExW(WS_EX_APPWINDOW, cls, title, style, monitor.rcWork.left, monitor.rcWork.top,
|
||||
rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, app.instance, &app);
|
||||
if (!w)
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot create native window")));
|
||||
// The system DPI may still describe the old primary display. The HWND tells
|
||||
// us the actual destination DPI, and must size the frame AND its controls.
|
||||
dpi = GetDpiForWindow(w);
|
||||
rect = {0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
|
||||
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
|
||||
int outerWidth = rect.right - rect.left, outerHeight = rect.bottom - rect.top;
|
||||
int left = monitor.rcWork.left + std::max(0L, (monitor.rcWork.right - monitor.rcWork.left - outerWidth) / 2);
|
||||
int top = monitor.rcWork.top + std::max(0L, (monitor.rcWork.bottom - monitor.rcWork.top - outerHeight) / 2);
|
||||
SetPropW(w, L"WindowLayout", new WindowLayout{width, height, 0, 0, !(extra & WS_THICKFRAME)});
|
||||
SetWindowSubclass(w, layoutProc, 2, 0);
|
||||
RECT initial = fitWindowRect({left, top, left + outerWidth, top + outerHeight}, monitor.rcWork);
|
||||
SetWindowPos(w, nullptr, initial.left, initial.top, initial.right - initial.left, initial.bottom - initial.top,
|
||||
SWP_NOACTIVATE | SWP_NOZORDER);
|
||||
arrangeWindow(w);
|
||||
SetWindowTextW(w, title);
|
||||
auto smallIcon = (HICON)LoadImageW(app.instance, MAKEINTRESOURCEW(IDI_KSWITCHER), IMAGE_ICON,
|
||||
GetSystemMetricsForDpi(SM_CXSMICON, dpi),
|
||||
@@ -154,6 +302,12 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
|
||||
}
|
||||
void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
// Creation may send WM_DPICHANGED before controls and the original font exist.
|
||||
if (!GetPropW(w, L"Dpi"))
|
||||
{
|
||||
fitToMonitor(w, *(RECT *)lp);
|
||||
return;
|
||||
}
|
||||
int previous = (int)(UINT_PTR)GetPropW(w, L"Dpi");
|
||||
int next = LOWORD(wp);
|
||||
if (!previous)
|
||||
@@ -166,7 +320,8 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
int previous, next;
|
||||
HFONT font;
|
||||
} state{w, previous, next, replacement};
|
||||
EnumChildWindows(
|
||||
auto *layout = windowLayoutState(w);
|
||||
if (!layout || !layout->scrolling) EnumChildWindows(
|
||||
w,
|
||||
[](HWND child, LPARAM data) -> BOOL
|
||||
{
|
||||
@@ -183,12 +338,19 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
return TRUE;
|
||||
},
|
||||
(LPARAM)&state);
|
||||
else
|
||||
{
|
||||
layout->x = MulDiv(layout->x, next, previous);
|
||||
layout->y = MulDiv(layout->y, next, previous);
|
||||
for (auto &[child, rect] : layout->controls)
|
||||
SendMessageW(child, WM_SETFONT, (WPARAM)replacement, TRUE);
|
||||
}
|
||||
SetPropW(w, L"Font", replacement);
|
||||
SetPropW(w, L"Dpi", (HANDLE)(UINT_PTR)next);
|
||||
if (old)
|
||||
DeleteObject(old);
|
||||
auto *r = (RECT *)lp;
|
||||
SetWindowPos(w, nullptr, r->left, r->top, r->right - r->left, r->bottom - r->top, SWP_NOZORDER);
|
||||
fitToMonitor(w, *(RECT *)lp);
|
||||
arrangeWindow(w);
|
||||
}
|
||||
std::filesystem::path pickFile(HWND owner, bool save)
|
||||
{
|
||||
@@ -1447,7 +1609,7 @@ void showLogs(App &app)
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
void showSettings(App &app)
|
||||
void showSettings(App &app, bool visible)
|
||||
{
|
||||
if (app.settingsWindow)
|
||||
{
|
||||
@@ -1505,8 +1667,11 @@ void showSettings(App &app)
|
||||
button(w, L"Save", IDOK, 548, 606, 100);
|
||||
button(w, L"Cancel", IDCANCEL, 660, 606, 100);
|
||||
fillSettings(w, (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
if (visible)
|
||||
{
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
}
|
||||
void showSnippets(App &app)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include <algorithm>
|
||||
namespace ks
|
||||
{
|
||||
inline RECT fitWindowRect(RECT desired, RECT work)
|
||||
{
|
||||
LONG width = std::clamp<LONG>(desired.right - desired.left, 1, std::max<LONG>(1, work.right - work.left));
|
||||
LONG height = std::clamp<LONG>(desired.bottom - desired.top, 1, std::max<LONG>(1, work.bottom - work.top));
|
||||
LONG x = std::clamp(desired.left, work.left, work.right - width);
|
||||
LONG y = std::clamp(desired.top, work.top, work.bottom - height);
|
||||
return {x, y, x + width, y + height};
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
#include "update.h"
|
||||
#include "json.hpp"
|
||||
|
||||
namespace ks
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct Handle
|
||||
{
|
||||
HANDLE value{};
|
||||
~Handle() { if (value) CloseHandle(value); }
|
||||
};
|
||||
std::wstring quoteArgument(const std::wstring &value)
|
||||
{
|
||||
// Windows argv escaping, including a path ending with a backslash.
|
||||
std::wstring out = L"\"";
|
||||
size_t slashes = 0;
|
||||
for (auto ch : value)
|
||||
{
|
||||
if (ch == L'\\') { ++slashes; continue; }
|
||||
out.append(ch == L'"' ? slashes * 2 + 1 : slashes, L'\\');
|
||||
slashes = 0;
|
||||
out += ch;
|
||||
}
|
||||
out.append(slashes * 2, L'\\');
|
||||
return out + L'"';
|
||||
}
|
||||
}
|
||||
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion)
|
||||
{
|
||||
UpdateResult result;
|
||||
GUID guid{};
|
||||
if (FAILED(CoCreateGuid(&guid))) throw std::runtime_error("Cannot create update identifier");
|
||||
wchar_t id[40]{};
|
||||
StringFromGUID2(guid, id, 40);
|
||||
auto work = std::filesystem::temp_directory_path() / (L"KSwitcher-update-" + std::wstring(id));
|
||||
std::filesystem::create_directory(work);
|
||||
bool detached = false;
|
||||
Handle child;
|
||||
try
|
||||
{
|
||||
HRSRC resource = FindResourceW(nullptr, MAKEINTRESOURCEW(201), RT_RCDATA);
|
||||
HGLOBAL loaded = resource ? LoadResource(nullptr, resource) : nullptr;
|
||||
auto bytes = loaded ? static_cast<const unsigned char *>(LockResource(loaded)) : nullptr;
|
||||
if (!bytes) throw std::runtime_error("The updater resource is missing");
|
||||
atomicWrite(work / L"update.ps1", Bytes(bytes, bytes + SizeofResource(nullptr, resource)));
|
||||
std::wstring readyName = L"Local\\KSwitcher.Update.Ready." + std::wstring(id);
|
||||
Handle ready{CreateEventW(nullptr, TRUE, FALSE, readyName.c_str())};
|
||||
if (!ready.value) throw std::runtime_error("Cannot create updater handshake");
|
||||
wchar_t system[MAX_PATH]{};
|
||||
if (!GetSystemDirectoryW(system, MAX_PATH)) throw std::runtime_error("Cannot locate Windows PowerShell");
|
||||
auto powershell = std::filesystem::path(system) / L"WindowsPowerShell/v1.0/powershell.exe";
|
||||
std::wstring command = quoteArgument(powershell.wstring()) + L" -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File " +
|
||||
quoteArgument((work / L"update.ps1").wstring()) + L" -Mode " + (install ? L"Install" : L"Check") +
|
||||
L" -CurrentVersion " + quoteArgument(AppVersion) + L" -WorkDirectory " + quoteArgument(work.wstring()) +
|
||||
L" -InstallDirectory " + quoteArgument(std::filesystem::path(executable()).parent_path().wstring()) +
|
||||
L" -ParentProcessId " + std::to_wstring(GetCurrentProcessId()) + L" -ReadyEvent " + quoteArgument(readyName);
|
||||
if (install) command += L" -ExpectedVersion " + quoteArgument(expectedVersion);
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(powershell.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
|
||||
nullptr, work.c_str(), &startup, &process))
|
||||
throw std::runtime_error("Cannot start Windows PowerShell updater");
|
||||
child.value = process.hProcess;
|
||||
Handle thread{process.hThread};
|
||||
HANDLE waits[]{ready.value, child.value};
|
||||
DWORD waited = WaitForMultipleObjects(2, waits, FALSE, 240000);
|
||||
if (waited != WAIT_OBJECT_0 && waited != WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
// The helper cannot replace files while this process is alive.
|
||||
TerminateProcess(child.value, 1);
|
||||
WaitForSingleObject(child.value, 5000);
|
||||
throw std::runtime_error("Update preparation timed out; the installed application was not changed");
|
||||
}
|
||||
auto data = readFile(work / L"result.json", 64 * 1024);
|
||||
auto json = nlohmann::json::parse(data.begin(), data.end());
|
||||
result.state = wide(json.at("state").get<std::string>());
|
||||
result.version = wide(json.value("version", std::string{}));
|
||||
result.error = wide(json.value("error", std::string{}));
|
||||
detached = install && waited == WAIT_OBJECT_0 && result.state == L"prepared";
|
||||
if (result.state == L"prepared" && !detached)
|
||||
throw std::runtime_error("Update helper exited before installation");
|
||||
if (!detached) std::filesystem::remove_all(work);
|
||||
return result;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
if (!detached && child.value && WaitForSingleObject(child.value, 0) == WAIT_TIMEOUT)
|
||||
{
|
||||
TerminateProcess(child.value, 1);
|
||||
WaitForSingleObject(child.value, 5000);
|
||||
}
|
||||
if (!detached) { std::error_code ignored; std::filesystem::remove_all(work, ignored); }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
void confirmUpdatedStartup(const std::wstring &eventName)
|
||||
{
|
||||
if (!eventName.starts_with(L"Local\\KSwitcher.Update.Started.")) return;
|
||||
Handle event{OpenEventW(EVENT_MODIFY_STATE, FALSE, eventName.c_str())};
|
||||
if (event.value) SetEvent(event.value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.2.1";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
struct UpdateResult
|
||||
{
|
||||
std::wstring state, version, error;
|
||||
bool interactive{}, install{};
|
||||
};
|
||||
UpdateResult runUpdate(bool install, const std::wstring &expectedVersion);
|
||||
void confirmUpdatedStartup(const std::wstring &eventName);
|
||||
}
|
||||
Reference in New Issue
Block a user