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;
|
||||
|
||||
Reference in New Issue
Block a user