Release v0.3.0: move settings into an on-demand single-instance configurator
This commit is contained in:
+37
-2
@@ -1,6 +1,7 @@
|
||||
#include "app.h"
|
||||
#include "../resources/resource.h"
|
||||
#include "ocr.h"
|
||||
#include "configuration.h"
|
||||
#include <algorithm>
|
||||
#include <commctrl.h>
|
||||
#include <set>
|
||||
@@ -398,6 +399,18 @@ void App::reloadLayouts()
|
||||
}
|
||||
registerHotkeys(settings);
|
||||
}
|
||||
void App::clearHistory()
|
||||
{
|
||||
if (busy) throw std::runtime_error("Wait for the current operation to finish.");
|
||||
if (history) history->clear();
|
||||
if (historyWindow) refreshHistory(*this);
|
||||
}
|
||||
void App::setHotkeyCapture(HWND field)
|
||||
{
|
||||
hotkeyCapture = field;
|
||||
hotkeyCaptureProcess = 0;
|
||||
if (field) GetWindowThreadProcessId(field, &hotkeyCaptureProcess);
|
||||
}
|
||||
bool App::isHotkey(DWORD key, UINT mods)
|
||||
{
|
||||
for (auto &[id, text] : shortcuts(settings))
|
||||
@@ -496,6 +509,10 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
|
||||
// down AND up events use exactly the same path as physical keyboard input.
|
||||
if (ownKeyboardInput(key) || installingUpdate)
|
||||
return false;
|
||||
DWORD captureProcess{};
|
||||
if (hotkeyCapture) GetWindowThreadProcessId(hotkeyCapture, &captureProcess);
|
||||
if (hotkeyCapture && (!captureProcess || captureProcess != hotkeyCaptureProcess))
|
||||
setHotkeyCapture(nullptr);
|
||||
if (hotkeyCapture && GetForegroundWindow() == GetAncestor(hotkeyCapture, GA_ROOT))
|
||||
{
|
||||
bool modifier = key.vkCode == VK_CONTROL || key.vkCode == VK_SHIFT || key.vkCode == VK_MENU ||
|
||||
@@ -579,6 +596,11 @@ void App::dispatch(int command)
|
||||
showSettings(*this);
|
||||
return;
|
||||
}
|
||||
if (command == 22)
|
||||
{
|
||||
showLogs(*this);
|
||||
return;
|
||||
}
|
||||
if (command == 21)
|
||||
{
|
||||
showSnippets(*this);
|
||||
@@ -1129,9 +1151,9 @@ void App::trayMenu()
|
||||
break;
|
||||
case 16:
|
||||
MessageBoxW(window,
|
||||
L"KSwitcher 0.2.2\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
||||
(std::wstring(L"KSwitcher ") + AppVersion + L"\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"account.\nNo .NET, WebView or graphics framework.").c_str(),
|
||||
L"About KSwitcher", MB_OK);
|
||||
break;
|
||||
case 17:
|
||||
@@ -1165,6 +1187,11 @@ void App::trayMenu()
|
||||
void App::checkUpdate(bool interactive, bool install, const std::wstring &version)
|
||||
{
|
||||
if (updateRunning) return;
|
||||
if (install && FindWindowW(L"KSwitcher.Config", nullptr))
|
||||
{
|
||||
report(L"Close the configurator before installing an update.");
|
||||
return;
|
||||
}
|
||||
if (install && busy)
|
||||
{
|
||||
report(L"Wait for the current text operation to finish, then select Update again.");
|
||||
@@ -1284,6 +1311,11 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
case WM_COPYDATA:
|
||||
{
|
||||
auto *data = (COPYDATASTRUCT *)lp;
|
||||
if (data && data->dwData >= ConfigRead && data->dwData < ConfigReply)
|
||||
{
|
||||
if (app->installingUpdate) return FALSE;
|
||||
return configurationRequest(*app, (HWND)wp, *data);
|
||||
}
|
||||
if (data && data->dwData == 1 && data->cbData == sizeof(int))
|
||||
{
|
||||
int cmd;
|
||||
@@ -1340,6 +1372,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
if (auto config = FindWindowW(L"KSwitcher.Config", nullptr);
|
||||
config && configurationPeer(config, L"KSwitcher.Config.exe"))
|
||||
PostMessageW(config, WM_CLOSE, 0, 0);
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
default:
|
||||
|
||||
@@ -43,6 +43,7 @@ class App
|
||||
std::atomic<ULONGLONG> pauseUntil{0};
|
||||
HWND pasteTarget{};
|
||||
HWND hotkeyCapture{};
|
||||
DWORD hotkeyCaptureProcess{};
|
||||
HWND logWindow{};
|
||||
std::mutex stateMutex;
|
||||
Run run;
|
||||
@@ -66,6 +67,8 @@ class App
|
||||
bool registerHotkeys(const Settings &, bool rollback = false);
|
||||
void applySettings(const Settings &);
|
||||
void reloadLayouts();
|
||||
void clearHistory();
|
||||
void setHotkeyCapture(HWND);
|
||||
bool isHotkey(DWORD, UINT);
|
||||
void keyboard(const KBDLLHOOKSTRUCT &, UINT modifiers);
|
||||
bool handleKeyboard(const KBDLLHOOKSTRUCT &, bool down, UINT modifiers);
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
#include "configuration.h"
|
||||
#include <commctrl.h>
|
||||
#include <cstring>
|
||||
|
||||
namespace ks
|
||||
{
|
||||
App *App::current = nullptr;
|
||||
namespace
|
||||
{
|
||||
HWND service{};
|
||||
Bytes response;
|
||||
bool replied{};
|
||||
Bytes request(App &app, ULONG_PTR command, const Bytes &payload = {})
|
||||
{
|
||||
if (!service || !configurationPeer(service, L"KSwitcher.exe"))
|
||||
throw std::runtime_error("KSwitcher has stopped. Close and reopen the configurator.");
|
||||
if (payload.size() > ConfigLimit) throw std::runtime_error("Settings are too large.");
|
||||
response.clear(); replied = false;
|
||||
COPYDATASTRUCT message{command, (DWORD)payload.size(), const_cast<unsigned char *>(payload.data())};
|
||||
DWORD_PTR result{};
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
if (!SendMessageTimeoutW(service, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&message,
|
||||
SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 15000, &result) || !replied)
|
||||
{
|
||||
if (GetLastError() == ERROR_ACCESS_DENIED)
|
||||
throw std::runtime_error("Open Settings from the KSwitcher tray menu so both processes use the same privileges.");
|
||||
throw std::runtime_error("KSwitcher did not respond. The operation may still finish; reopen Settings to check.");
|
||||
}
|
||||
if (result != 1)
|
||||
throw std::runtime_error(response.empty() ? "KSwitcher rejected the request." :
|
||||
std::string(response.begin(), response.end()));
|
||||
return response;
|
||||
}
|
||||
LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *app = App::current;
|
||||
if (!app) return DefWindowProcW(window, message, wp, lp);
|
||||
if (message == WM_COPYDATA)
|
||||
{
|
||||
auto *data = reinterpret_cast<COPYDATASTRUCT *>(lp);
|
||||
if ((HWND)wp == service && data && data->dwData == ConfigReply && data->cbData <= ConfigLimit &&
|
||||
(!data->cbData || data->lpData))
|
||||
{
|
||||
response.clear();
|
||||
if (data->cbData)
|
||||
{
|
||||
auto first = static_cast<const unsigned char *>(data->lpData);
|
||||
response.assign(first, first + data->cbData);
|
||||
}
|
||||
replied = true;
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
if (message == WM_APP + 20)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (wp == 21) showSnippets(*app);
|
||||
else showSettings(*app);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
MessageBoxW(app->settingsWindow, wide(e.what()).c_str(), L"KSwitcher configurator", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_CLOSE)
|
||||
{
|
||||
if (app->settingsWindow) DestroyWindow(app->settingsWindow);
|
||||
if (app->snippetsWindow) DestroyWindow(app->snippetsWindow);
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(window, message, wp, lp);
|
||||
}
|
||||
}
|
||||
// The configurator owns no history store, dictionaries, worker or input hooks.
|
||||
// All mutations are validated and committed by the resident process.
|
||||
App::App(HINSTANCE h, bool, const std::filesystem::path &) : instance(h)
|
||||
{
|
||||
current = this;
|
||||
layouts = readLayouts();
|
||||
}
|
||||
App::~App()
|
||||
{
|
||||
if (settingsWindow) DestroyWindow(settingsWindow);
|
||||
if (snippetsWindow) DestroyWindow(snippetsWindow);
|
||||
if (window) DestroyWindow(window);
|
||||
current = nullptr;
|
||||
}
|
||||
void App::applySettings(const Settings &next)
|
||||
{
|
||||
validateSettings(next);
|
||||
auto before = encodeSettings(settings), after = encodeSettings(next);
|
||||
DWORD length = (DWORD)before.size();
|
||||
Bytes payload(sizeof(length));
|
||||
memcpy(payload.data(), &length, sizeof(length));
|
||||
payload.insert(payload.end(), before.begin(), before.end());
|
||||
payload.insert(payload.end(), after.begin(), after.end());
|
||||
settings = decodeSettings(request(*this, ConfigSave, payload), layouts);
|
||||
}
|
||||
void App::reloadLayouts()
|
||||
{
|
||||
auto bytes = request(*this, ConfigLayouts);
|
||||
layouts = readLayouts();
|
||||
settings = decodeSettings(bytes, layouts);
|
||||
}
|
||||
void App::clearHistory() { request(*this, ConfigClearHistory); }
|
||||
void App::setHotkeyCapture(HWND field)
|
||||
{
|
||||
hotkeyCapture = field;
|
||||
Bytes payload(sizeof(field));
|
||||
memcpy(payload.data(), &field, sizeof(field));
|
||||
// Focus notifications must not let an exception escape a window callback.
|
||||
try { request(*this, ConfigCapture, payload); }
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
hotkeyCapture = nullptr;
|
||||
if (field) MessageBoxW(settingsWindow, wide(e.what()).c_str(), L"Shortcut capture unavailable", MB_OK | MB_ICONWARNING);
|
||||
}
|
||||
}
|
||||
void showLogs(App &app) { request(app, ConfigLogs); }
|
||||
|
||||
int runConfigurator(HINSTANCE instance, bool snippets)
|
||||
{
|
||||
HANDLE singleton = CreateMutexW(nullptr, TRUE, L"Local\\KSwitcher.Config.Singleton");
|
||||
if (!singleton) throw std::runtime_error("Cannot create configurator instance lock.");
|
||||
const bool existing = GetLastError() == ERROR_ALREADY_EXISTS;
|
||||
struct Close { HANDLE handle; ~Close() { CloseHandle(handle); } } close{singleton};
|
||||
if (existing)
|
||||
{
|
||||
HWND window{};
|
||||
for (int i = 0; i < 100 && !window; ++i)
|
||||
{
|
||||
window = FindWindowW(L"KSwitcher.Config", nullptr);
|
||||
if (!window) Sleep(50);
|
||||
}
|
||||
if (!window || !configurationPeer(window, L"KSwitcher.Config.exe"))
|
||||
throw std::runtime_error("The configurator is starting or belongs to another installation. Try again shortly.");
|
||||
DWORD pid{}; GetWindowThreadProcessId(window, &pid);
|
||||
AllowSetForegroundWindow(pid);
|
||||
PostMessageW(window, WM_APP + 20, snippets ? 21 : 20, 0);
|
||||
return 0;
|
||||
}
|
||||
service = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
|
||||
if (!service)
|
||||
{
|
||||
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.exe";
|
||||
auto command = L"\"" + path.wstring() + L"\"";
|
||||
STARTUPINFOW startup{sizeof(startup)}; PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||
path.parent_path().c_str(), &startup, &process))
|
||||
throw std::runtime_error("Cannot start KSwitcher beside the configurator.");
|
||||
CloseHandle(process.hThread); CloseHandle(process.hProcess);
|
||||
for (int i = 0; i < 100 && !service; ++i)
|
||||
{
|
||||
Sleep(50); service = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
|
||||
}
|
||||
}
|
||||
if (!configurationPeer(service, L"KSwitcher.exe"))
|
||||
throw std::runtime_error("Run the configurator from the active KSwitcher installation.");
|
||||
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES};
|
||||
InitCommonControlsEx(&controls);
|
||||
App app(instance);
|
||||
WNDCLASSW cls{}; cls.hInstance = instance; cls.lpfnWndProc = controller; cls.lpszClassName = L"KSwitcher.Config";
|
||||
RegisterClassW(&cls);
|
||||
app.window = CreateWindowExW(WS_EX_TOOLWINDOW, cls.lpszClassName, L"KSwitcher configurator", WS_POPUP,
|
||||
0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
|
||||
if (!app.window) throw std::runtime_error("Cannot create configurator window.");
|
||||
app.settings = decodeSettings(request(app, ConfigRead), app.layouts);
|
||||
if (snippets) showSnippets(app); else showSettings(app);
|
||||
MSG message;
|
||||
while ((app.settingsWindow || app.snippetsWindow) && GetMessageW(&message, nullptr, 0, 0) > 0)
|
||||
{
|
||||
if (app.settingsWindow && !IsWindowEnabled(app.settingsWindow))
|
||||
{
|
||||
HWND popup = GetLastActivePopup(app.settingsWindow);
|
||||
if (popup != app.settingsWindow && IsDialogMessageW(popup, &message)) continue;
|
||||
}
|
||||
if (app.settingsWindow && IsDialogMessageW(app.settingsWindow, &message)) continue;
|
||||
if (app.snippetsWindow && IsDialogMessageW(app.snippetsWindow, &message)) continue;
|
||||
TranslateMessage(&message); DispatchMessageW(&message);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
|
||||
{
|
||||
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
int result = 1;
|
||||
try
|
||||
{
|
||||
int count{}; auto args = CommandLineToArgvW(GetCommandLineW(), &count);
|
||||
bool snippets = count > 1 && std::wstring(args[1]) == L"--snippets";
|
||||
LocalFree(args);
|
||||
result = ks::runConfigurator(instance, snippets);
|
||||
}
|
||||
catch (const std::exception &e)
|
||||
{
|
||||
MessageBoxW(nullptr, ks::wide(e.what()).c_str(), L"KSwitcher configurator", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
CoUninitialize();
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#include "configuration.h"
|
||||
#include <cstring>
|
||||
namespace ks
|
||||
{
|
||||
bool configurationPeer(HWND window, const wchar_t *filename)
|
||||
{
|
||||
DWORD pid{};
|
||||
GetWindowThreadProcessId(window, &pid);
|
||||
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||
if (!process) return false;
|
||||
wchar_t path[32768]; DWORD count = std::size(path);
|
||||
bool ok = QueryFullProcessImageNameW(process, 0, path, &count) != 0;
|
||||
CloseHandle(process);
|
||||
auto expected = std::filesystem::path(executable()).parent_path() / filename;
|
||||
return ok && _wcsicmp(path, expected.c_str()) == 0;
|
||||
}
|
||||
void launchConfiguration(App &app, bool snippets)
|
||||
{
|
||||
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Config.exe";
|
||||
if (!std::filesystem::exists(path))
|
||||
throw std::runtime_error("KSwitcher.Config.exe is missing. Extract the complete application package.");
|
||||
auto command = L"\"" + path.wstring() + L"\"" + (snippets ? L" --snippets" : L"");
|
||||
STARTUPINFOW startup{sizeof(startup)};
|
||||
PROCESS_INFORMATION process{};
|
||||
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||
path.parent_path().c_str(), &startup, &process))
|
||||
throw std::runtime_error(utf8(lastError(L"Cannot start configurator")));
|
||||
AllowSetForegroundWindow(process.dwProcessId);
|
||||
CloseHandle(process.hThread);
|
||||
CloseHandle(process.hProcess);
|
||||
(void)app;
|
||||
}
|
||||
LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &message)
|
||||
{
|
||||
if (!configurationPeer(sender, L"KSwitcher.Config.exe") || message.cbData > ConfigLimit ||
|
||||
(message.cbData && !message.lpData)) return FALSE;
|
||||
Bytes reply;
|
||||
bool ok = false;
|
||||
try
|
||||
{
|
||||
switch (message.dwData)
|
||||
{
|
||||
case ConfigSave:
|
||||
{
|
||||
if (app.busy) throw std::runtime_error("Wait for the current text operation to finish.");
|
||||
if (message.cbData < sizeof(DWORD)) throw std::runtime_error("Invalid settings request.");
|
||||
DWORD length{};
|
||||
memcpy(&length, message.lpData, sizeof(length));
|
||||
if (length > message.cbData - sizeof(length)) throw std::runtime_error("Invalid settings snapshot.");
|
||||
auto begin = static_cast<const unsigned char *>(message.lpData) + sizeof(length);
|
||||
if (Bytes(begin, begin + length) != encodeSettings(app.settings))
|
||||
throw std::runtime_error("Settings changed in KSwitcher. Close and reopen the configurator before saving.");
|
||||
Bytes next(begin + length, static_cast<const unsigned char *>(message.lpData) + message.cbData);
|
||||
if (next.empty()) throw std::runtime_error("Empty settings request.");
|
||||
app.applySettings(decodeSettings(next, app.layouts));
|
||||
reply = encodeSettings(app.settings);
|
||||
break;
|
||||
}
|
||||
case ConfigLayouts:
|
||||
if (app.busy) throw std::runtime_error("Wait for the current text operation to finish.");
|
||||
app.reloadLayouts();
|
||||
[[fallthrough]];
|
||||
case ConfigRead:
|
||||
reply = encodeSettings(app.settings);
|
||||
break;
|
||||
case ConfigClearHistory:
|
||||
app.clearHistory();
|
||||
break;
|
||||
case ConfigCapture:
|
||||
{
|
||||
if (message.cbData != sizeof(HWND)) throw std::runtime_error("Invalid shortcut field.");
|
||||
HWND field{}; memcpy(&field, message.lpData, sizeof(field));
|
||||
DWORD fieldPid{}, senderPid{};
|
||||
GetWindowThreadProcessId(field, &fieldPid);
|
||||
GetWindowThreadProcessId(sender, &senderPid);
|
||||
if (field && fieldPid != senderPid) throw std::runtime_error("Invalid shortcut field owner.");
|
||||
app.setHotkeyCapture(field);
|
||||
break;
|
||||
}
|
||||
case ConfigLogs:
|
||||
PostMessageW(app.window, WM_HOTKEY, 22, 0);
|
||||
break;
|
||||
default: return FALSE;
|
||||
}
|
||||
ok = true;
|
||||
}
|
||||
catch (const std::exception &error)
|
||||
{
|
||||
std::string text = error.what();
|
||||
reply.assign(text.begin(), text.end());
|
||||
}
|
||||
COPYDATASTRUCT response{ConfigReply, (DWORD)reply.size(), reply.data()};
|
||||
DWORD_PTR received{};
|
||||
if (!SendMessageTimeoutW(sender, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&response,
|
||||
SMTO_ABORTIFHUNG, 2000, &received)) return FALSE;
|
||||
return ok ? 1 : 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "app.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
||||
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
|
||||
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
||||
ConfigReply = ConfigRead + 6;
|
||||
inline constexpr size_t ConfigLimit = 8 * 1024 * 1024;
|
||||
bool configurationPeer(HWND, const wchar_t *filename);
|
||||
void launchConfiguration(App &, bool snippets = false);
|
||||
LRESULT configurationRequest(App &, HWND sender, const COPYDATASTRUCT &);
|
||||
}
|
||||
+11
-3
@@ -102,6 +102,10 @@ void validateSettings(const Settings &s)
|
||||
}
|
||||
}
|
||||
Settings loadSettings(const std::filesystem::path &path, const std::vector<Layout> &layouts)
|
||||
{
|
||||
return decodeSettings(std::filesystem::exists(path) ? readFile(path, 4 * 1024 * 1024) : Bytes{}, layouts);
|
||||
}
|
||||
Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
||||
{
|
||||
Settings s;
|
||||
for (size_t i = 0; i < layouts.size(); ++i)
|
||||
@@ -111,9 +115,9 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
|
||||
s.direct.push_back(i < 3 ? L"Ctrl+Alt+Shift+" + std::to_wstring(i + 1) : L"");
|
||||
s.directIds.push_back(layouts[i].id);
|
||||
}
|
||||
if (!std::filesystem::exists(path))
|
||||
if (bytes.empty())
|
||||
return s;
|
||||
auto j = parse(readFile(path, 4 * 1024 * 1024));
|
||||
auto j = parse(bytes);
|
||||
auto format = j.value("format", std::string());
|
||||
if (format != "kswitcher-settings" && format != "typlo-settings")
|
||||
throw std::runtime_error("Not a KSwitcher or Typlo settings file");
|
||||
@@ -177,6 +181,10 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
|
||||
return s;
|
||||
}
|
||||
void saveSettings(const std::filesystem::path &path, const Settings &s)
|
||||
{
|
||||
atomicWrite(path, encodeSettings(s));
|
||||
}
|
||||
Bytes encodeSettings(const Settings &s)
|
||||
{
|
||||
validateSettings(s);
|
||||
Json j = {{"format", "kswitcher-settings"},
|
||||
@@ -221,7 +229,7 @@ void saveSettings(const std::filesystem::path &path, const Settings &s)
|
||||
j["excludedApps"] = Json::array();
|
||||
for (auto &name : s.excluded)
|
||||
j["excludedApps"].push_back(utf8(name));
|
||||
atomicWrite(path, jsonBytes(j));
|
||||
return jsonBytes(j);
|
||||
}
|
||||
Bytes encodeClip(const Clip &clip)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
namespace ks
|
||||
{
|
||||
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
|
||||
Settings decodeSettings(const Bytes &, const std::vector<Layout> &);
|
||||
Bytes encodeSettings(const Settings &);
|
||||
void saveSettings(const std::filesystem::path &, const Settings &);
|
||||
void validateSettings(const Settings &);
|
||||
struct Entry
|
||||
|
||||
+26
-10
@@ -1,5 +1,6 @@
|
||||
#include "../resources/resource.h"
|
||||
#include "app.h"
|
||||
#include "configuration.h"
|
||||
#include "ocr.h"
|
||||
#include "richtext.h"
|
||||
#include "ui_geometry.h"
|
||||
@@ -185,13 +186,13 @@ LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
|
||||
auto *app = (App *)data;
|
||||
if (msg == WM_SETFOCUS)
|
||||
{
|
||||
app->hotkeyCapture = w;
|
||||
app->setHotkeyCapture(w);
|
||||
SendMessageW(w, EM_SETSEL, 0, -1);
|
||||
}
|
||||
if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY)
|
||||
{
|
||||
if (app->hotkeyCapture == w)
|
||||
app->hotkeyCapture = nullptr;
|
||||
app->setHotkeyCapture(nullptr);
|
||||
}
|
||||
if (msg == CaptureHotkeyMessage)
|
||||
{
|
||||
@@ -218,13 +219,13 @@ LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
|
||||
return 0;
|
||||
return DefSubclassProc(w, msg, wp, lp);
|
||||
}
|
||||
void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
|
||||
[[maybe_unused]] void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
|
||||
{
|
||||
HWND field = edit(w, id, x, y, width, 25, ES_AUTOHSCROLL | ES_READONLY);
|
||||
SendMessageW(field, EM_SETCUEBANNER, FALSE, (LPARAM)L"Click, then press a shortcut");
|
||||
SetWindowSubclass(field, hotkeyProc, 1, (DWORD_PTR)&app);
|
||||
}
|
||||
void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
|
||||
[[maybe_unused]] void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
|
||||
{
|
||||
control(w, L"BUTTON", text, WS_TABSTOP | BS_AUTOCHECKBOX, id, x, y, width, 24);
|
||||
}
|
||||
@@ -241,11 +242,11 @@ void value(HWND w, int id, const std::wstring &text)
|
||||
{
|
||||
SetDlgItemTextW(w, id, text.c_str());
|
||||
}
|
||||
bool checked(HWND w, int id)
|
||||
[[maybe_unused]] bool checked(HWND w, int id)
|
||||
{
|
||||
return IsDlgButtonChecked(w, id) == BST_CHECKED;
|
||||
}
|
||||
void check(HWND w, int id, bool yes)
|
||||
[[maybe_unused]] void check(HWND w, int id, bool yes)
|
||||
{
|
||||
CheckDlgButton(w, id, yes ? BST_CHECKED : BST_UNCHECKED);
|
||||
}
|
||||
@@ -352,7 +353,7 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
||||
fitToMonitor(w, *(RECT *)lp);
|
||||
arrangeWindow(w);
|
||||
}
|
||||
std::filesystem::path pickFile(HWND owner, bool save)
|
||||
[[maybe_unused]] std::filesystem::path pickFile(HWND owner, bool save)
|
||||
{
|
||||
wchar_t path[32768]{};
|
||||
OPENFILENAMEW ofn{sizeof(ofn)};
|
||||
@@ -372,6 +373,7 @@ void disposeFont(HWND w)
|
||||
DeleteObject(h);
|
||||
}
|
||||
|
||||
#ifndef KS_CONFIG_APP
|
||||
struct SearchResult
|
||||
{
|
||||
uint64_t generation;
|
||||
@@ -755,6 +757,8 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
|
||||
#endif
|
||||
#ifndef KS_RESIDENT_APP
|
||||
struct SettingsView
|
||||
{
|
||||
App *app{};
|
||||
@@ -1146,11 +1150,11 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
fillSettings(w, v);
|
||||
return 0;
|
||||
}
|
||||
if (id == 254 && v->app->history)
|
||||
if (id == 254)
|
||||
{
|
||||
if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history",
|
||||
MB_YESNO | MB_ICONWARNING) == IDYES)
|
||||
v->app->history->clear();
|
||||
v->app->clearHistory();
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
@@ -1294,6 +1298,8 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
}
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
#endif
|
||||
#ifndef KS_CONFIG_APP
|
||||
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (msg == WM_SIZE)
|
||||
@@ -1305,7 +1311,9 @@ LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
disposeFont(w);
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
#endif
|
||||
} // namespace
|
||||
#ifndef KS_CONFIG_APP
|
||||
void refreshHistory(App &app)
|
||||
{
|
||||
if (app.historyWindow)
|
||||
@@ -1498,7 +1506,7 @@ void refreshLogView(HWND w, App &app)
|
||||
InvalidateRect(list, nullptr, TRUE);
|
||||
if (row)
|
||||
ListView_EnsureVisible(list, row - 1, FALSE);
|
||||
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: problems and suspicious repeats only."
|
||||
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: failed operations only."
|
||||
: L"Logging disabled. Showing previously saved entries.");
|
||||
}
|
||||
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
@@ -1609,6 +1617,8 @@ void showLogs(App &app)
|
||||
showRequestedWindow(w);
|
||||
activateWindow(w);
|
||||
}
|
||||
#endif
|
||||
#ifndef KS_RESIDENT_APP
|
||||
void showSettings(App &app, bool visible)
|
||||
{
|
||||
if (app.settingsWindow)
|
||||
@@ -1699,6 +1709,11 @@ void showSnippets(App &app)
|
||||
showRequestedWindow(w);
|
||||
SetForegroundWindow(w);
|
||||
}
|
||||
#else
|
||||
void showSettings(App &app, bool) { launchConfiguration(app); }
|
||||
void showSnippets(App &app) { launchConfiguration(app, true); }
|
||||
#endif
|
||||
#ifndef KS_CONFIG_APP
|
||||
void showShortcuts(App &app)
|
||||
{
|
||||
auto &s = app.settings;
|
||||
@@ -1736,4 +1751,5 @@ void showEntry(App &app, const std::wstring &id)
|
||||
error(app.historyWindow, e);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} // namespace ks
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.2.3";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.3.0";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
struct UpdateResult
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user