206 lines
8.2 KiB
C++
206 lines
8.2 KiB
C++
#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;
|
|
}
|