feat: release kswitcher 0.6.64

Add mouse button actions and application profiles, with volume controls and per-monitor settings layout fixes.
This commit is contained in:
agrynco committed 2026-11-12 00:39:12 +02:00
1 parent 5a2106899d
commit 14f33012a3
23 files changed
+2233 -107

No files matched your search

+325 -9
View File
@@ -847,6 +847,8 @@ void App::resetLanInput()
// shortcut/chat latches across an ownership handoff.
hotkeyHook = {};
chatSwitchHook = {};
pendingMouseClicks = {};
mouseActions.clear();
resetInput();
}
void App::resetInput()
@@ -1169,23 +1171,324 @@ LRESULT App::keyboardAfterHooks(int code, WPARAM wp, LPARAM lp, LRESULT routed)
}
return routed;
}
constexpr UINT MouseActionMessage = WM_APP + 79;
constexpr UINT MouseVolumeReleasedMessage = WM_APP + 80;
constexpr UINT_PTR MouseClickTimer = 9;
constexpr UINT_PTR FirstMouseVolumeTimer = 10;
namespace
{
constexpr wchar_t VolumeIndicatorClass[] = L"KSwitcher.VolumeIndicator";
constexpr UINT_PTR VolumeIndicatorTimer = 1;
constexpr WORD VolumeIndicatorMuted = 1, VolumeIndicatorHeld = 2;
void paintVolumeIndicator(HWND w)
{
PAINTSTRUCT paint{};
const auto dc = BeginPaint(w, &paint);
RECT bounds{};
GetClientRect(w, &bounds);
FillRect(dc, &bounds, GetSysColorBrush(COLOR_WINDOW));
FrameRect(dc, &bounds, GetSysColorBrush(COLOR_WINDOWFRAME));
const int dpi = static_cast<int>(GetDpiForWindow(w));
const int padding = MulDiv(8, dpi, 96);
const int captionHeight = MulDiv(26, dpi, 96);
const auto state = GetWindowLongPtrW(w, GWLP_USERDATA);
const int percent = LOWORD(state);
const bool muted = (HIWORD(state) & VolumeIndicatorMuted) != 0;
RECT track{padding, padding, bounds.right - padding, bounds.bottom - captionHeight};
FillRect(dc, &track, GetSysColorBrush(COLOR_3DFACE));
RECT fill = track;
fill.top = fill.bottom - MulDiv(fill.bottom - fill.top, percent, 100);
FillRect(dc, &fill, GetSysColorBrush(muted ? COLOR_GRAYTEXT : COLOR_HIGHLIGHT));
RECT caption{0, track.bottom, bounds.right, bounds.bottom};
const auto text = muted ? std::wstring(L"Mute") : std::to_wstring(percent) + L"%";
const auto font = CreateFontW(-MulDiv(12, dpi, 96), 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, 0, 0, 0, 0, L"Segoe UI");
const auto previous = font ? SelectObject(dc, font) : nullptr;
SetBkMode(dc, TRANSPARENT);
SetTextColor(dc, GetSysColor(COLOR_WINDOWTEXT));
DrawTextW(dc, text.c_str(), -1, &caption, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX);
if (previous) SelectObject(dc, previous);
if (font) DeleteObject(font);
EndPaint(w, &paint);
}
LRESULT CALLBACK volumeIndicatorProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg)
{
case WM_PAINT: paintVolumeIndicator(w); return 0;
case WM_NCHITTEST: return HTTRANSPARENT;
case WM_MOUSEACTIVATE: return MA_NOACTIVATE;
case WM_TIMER:
if (wp == VolumeIndicatorTimer)
{
KillTimer(w, VolumeIndicatorTimer);
if (!(HIWORD(GetWindowLongPtrW(w, GWLP_USERDATA)) & VolumeIndicatorHeld))
ShowWindow(w, SW_HIDE);
return 0;
}
break;
}
return DefWindowProcW(w, msg, wp, lp);
}
void setVolumeIndicatorHold(HWND indicator, bool held)
{
const auto state = GetWindowLongPtrW(indicator, GWLP_USERDATA);
const WORD flags = (HIWORD(state) & VolumeIndicatorMuted) | (held ? VolumeIndicatorHeld : 0);
SetWindowLongPtrW(indicator, GWLP_USERDATA, MAKELPARAM(LOWORD(state), flags));
if (held) KillTimer(indicator, VolumeIndicatorTimer);
else SetTimer(indicator, VolumeIndicatorTimer, 1000, nullptr);
}
void showVolumeIndicator(HWND owner, HINSTANCE instance, POINT point, SystemVolume volume, bool held)
{
MONITORINFO monitor{sizeof(monitor)};
const auto targetMonitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST);
if (!GetMonitorInfoW(targetMonitor, &monitor)) return;
const auto &area = monitor.rcWork;
auto indicator = reinterpret_cast<HWND>(GetPropW(owner, VolumeIndicatorClass));
if (!indicator)
{
WNDCLASSW type{};
type.lpfnWndProc = volumeIndicatorProc;
type.hInstance = instance;
type.lpszClassName = VolumeIndicatorClass;
RegisterClassW(&type);
indicator = CreateWindowExW(WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW | WS_EX_TOPMOST |
WS_EX_LAYERED | WS_EX_TRANSPARENT, VolumeIndicatorClass, L"Volume",
WS_POPUP, area.left, area.top, 1, 1, owner, nullptr, instance, nullptr);
if (!indicator) return;
if (!SetPropW(owner, VolumeIndicatorClass, indicator) ||
!SetLayeredWindowAttributes(indicator, 0, 255, LWA_ALPHA))
{
RemovePropW(owner, VolumeIndicatorClass);
DestroyWindow(indicator);
return;
}
}
// Place the window on the destination monitor before querying its DPI.
if (MonitorFromWindow(indicator, MONITOR_DEFAULTTONEAREST) != targetMonitor)
SetWindowPos(indicator, nullptr, area.left, area.top, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW);
const int dpi = static_cast<int>(GetDpiForWindow(indicator));
const int width = MulDiv(48, dpi, 96);
const int height = MulDiv(132, dpi, 96);
const int margin = MulDiv(16, dpi, 96);
SetWindowLongPtrW(indicator, GWLP_USERDATA, MAKELPARAM(volume.percent, volume.muted ? VolumeIndicatorMuted : 0));
setVolumeIndicatorHold(indicator, held);
SetWindowPos(indicator, HWND_TOPMOST, area.right - width - margin, area.bottom - height - margin,
width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW);
InvalidateRect(indicator, nullptr, FALSE);
}
}
void App::queueMouseAction(MousePress press)
{
if (press.action.type == MouseActionType::Standard) return;
mouseActions.push_back(std::move(press));
PostMessageW(window, MouseActionMessage, 0, 0);
}
void App::flushMouseClicks()
{
ULONGLONG next = 0;
const auto now = GetTickCount64();
for (auto &press : pendingMouseClicks)
{
if (!press.target) continue;
const auto deadline = press.started + GetDoubleClickTime();
if (now >= deadline)
{
queueMouseAction(std::exchange(press, {}));
}
else if (!next || deadline < next) next = deadline;
}
if (next) SetTimer(window, MouseClickTimer, static_cast<UINT>(next - now), nullptr);
else KillTimer(window, MouseClickTimer);
}
void App::beginMouseVolume(int button)
{
const auto &press = mousePresses[button];
if (!press.waitForSecond) queueMouseAction(press);
const UINT delay = press.waitForSecond ? std::max(MouseVolumeHoldDelay, GetDoubleClickTime()) : MouseVolumeHoldDelay;
SetTimer(window, FirstMouseVolumeTimer + button, delay, nullptr);
}
void App::repeatMouseVolume(int button)
{
const auto timer = FirstMouseVolumeTimer + button;
auto &press = mousePresses[button];
if (!press.held || !isMouseVolumeStep(press.action.type) ||
busy || suspended || installingUpdate || hotkeyCapture || !modifiersReleased() ||
!IsWindow(press.target) || (!press.action.applications.empty() && press.target != GetForegroundWindow()))
{
KillTimer(window, timer);
return;
}
const auto elapsed = GetTickCount64() - press.started;
const UINT delay = press.waitForSecond ? std::max(MouseVolumeHoldDelay, GetDoubleClickTime()) : MouseVolumeHoldDelay;
if (elapsed < delay) return;
press.waitForSecond = false;
queueMouseAction(press);
SetTimer(window, timer, mouseVolumeRepeatInterval(elapsed), nullptr);
}
bool App::volumeButtonHeld() const
{
return std::any_of(mousePresses.begin(), mousePresses.end(), [](const MousePress &press)
{
return press.held && !press.waitForSecond && isMouseVolumeStep(press.action.type);
});
}
void App::dispatchMouseActions()
{
std::deque<MousePress> actions;
actions.swap(mouseActions);
if (busy || suspended || installingUpdate || hotkeyCapture || !modifiersReleased())
{
if (!actions.empty()) log(L"Mouse action skipped while input was unavailable.");
return;
}
for (const auto &press : actions)
{
if (!IsWindow(press.target)) continue;
const auto &action = press.action;
switch (action.type)
{
case MouseActionType::Standard: break;
case MouseActionType::Shortcut:
if (press.target != GetForegroundWindow())
log(L"Mouse shortcut skipped because the active window changed.");
else if (!chord(static_cast<WORD>(action.shortcut.key), action.shortcut.modifiers))
log(L"Windows did not accept all input for the mouse shortcut.");
else
log(L"Windows accepted input for the mouse shortcut.");
break;
case MouseActionType::Back:
case MouseActionType::Forward:
PostMessageW(press.target, WM_APPCOMMAND, reinterpret_cast<WPARAM>(press.target),
MAKELPARAM(0, FAPPCOMMAND_MOUSE | (action.type == MouseActionType::Back ?
APPCOMMAND_BROWSER_BACKWARD : APPCOMMAND_BROWSER_FORWARD)));
break;
case MouseActionType::VolumeUp:
case MouseActionType::VolumeDown:
case MouseActionType::Mute:
if (const auto volume = changeSystemVolume(action.type, action.volumeStep))
showVolumeIndicator(window, instance, press.point, *volume, volumeButtonHeld());
else log(L"Mouse action could not change or read system volume.");
break;
case MouseActionType::Launch:
if (!launchThroughExplorer(action.executable))
log(L"Mouse action could not launch " + action.executable);
else
log(L"Mouse action launched " + std::filesystem::path(action.executable).filename().wstring());
break;
}
}
}
LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
{
if (code == HC_ACTION && current)
{
if (current->lanService)
auto &app = *current;
const auto &mouse = *reinterpret_cast<const MSLLHOOKSTRUCT *>(lp);
const int button = mouse.flags & LLMHF_INJECTED ? -1 : mouseButton(UINT(wp), HIWORD(mouse.mouseData));
const bool down = mouseButtonDown(UINT(wp));
if (button >= 0 && app.mousePresses[button].held)
{
bool consumed = current->lanService->mouse(UINT(wp), *(MSLLHOOKSTRUCT *)lp,
!current->busy && !current->suspended && !current->hotkeyCapture);
if (current->lanService->takeInputChange()) current->resetLanInput();
auto &active = app.mousePresses[button];
const bool consumed = active.action.type != MouseActionType::Standard;
if (!down)
{
auto press = std::exchange(active, {});
if (isMouseVolumeStep(press.action.type))
{
KillTimer(app.window, FirstMouseVolumeTimer + button);
PostMessageW(app.window, MouseVolumeReleasedMessage, 0, 0);
}
if (press.waitForSecond)
{
app.pendingMouseClicks[button] = std::move(press);
app.flushMouseClicks();
}
else if (!isMouseVolumeStep(press.action.type)) app.queueMouseAction(std::move(press));
}
if (consumed) return 1;
}
// 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)
if (app.lanService)
{
if (current->busy) diagnosticEvent(L"Input context changed: mouse message=" + std::to_wstring(wp));
current->resetInput();
const bool mouseActionHeld = std::any_of(app.mousePresses.begin(), app.mousePresses.end(),
[](const MousePress &press) { return press.held; });
bool consumed = app.lanService->mouse(UINT(wp), mouse,
!app.busy && !app.suspended && !app.hotkeyCapture && !mouseActionHeld);
if (app.lanService->takeInputChange()) app.resetLanInput();
if (consumed) return 1;
}
if (button >= 0 && down && !app.settings.mouseBindings.empty() && !app.busy && !app.suspended &&
!app.installingUpdate && !app.hotkeyCapture && modifiersReleased())
{
const auto hit = WindowFromPoint(mouse.pt);
const auto target = GetAncestor(hit, GA_ROOT);
const auto name = target ? mouseApplicationName(target) : L"";
wchar_t windowClass[64]{};
if (target && name == L"explorer") GetClassNameW(target, windowClass, std::size(windowClass));
const bool ownWindow = name == L"kswitcher" || name == L"kswitcher.config";
const bool shellSurface = target == GetShellWindow() || wcscmp(windowClass, L"Progman") == 0 ||
wcscmp(windowClass, L"WorkerW") == 0 || wcscmp(windowClass, L"Shell_TrayWnd") == 0 ||
wcscmp(windowClass, L"Shell_SecondaryTrayWnd") == 0;
const auto *global = mouseBinding(app.settings, GlobalMouseProfile, button);
const bool globalLaunch = button >= 2 && global && global->type == MouseActionType::Launch &&
mouseActionApplies(*global, name);
const bool restricted = ownWindow || shellSurface || name.empty();
const bool testButton = name == L"kswitcher.config" && hit == GetDlgItem(target, 737);
const auto *fallback = restricted ? effectiveMouseAction(app.settings, L"", button, false) : nullptr;
const auto *restrictedAction = isMouseVolumeAction(fallback) ? fallback : globalLaunch ? global : nullptr;
const bool eligible = target && !testButton && (!restricted || (button >= 2 && restrictedAction)) &&
(button >= 2 || target == GetForegroundWindow());
if (eligible)
{
const bool foreground = target == GetForegroundWindow();
bool singleScoped{}, doubleScoped{};
auto *single = restricted ? restrictedAction :
effectiveMouseAction(app.settings, name, button, foreground, &singleScoped);
auto *doubleClick = !restricted && button < 3 ?
effectiveMouseAction(app.settings, name, 5 + button, foreground, &doubleScoped) : nullptr;
if (!foreground)
{
if (single && single->type == MouseActionType::Shortcut) single = nullptr;
if (doubleClick && doubleClick->type == MouseActionType::Shortcut) doubleClick = nullptr;
}
const auto now = GetTickCount64();
auto &pending = app.pendingMouseClicks[button];
const bool second = doubleClick && pending.target == target &&
doubleMouseClick(pending.point, mouse.pt, now - pending.started);
if (second) pending = {};
else if (pending.target) app.queueMouseAction(std::exchange(pending, {}));
const auto *action = second ? doubleClick : single;
if (action || doubleClick)
app.mousePresses[button] = {target, mouse.pt, now, action ? *action : MouseAction{},
true, doubleClick && !second};
if (action)
{
if (second ? doubleScoped : singleScoped) app.mousePresses[button].action.applications.insert(name);
if (isMouseVolumeStep(action->type))
app.beginMouseVolume(button);
app.resetInput();
return 1;
}
}
}
// Remote clicks invalidate a typing range just like local clicks.
if (down || wp == WM_MOUSEWHEEL || wp == WM_MOUSEHWHEEL)
{
if (app.busy) diagnosticEvent(L"Input context changed: mouse message=" + std::to_wstring(wp));
app.resetInput();
}
}
return CallNextHookEx(nullptr, code, wp, lp);
@@ -2205,6 +2508,13 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
case WM_HOTKEY:
app->dispatch((int)wp);
return 0;
case MouseActionMessage:
app->dispatchMouseActions();
return 0;
case MouseVolumeReleasedMessage:
if (const auto indicator = reinterpret_cast<HWND>(GetPropW(hwnd, VolumeIndicatorClass)))
setVolumeIndicatorHold(indicator, app->volumeButtonHeld());
return 0;
case WM_POWERBROADCAST:
if (wp == PBT_APMSUSPEND && app->lanService)
{
@@ -2294,6 +2604,12 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
if(app->lanService&&app->lanService->tickInput(!app->busy&&!app->suspended&&!app->hotkeyCapture))app->resetLanInput();
return 0;
case WM_TIMER:
if (wp == MouseClickTimer) { app->flushMouseClicks(); return 0; }
if (wp >= FirstMouseVolumeTimer && wp < FirstMouseVolumeTimer + std::size(MouseButtonNames))
{
app->repeatMouseVolume(static_cast<int>(wp - FirstMouseVolumeTimer));
return 0;
}
if (wp == 8)
{
if (app->settings.appLanguageSets.empty() || app->busy || app->installingUpdate) return 0;
+19
View File
@@ -5,6 +5,8 @@
#include "sync.h"
#include "chat_context.h"
#include "lan.h"
#include "mouse_buttons.h"
#include <array>
#include <atomic>
#include <condition_variable>
#include <deque>
@@ -105,6 +107,23 @@ class App
friend struct InputRegressionAccess;
void releaseIdleMemory(ULONGLONG now);
HHOOK keyboardHook{}, mouseHook{};
struct MousePress
{
HWND target{};
POINT point{};
ULONGLONG started{};
MouseAction action;
bool held{}, waitForSecond{};
};
std::array<MousePress, std::size(MouseButtonNames)> mousePresses{};
std::array<MousePress, std::size(MouseButtonNames)> pendingMouseClicks{};
std::deque<MousePress> mouseActions;
void queueMouseAction(MousePress);
void flushMouseClicks();
void beginMouseVolume(int button);
void repeatMouseVolume(int button);
bool volumeButtonHeld() const;
void dispatchMouseActions();
std::unique_ptr<LanService> lanService;
HANDLE historyProcess{};
std::mutex jobsMutex;
+15
View File
@@ -3,6 +3,7 @@
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <string>
#include <vector>
#include <windows.h>
@@ -58,6 +59,17 @@ struct AppLanguageSet
std::vector<std::wstring> languages, applications;
bool operator==(const AppLanguageSet &) const = default;
};
enum class MouseActionType { Standard, Shortcut, Back, Forward, VolumeUp, VolumeDown, Mute, Launch };
inline constexpr int DefaultMouseVolumeStep = 4;
struct MouseAction
{
MouseActionType type = MouseActionType::Standard;
Hotkey shortcut;
std::wstring executable;
std::set<std::wstring> applications{};
int volumeStep = DefaultMouseVolumeStep;
bool operator==(const MouseAction &) const = default;
};
struct Settings
{
bool recent = true, switchLayout = true, history = true, autostart = false, awake = false;
@@ -88,6 +100,9 @@ struct Settings
// Exact application + visible chat name; an absent entry uses the global cycle.
std::map<std::wstring, std::vector<std::wstring>> chatCycles;
std::map<std::wstring, AppLanguageSet> appLanguageSets;
std::map<std::wstring, std::map<std::wstring, MouseAction>> mouseBindings;
std::set<std::wstring> disabledMouseProfiles;
std::map<std::wstring, std::set<std::wstring>> mouseProfileApplications;
std::vector<std::wstring> excluded{L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
std::vector<Snippet> snippets;
std::vector<std::wstring> snippetTombstones;
+123
View File
@@ -0,0 +1,123 @@
#pragma once
#include "core.h"
#include <cstdlib>
namespace ks
{
inline constexpr const wchar_t *MouseButtonNames[] = {L"Left", L"Right", L"Middle", L"XButton1", L"XButton2"};
inline constexpr const wchar_t *MouseEvents[] = {L"Left", L"Right", L"Middle", L"XButton1", L"XButton2",
L"Double left", L"Double right", L"Double middle"};
inline constexpr const wchar_t *MouseActions[] = {L"Standard behavior", L"Send key combination", L"Back", L"Forward",
L"Volume up", L"Volume down", L"Mute", L"Launch application"};
inline constexpr wchar_t GlobalMouseProfile[] = L"*";
inline constexpr std::wstring_view NamedMouseProfilePrefix = L"name:";
bool validMouseApplication(const std::wstring &app);
inline bool isNamedMouseProfile(std::wstring_view profile)
{
return profile.starts_with(NamedMouseProfilePrefix);
}
inline std::wstring mouseProfileName(const std::wstring &profile)
{
if (profile == GlobalMouseProfile) return L"Global";
return isNamedMouseProfile(profile) ? profile.substr(NamedMouseProfilePrefix.size()) : profile;
}
inline bool mouseActionApplies(const MouseAction &action, const std::wstring &app)
{
return action.applications.empty() || action.applications.contains(app);
}
inline const std::set<std::wstring> &mouseActionScope(const Settings &settings, const std::wstring &profile,
const MouseAction &action)
{
const auto scope = settings.mouseProfileApplications.find(profile);
return scope == settings.mouseProfileApplications.end() ? action.applications : scope->second;
}
inline bool isMouseVolumeAction(const MouseAction *action)
{
return action && (action->type == MouseActionType::VolumeUp ||
action->type == MouseActionType::VolumeDown || action->type == MouseActionType::Mute);
}
inline bool isMouseVolumeStep(MouseActionType action)
{
return action == MouseActionType::VolumeUp || action == MouseActionType::VolumeDown;
}
inline constexpr UINT MouseVolumeHoldDelay = 350;
inline UINT mouseVolumeRepeatInterval(ULONGLONG heldFor)
{
if (heldFor < 1000) return 140;
if (heldFor < 2000) return 80;
return 40;
}
inline const MouseAction *mouseBinding(const Settings &settings, const std::wstring &app, int event)
{
auto profile = settings.mouseBindings.find(app);
if (profile == settings.mouseBindings.end()) return nullptr;
auto action = profile->second.find(MouseEvents[event]);
return action == profile->second.end() ? nullptr : &action->second;
}
inline const MouseAction *effectiveMouseAction(const Settings &settings, const std::wstring &app, int event,
bool foreground = true, bool *profileScoped = nullptr)
{
if (profileScoped) *profileScoped = false;
auto *action = mouseBinding(settings, app, event);
if (!action)
{
const MouseAction *unrestricted = nullptr;
for (auto profile = settings.mouseBindings.lower_bound(std::wstring(NamedMouseProfilePrefix));
profile != settings.mouseBindings.end() && isNamedMouseProfile(profile->first); ++profile)
{
if (settings.disabledMouseProfiles.contains(profile->first)) continue;
const auto binding = profile->second.find(MouseEvents[event]);
if (binding == profile->second.end()) continue;
const auto &candidate = binding->second;
const auto &scope = mouseActionScope(settings, profile->first, candidate);
if (foreground && scope.contains(app))
{
if (profileScoped) *profileScoped = true;
return candidate.type == MouseActionType::Standard ? nullptr : &candidate;
}
if (!unrestricted && scope.empty()) unrestricted = &candidate;
}
const auto *global = mouseBinding(settings, GlobalMouseProfile, event);
if (foreground && global && global->applications.contains(app)) action = global;
else if (unrestricted) action = unrestricted;
else if (global && global->applications.empty()) action = global;
}
return action && action->type != MouseActionType::Standard && mouseActionApplies(*action, app) ? action : nullptr;
}
inline bool doubleMouseClick(POINT first, POINT second, ULONGLONG elapsed)
{
return elapsed <= GetDoubleClickTime() &&
std::abs(static_cast<long long>(first.x) - second.x) * 2 <= GetSystemMetrics(SM_CXDOUBLECLK) &&
std::abs(static_cast<long long>(first.y) - second.y) * 2 <= GetSystemMetrics(SM_CYDOUBLECLK);
}
inline int mouseButton(UINT message, WORD xbutton)
{
switch (message)
{
case WM_LBUTTONDOWN: case WM_LBUTTONUP: return 0;
case WM_RBUTTONDOWN: case WM_RBUTTONUP: return 1;
case WM_MBUTTONDOWN: case WM_MBUTTONUP: return 2;
case WM_XBUTTONDOWN: case WM_XBUTTONUP:
if (xbutton == XBUTTON1) return 3;
if (xbutton == XBUTTON2) return 4;
}
return -1;
}
inline bool mouseButtonDown(UINT message)
{
return message == WM_LBUTTONDOWN || message == WM_RBUTTONDOWN ||
message == WM_MBUTTONDOWN || message == WM_XBUTTONDOWN;
}
}
+123 -1
View File
@@ -4,12 +4,19 @@
#include <fstream>
#include <iomanip>
#include <imm.h>
#include <endpointvolume.h>
#include <mmdeviceapi.h>
#include <map>
#include <memory>
#include <mutex>
#include <set>
#include <shellapi.h>
#include <shlobj.h>
#include <exdisp.h>
#include <shldisp.h>
#include <tlhelp32.h>
#include <wincrypt.h>
#include <wrl/client.h>
namespace ks
{
@@ -265,6 +272,83 @@ std::wstring processName(HWND window)
CloseHandle(h);
return ok ? std::filesystem::path(std::wstring(path, n)).stem().wstring() : L"";
}
std::wstring mouseApplicationName(HWND window)
{
auto name = lower(processName(window));
if (name != L"applicationframehost") return name;
std::wstring hosted;
EnumChildWindows(window, [](HWND child, LPARAM output) -> BOOL {
wchar_t kind[64]{};
if (!GetClassNameW(child, kind, std::size(kind)) || wcscmp(kind, L"Windows.UI.Core.CoreWindow") != 0)
return TRUE;
auto &app = *reinterpret_cast<std::wstring *>(output);
app = lower(processName(child));
return app.empty();
}, reinterpret_cast<LPARAM>(&hosted));
return hosted.empty() ? name : hosted;
}
bool launchThroughExplorer(const std::wstring &path)
{
HANDLE token{};
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return false;
TOKEN_ELEVATION elevation{};
DWORD size{};
const auto queried = GetTokenInformation(token, TokenElevation, &elevation, sizeof(elevation), &size);
CloseHandle(token);
if (!queried) return false;
if (!elevation.TokenIsElevated)
{
SHELLEXECUTEINFOW launch{sizeof(launch)};
launch.fMask = SEE_MASK_FLAG_NO_UI;
launch.lpFile = path.c_str();
launch.nShow = SW_SHOWNORMAL;
return ShellExecuteExW(&launch) != FALSE;
}
// An elevated resident delegates execution to the interactive desktop shell.
auto release = [](IUnknown *object) { if (object) object->Release(); };
IShellWindows *windowsRaw{};
auto hr = CoCreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_ALL, IID_IShellWindows,
reinterpret_cast<void **>(&windowsRaw));
std::unique_ptr<IShellWindows, decltype(release)> windows(windowsRaw, release);
if (FAILED(hr) || !windows) return false;
VARIANT empty{}, desktop{};
desktop.vt = VT_I4;
long windowHandle{};
IDispatch *desktopRaw{};
hr = windows->FindWindowSW(&desktop, &empty, SWC_DESKTOP, &windowHandle, SWFO_NEEDDISPATCH, &desktopRaw);
std::unique_ptr<IDispatch, decltype(release)> dispatch(desktopRaw, release);
if (FAILED(hr) || !dispatch) return false;
IServiceProvider *providerRaw{};
hr = dispatch->QueryInterface(IID_IServiceProvider, reinterpret_cast<void **>(&providerRaw));
std::unique_ptr<IServiceProvider, decltype(release)> provider(providerRaw, release);
if (FAILED(hr) || !provider) return false;
IShellBrowser *browserRaw{};
hr = provider->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, reinterpret_cast<void **>(&browserRaw));
std::unique_ptr<IShellBrowser, decltype(release)> browser(browserRaw, release);
if (FAILED(hr) || !browser) return false;
IShellView *viewRaw{};
hr = browser->QueryActiveShellView(&viewRaw);
std::unique_ptr<IShellView, decltype(release)> view(viewRaw, release);
if (FAILED(hr) || !view) return false;
IShellFolderViewDual *folderRaw{};
hr = view->GetItemObject(SVGIO_BACKGROUND, IID_IShellFolderViewDual, reinterpret_cast<void **>(&folderRaw));
std::unique_ptr<IShellFolderViewDual, decltype(release)> folder(folderRaw, release);
if (FAILED(hr) || !folder) return false;
IDispatch *applicationRaw{};
hr = folder->get_Application(&applicationRaw);
std::unique_ptr<IDispatch, decltype(release)> application(applicationRaw, release);
if (FAILED(hr) || !application) return false;
IShellDispatch2 *shellRaw{};
hr = application->QueryInterface(IID_IShellDispatch2, reinterpret_cast<void **>(&shellRaw));
std::unique_ptr<IShellDispatch2, decltype(release)> shell(shellRaw, release);
if (FAILED(hr) || !shell) return false;
BSTR executable = SysAllocString(path.c_str());
if (!executable) return false;
hr = shell->ShellExecute(executable, empty, empty, empty, empty);
SysFreeString(executable);
return SUCCEEDED(hr);
}
Clip readClipboard(HWND window, size_t limit, ClipboardRead mode, std::optional<DWORD> expected)
{
ClipboardLock lock(window);
@@ -411,7 +495,9 @@ bool chord(WORD key, UINT mods)
auto add = [&](WORD k, bool up)
{
INPUT i = keyboardInput(k, up ? KEYEVENTF_KEYUP : 0);
if (k == VK_LEFT || k == VK_RIGHT || k == VK_INSERT)
if ((k >= VK_PRIOR && k <= VK_DOWN) || k == VK_INSERT || k == VK_DELETE ||
k == VK_DIVIDE || k == VK_LWIN || k == VK_SNAPSHOT ||
(k >= VK_VOLUME_MUTE && k <= VK_VOLUME_UP))
i.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
inputs.push_back(i);
};
@@ -431,6 +517,42 @@ bool chord(WORD key, UINT mods)
add(k, true);
return SendInput((UINT)inputs.size(), inputs.data(), sizeof(INPUT)) == inputs.size();
}
std::optional<SystemVolume> changeSystemVolume(MouseActionType action, int step)
{
using Microsoft::WRL::ComPtr;
ComPtr<IMMDeviceEnumerator> devices;
ComPtr<IMMDevice> output;
ComPtr<IAudioEndpointVolume> volume;
if (FAILED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&devices))) ||
FAILED(devices->GetDefaultAudioEndpoint(eRender, eMultimedia, &output)) ||
FAILED(output->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr,
reinterpret_cast<void **>(volume.GetAddressOf()))))
return std::nullopt;
float level{};
BOOL muted{};
HRESULT changed;
switch (action)
{
case MouseActionType::VolumeUp:
case MouseActionType::VolumeDown:
{
if (FAILED(volume->GetMasterVolumeLevelScalar(&level))) return std::nullopt;
const float delta = (action == MouseActionType::VolumeUp ? step : -step) / 100.0f;
changed = volume->SetMasterVolumeLevelScalar(std::clamp(level + delta, 0.0f, 1.0f), nullptr);
break;
}
case MouseActionType::Mute:
{
if (FAILED(volume->GetMute(&muted))) return std::nullopt;
changed = volume->SetMute(!muted, nullptr);
break;
}
default: return std::nullopt;
}
if (FAILED(changed) || FAILED(volume->GetMasterVolumeLevelScalar(&level)) || FAILED(volume->GetMute(&muted)))
return std::nullopt;
return SystemVolume{std::clamp(static_cast<int>(level * 100 + 0.5f), 0, 100), muted != FALSE};
}
bool needsSelectionDelay(HWND target)
{
const auto name = lower(processName(target));
+8
View File
@@ -43,6 +43,8 @@ bool ownsClipboard(HWND);
bool webViewDescendant(DWORD owner, DWORD host,
const std::function<std::optional<std::pair<DWORD, std::wstring>>(DWORD)> &read);
std::wstring processName(HWND);
std::wstring mouseApplicationName(HWND);
bool launchThroughExplorer(const std::wstring &);
enum class ClipboardRead { All, Selection, WithoutImages };
Clip readClipboard(HWND, size_t limit = 32 * 1024 * 1024,
ClipboardRead = ClipboardRead::All, std::optional<DWORD> expected = {});
@@ -50,6 +52,12 @@ DWORD writeClipboard(HWND, const Clip &);
DWORD writeClipboardGuarded(HWND, const Clip &, std::optional<DWORD> expected);
Clip textClip(const std::wstring &);
bool chord(WORD key, UINT modifiers = 0);
struct SystemVolume
{
int percent{};
bool muted{};
};
std::optional<SystemVolume> changeSystemVolume(MouseActionType action, int step);
// Process-local marker, shared by every KSwitcher input producer (not an authentication token).
ULONG_PTR inputMarker();
bool ownKeyboardInput(const KBDLLHOOKSTRUCT &);
+169
View File
@@ -1,4 +1,5 @@
#include "storage.h"
#include "mouse_buttons.h"
#include "json.hpp"
#include "ocr.h"
#include <algorithm>
@@ -68,8 +69,87 @@ void validateLanguageCycle(const std::vector<std::wstring> &cycle)
throw std::runtime_error("Invalid or duplicate language layout.");
}
} // namespace
bool validMouseApplication(const std::wstring &app)
{
return !app.empty() && app.size() <= 260 && app == lower(app) &&
app.find_first_of(L"\\/:*?\"<>|\r\n") == app.npos &&
app != L"kswitcher" && app != L"kswitcher.config";
}
static void validateMouseProfileConflicts(const Settings &s)
{
std::map<std::pair<std::wstring, std::wstring>, std::wstring> claimed;
for (const auto &[profile, bindings] : s.mouseBindings)
{
if (s.disabledMouseProfiles.contains(profile)) continue;
for (const auto &[event, action] : bindings)
{
auto claim = [&](const std::wstring &app)
{
const auto [other, added] = claimed.try_emplace({event, app}, profile);
if (!added && (isNamedMouseProfile(profile) || isNamedMouseProfile(other->second)))
throw std::runtime_error(utf8(L"Mouse action conflict: " + event + L" for " +
(app.empty() ? L"all applications" : app + L".exe") + L" in " +
mouseProfileName(other->second) + L" and " + mouseProfileName(profile) + L"."));
};
if (profile != GlobalMouseProfile && !isNamedMouseProfile(profile))
{
if (mouseActionApplies(action, profile)) claim(profile);
}
else
{
const auto &scope = mouseActionScope(s, profile, action);
if (scope.empty()) claim(L"");
else for (const auto &app : scope) claim(app);
}
}
}
}
void validateSettings(const Settings &s)
{
if (s.mouseBindings.size() > 1000) throw std::runtime_error("Too many mouse profiles.");
for (const auto &[app, bindings] : s.mouseBindings)
{
if (isNamedMouseProfile(app))
{
const auto name = mouseProfileName(app);
if (name.empty() || name.size() > 80 || lower(name) == L"global" || name.find_first_of(L"\r\n\t") != name.npos)
throw std::runtime_error("Enter a profile name other than Global (up to 80 characters).");
}
else if (app != GlobalMouseProfile && !validMouseApplication(app))
throw std::runtime_error("Enter the target application's executable name.");
for (const auto &[event, action] : bindings)
{
if (action.applications.size() > 1000 ||
!std::all_of(action.applications.begin(), action.applications.end(), validMouseApplication))
throw std::runtime_error("Enter valid target application executable names.");
if (std::find(std::begin(MouseEvents), std::end(MouseEvents), event) == std::end(MouseEvents) ||
static_cast<size_t>(action.type) >= std::size(MouseActions))
throw std::runtime_error("Invalid mouse event or action.");
if (isMouseVolumeStep(action.type) && (action.volumeStep < 1 || action.volumeStep > 100))
throw std::runtime_error("Enter a volume step from 1 to 100.");
if (action.type == MouseActionType::Shortcut &&
(!action.shortcut.key || Hotkey::parse(action.shortcut.format()) != action.shortcut))
throw std::runtime_error("Enter a shortcut such as Ctrl+Shift+Tab.");
if (action.type == MouseActionType::Launch)
{
const std::filesystem::path path(action.executable);
const bool executableName = !path.has_root_path() && !path.has_parent_path() &&
action.executable.find_first_of(L"\\/:*?\"<>|\r\n") == std::wstring::npos;
if ((!executableName && !path.is_absolute()) || lower(path.extension().wstring()) != L".exe")
throw std::runtime_error("Enter an executable name such as calc.exe, or choose a file with Browse.");
}
}
}
for (const auto &profile : s.disabledMouseProfiles)
if (!isNamedMouseProfile(profile) || !s.mouseBindings.contains(profile))
throw std::runtime_error("Unknown disabled mouse profile.");
for (const auto &[profile, apps] : s.mouseProfileApplications)
if (!isNamedMouseProfile(profile) || !s.mouseBindings.contains(profile) || apps.size() > 1000 ||
!std::all_of(apps.begin(), apps.end(), validMouseApplication))
throw std::runtime_error("Invalid applications for the mouse profile.");
validateMouseProfileConflicts(s);
if (s.appLanguageSets.size() > 1000) throw std::runtime_error("Too many application language sets");
std::set<std::wstring> assigned;
for (const auto &[name, set] : s.appLanguageSets)
@@ -302,6 +382,63 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
s.appLanguageSets[wide(name)] = std::move(set);
}
}
if (j.contains("mouseBindings"))
{
const auto &profiles = j.at("mouseBindings");
if (!profiles.is_object()) throw std::runtime_error("Invalid mouse profiles.");
for (const auto &[app, bindings] : profiles.items())
{
if (!bindings.is_object()) throw std::runtime_error("Invalid mouse profile.");
if (isNamedMouseProfile(wide(app))) s.mouseBindings.try_emplace(wide(app));
for (const auto &[event, item] : bindings.items())
{
MouseAction action;
if (item.is_string())
{
auto shortcut = Hotkey::parse(wide(item.get<std::string>()));
if (!shortcut || !shortcut->key) throw std::runtime_error("Invalid mouse shortcut.");
action = {MouseActionType::Shortcut, *shortcut};
}
else
{
auto name = wide(item.at("action").get<std::string>());
if (name == L"Send shortcut") name = MouseActions[static_cast<size_t>(MouseActionType::Shortcut)];
const auto found = std::find(std::begin(MouseActions), std::end(MouseActions), name);
if (found == std::end(MouseActions)) throw std::runtime_error("Invalid mouse action.");
action.type = static_cast<MouseActionType>(found - std::begin(MouseActions));
if (action.type == MouseActionType::Shortcut)
action.shortcut = Hotkey::parse(wide(item.at("shortcut").get<std::string>())).value_or(Hotkey{});
if (action.type == MouseActionType::Launch)
action.executable = wide(item.at("executable").get<std::string>());
if (isMouseVolumeStep(action.type) && item.contains("volumeStep"))
{
const auto &step = item.at("volumeStep");
if (!step.is_number_integer() || step.get<double>() < 1 || step.get<double>() > 100)
throw std::runtime_error("Enter a volume step from 1 to 100.");
action.volumeStep = step.get<int>();
}
if (item.contains("applications"))
for (const auto &name : item.at("applications").get<std::vector<std::string>>())
action.applications.insert(wide(name));
}
s.mouseBindings[wide(app)][wide(event)] = std::move(action);
}
}
}
if (j.contains("mouseProfileApplications"))
{
const auto &scopes = j.at("mouseProfileApplications");
if (!scopes.is_object()) throw std::runtime_error("Invalid mouse profile applications.");
for (const auto &[profile, names] : scopes.items())
{
if (names.is_null()) continue;
auto &scope = s.mouseProfileApplications[wide(profile)];
for (const auto &name : names.get<std::vector<std::string>>()) scope.insert(wide(name));
}
}
if (j.contains("disabledMouseProfiles") && !j.at("disabledMouseProfiles").is_null())
for (const auto &profile : j.at("disabledMouseProfiles").get<std::vector<std::string>>())
s.disabledMouseProfiles.insert(wide(profile));
if (j.contains("chatCycles"))
{
if (!j.at("chatCycles").is_object()) throw std::runtime_error("Invalid chat profiles object");
@@ -383,6 +520,38 @@ Bytes encodeSettings(const Settings &s)
for (const auto &id : set.languages) item["languages"].push_back(utf8(id));
for (const auto &app : set.applications) item["applications"].push_back(utf8(app));
}
j["mouseBindings"] = Json::object();
for (const auto &[app, bindings] : s.mouseBindings)
{
if (isNamedMouseProfile(app)) j["mouseBindings"][utf8(app)] = Json::object();
for (const auto &[event, action] : bindings)
{
auto &item = j["mouseBindings"][utf8(app)][utf8(event)];
if (action.type == MouseActionType::Shortcut && action.applications.empty())
item = utf8(action.shortcut.format());
else
{
item = {{"action", utf8(MouseActions[static_cast<size_t>(action.type)])}};
if (action.type == MouseActionType::Shortcut)
{
item["action"] = "Send shortcut";
item["shortcut"] = utf8(action.shortcut.format());
}
if (action.type == MouseActionType::Launch) item["executable"] = utf8(action.executable);
if (isMouseVolumeStep(action.type) && action.volumeStep != DefaultMouseVolumeStep)
item["volumeStep"] = action.volumeStep;
for (const auto &app : action.applications) item["applications"].push_back(utf8(app));
}
}
}
j["mouseProfileApplications"] = Json::object();
for (const auto &[profile, apps] : s.mouseProfileApplications)
{
auto &scope = j["mouseProfileApplications"][utf8(profile)] = Json::array();
for (const auto &app : apps) scope.push_back(utf8(app));
}
j["disabledMouseProfiles"] = Json::array();
for (const auto &profile : s.disabledMouseProfiles) j["disabledMouseProfiles"].push_back(utf8(profile));
j["chatCycles"] = Json::object();
for (const auto &[key, cycle] : s.chatCycles)
for (const auto &id : cycle) j["chatCycles"][utf8(key)].push_back(utf8(id));
+3 -1
View File
@@ -1,4 +1,5 @@
#include "sync.h"
#include "mouse_buttons.h"
#include "json.hpp"
#include <winhttp.h>
#include <wincred.h>
@@ -226,7 +227,8 @@ Bytes sharedSyncSettings(const Settings &s)
return a.at("layoutId").get<std::string>() < b.at("layoutId").get<std::string>();
});
for (auto key : {"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock", "appLanguageSets"}) j.erase(key);
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock",
"appLanguageSets", "mouseBindings", "disabledMouseProfiles", "mouseProfileApplications"}) j.erase(key);
return bytes(canonicalProfile(j));
}
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
+20 -2
View File
@@ -58,6 +58,20 @@ Json flatProfile(Json j)
for (auto it = chats.begin(); it != chats.end();)
if (it.value().is_null()) it = chats.erase(it);
else ++it;
if (j.contains("mouseBindings"))
{
auto &profiles = j.at("mouseBindings");
for (auto profile = profiles.begin(); profile != profiles.end();)
{
auto &buttons = profile.value();
for (auto button = buttons.begin(); button != buttons.end();)
if (button.value().is_null()) button = buttons.erase(button);
else ++button;
if (buttons.is_null() || (buttons.empty() && !isNamedMouseProfile(wide(profile.key()))))
profile = profiles.erase(profile);
else ++profile;
}
}
Json snippets = Json::array(), deleted = Json::array();
for (const auto &[id, item] : j.at("snippets").items())
{
@@ -99,7 +113,7 @@ std::wstring fieldLabel(const std::string &key)
{"translitHotkey", L"Transliteration shortcut"}, {"undoHotkey", L"Undo shortcut"},
{"clipboardHotkey", L"Clipboard shortcut"}, {"screenshotHotkey", L"Screenshot shortcut"},
{"layouts", L"Layout cycle order"}, {"layoutHotkeys", L"Layout shortcuts"},
{"chatCycles", L"Chat languages"},
{"chatCycles", L"Chat languages"}, {"mouseBindings", L"Mouse buttons"},
{"convertLastWord", L"Convert recent word"}, {"switchSystemLayout", L"Switch system layout"},
{"autoCorrectMinLength", L"Minimum word length"}, {"autoCorrectMaxRank", L"Maximum word rank"},
{"autoCorrectLayout", L"Automatic layout correction"}, {"autoCorrectLanguages", L"Dictionary languages"},
@@ -133,7 +147,11 @@ MergeNode mergeNode(const Json &b, const Json &l, const Json &r, const std::stri
bool forcedChild = std::any_of(forced.begin(), forced.end(), [&](const std::string &key) { return key.starts_with(path + "/"); });
if (!forcedChild && l == b) return {r, r, r};
if (!forcedChild && r == b) return {l, l, l};
if (l.is_object() && r.is_object() && (b.is_object() || b.is_null()) && !tombstone(l) && !tombstone(r) && !tombstone(b))
// A mouse action and its application scope must come from the same edit.
constexpr std::string_view mouseProfiles = "/mouseBindings/";
const bool mouseAction = path.starts_with(mouseProfiles) && path.find('/', mouseProfiles.size()) != std::string::npos;
if (!mouseAction && l.is_object() && r.is_object() && (b.is_object() || b.is_null()) &&
!tombstone(l) && !tombstone(r) && !tombstone(b))
{
MergeNode out{Json::object(), Json::object(), Json::object()};
std::set<std::string> keys;
+114 -23
View File
@@ -15,7 +15,9 @@
#include <richedit.h>
#include <set>
#include <shellapi.h>
#include <shlobj.h>
#include <sstream>
#include <wrl/client.h>
namespace ks
{
@@ -40,7 +42,12 @@ struct WindowLayout
std::vector<std::pair<HWND, RECT>> controls; // Original 96-DPI geometry; never rescale rounded pixels.
HWND stretch{}, bottomLeft{}, bottomRight{}, details{};
bool autoOptions{};
bool fixedClient{}, mouseSettings{};
HWND rowCombo{};
};
#ifndef KS_RESIDENT_APP
void scaleMouseTableColumns(HWND w, int previousDpi, int nextDpi);
#endif
WindowLayout *windowLayoutState(HWND w)
{
return reinterpret_cast<WindowLayout *>(GetPropW(w, L"WindowLayout"));
@@ -57,24 +64,44 @@ void fitToMonitor(HWND w, RECT rect)
SetWindowPos(w, nullptr, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
SWP_NOACTIVATE | SWP_NOZORDER);
}
void fitFixedClient(HWND w, WindowLayout &layout, RECT frame)
{
RECT current{}, client{};
GetWindowRect(w, &current);
GetClientRect(w, &client);
const auto style = GetWindowLongPtrW(w, GWL_STYLE);
const UINT dpi = GetDpiForWindow(w);
const int vertical = (style & WS_VSCROLL) ? GetSystemMetricsForDpi(SM_CXVSCROLL, dpi) : 0;
const int horizontal = (style & WS_HSCROLL) ? GetSystemMetricsForDpi(SM_CYHSCROLL, dpi) : 0;
frame.right = frame.left + scale(w, layout.width) + current.right - current.left - client.right - vertical;
frame.bottom = frame.top + scale(w, layout.height) + current.bottom - current.top - client.bottom - horizontal;
fitToMonitor(w, frame);
}
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);
}
RECT client{}; GetClientRect(w, &client);
RECT client{};
GetClientRect(w, &client);
const auto style = GetWindowLongPtrW(w, GWL_STYLE);
const UINT dpi = GetDpiForWindow(w);
const int verticalBar = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi);
const int horizontalBar = GetSystemMetricsForDpi(SM_CYHSCROLL, dpi);
// Start from the full client area so two obsolete bars cannot keep each other visible.
const int fullWidth = client.right + ((style & WS_VSCROLL) ? verticalBar : 0);
const int fullHeight = client.bottom + ((style & WS_HSCROLL) ? horizontalBar : 0);
const int width = scale(w, layout->width);
const int height = scale(w, layout->height);
const bool needsHorizontal = width > fullWidth || (height > fullHeight && width > fullWidth - verticalBar);
const bool needsVertical = height > fullHeight - (needsHorizontal ? horizontalBar : 0);
SCROLLINFO horizontal{sizeof(horizontal), SIF_RANGE | SIF_PAGE | SIF_POS, 0, width - 1,
(UINT)std::max(1, fullWidth - (needsVertical ? verticalBar : 0)), layout->x};
SCROLLINFO vertical{sizeof(vertical), SIF_RANGE | SIF_PAGE | SIF_POS, 0, height - 1,
(UINT)std::max(1, fullHeight - (needsHorizontal ? horizontalBar : 0)), layout->y};
layout->x = SetScrollInfo(w, SB_HORZ, &horizontal, TRUE);
layout->y = SetScrollInfo(w, SB_VERT, &vertical, TRUE);
GetClientRect(w, &client);
bool expandable = layout->history || layout->stretch || layout->autoOptions;
int extraWidth = expandable ? std::max(0, int(client.right) - scale(w, layout->width)) : 0;
int extraHeight = expandable ? std::max(0, int(client.bottom) - scale(w, layout->height)) : 0;
@@ -143,6 +170,15 @@ void arrangeWindow(HWND w)
SWP_NOACTIVATE | SWP_NOZORDER);
}
}
if (layout->rowCombo)
{
RECT bounds{};
GetWindowRect(layout->rowCombo, &bounds);
const int difference = scale(w, 25) - (bounds.bottom - bounds.top);
const auto itemHeight = SendMessageW(layout->rowCombo, CB_GETITEMHEIGHT, static_cast<WPARAM>(-1), 0);
if (difference && itemHeight != CB_ERR && itemHeight + difference > 0)
SendMessageW(layout->rowCombo, CB_SETITEMHEIGHT, static_cast<WPARAM>(-1), itemHeight + difference);
}
layout->arranging = false;
}
void scrollWindow(HWND w, WindowLayout *layout, bool horizontal, int requested)
@@ -191,7 +227,8 @@ LRESULT CALLBACK layoutProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
{
RECT rect{};
GetWindowRect(w, &rect);
fitToMonitor(w, rect);
if (layout->fixedClient) fitFixedClient(w, *layout, rect);
else fitToMonitor(w, rect);
arrangeWindow(w);
}
if (layout->scrolling && (msg == WM_VSCROLL || msg == WM_HSCROLL))
@@ -340,7 +377,7 @@ void error(HWND w, const std::exception &e)
MessageBoxW(w, wide(e.what()).c_str(), L"KSwitcher", MB_OK | MB_ICONWARNING);
}
HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc, int width, int height,
DWORD extra = 0, bool resizable = true)
DWORD extra = 0, bool resizable = true, HWND owner = nullptr)
{
WNDCLASSW wc{};
wc.hInstance = app.instance;
@@ -354,22 +391,25 @@ HWND makeWindow(App &app, const wchar_t *cls, const wchar_t *title, WNDPROC proc
RECT rect{0, 0, MulDiv(width, dpi, 96), MulDiv(height, dpi, 96)};
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPCHILDREN | extra;
if (resizable) style |= WS_THICKFRAME | WS_MAXIMIZEBOX;
AdjustWindowRectExForDpi(&rect, style, FALSE, WS_EX_APPWINDOW, dpi);
const DWORD exStyle = owner ? WS_EX_DLGMODALFRAME : WS_EX_APPWINDOW;
AdjustWindowRectExForDpi(&rect, style, FALSE, exStyle, dpi);
POINT cursor{};
GetCursorPos(&cursor);
MONITORINFO monitor{sizeof(monitor)};
GetMonitorInfoW(MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST), &monitor);
HWND w =
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);
CreateWindowExW(exStyle, cls, title, style, monitor.rcWork.left, monitor.rcWork.top,
rect.right - rect.left, rect.bottom - rect.top, owner, 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;
RECT frame{}, client{};
GetWindowRect(w, &frame);
GetClientRect(w, &client);
int outerWidth = scale(w, width) + (frame.right - frame.left) - client.right;
int outerHeight = scale(w, height) + (frame.bottom - frame.top) - client.bottom;
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)});
@@ -441,7 +481,17 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
if (layout && layout->history)
SendDlgItemMessageW(w, 102, LB_SETITEMHEIGHT, 0, scale(w, 88));
fitToMonitor(w, *(RECT *)lp);
if (layout && layout->fixedClient)
{
RECT frame{};
GetWindowRect(w, &frame);
fitFixedClient(w, *layout, frame);
}
arrangeWindow(w);
#ifndef KS_RESIDENT_APP
if (layout && layout->mouseSettings && previous != next)
scaleMouseTableColumns(w, previous, next);
#endif
}
[[maybe_unused]] std::filesystem::path pickFile(HWND owner, bool save)
{
@@ -1046,6 +1096,9 @@ struct SettingsView
bool filling{};
std::vector<std::vector<HWND>> pages;
bool ready{}, refreshing{}, visitedLan{};
std::vector<std::wstring> mouseProfiles;
unsigned mouseEvents{}, rawMouseEvents{};
bool rawMouseRegistered{};
};
void updateSettingsState(HWND w, SettingsView *v);
void selectSettingsPage(HWND w, SettingsView *v)
@@ -1190,6 +1243,7 @@ void showChatLanguages(HWND parent, SettingsView *settings)
SetFocus(GetDlgItem(w, 710));
}
#include "ui_app_languages.inl"
#include "ui_mouse_buttons.inl"
void collectLayout(HWND w, SettingsView *v)
{
if (v->selection < 0 || v->selection >= (int)v->order.size())
@@ -1268,6 +1322,7 @@ void fillSettings(HWND w, SettingsView *v)
v->order.push_back(i);
v->selection = 0;
fillLayouts(w, v);
fillMouseProfiles(w, *v);
v->filling = false;
updateSettingsState(w, v);
}
@@ -1710,6 +1765,16 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg)
{
case WM_DRAWITEM:
if (wp == AddMouseProfile || wp == RenameMouseProfile || wp == DeleteMouseProfile)
{
drawActionApplicationIcon(w, *reinterpret_cast<const DRAWITEMSTRUCT *>(lp));
return TRUE;
}
break;
case WM_INPUT:
readMouseTestInput(w, *v, reinterpret_cast<HRAWINPUT>(lp));
return DefWindowProcW(w, msg, wp, lp);
case ConfigSettingsDownloaded:
v->draft = v->app->settings;
fillSettings(w, v);
@@ -1721,6 +1786,24 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (LOWORD(wp) != WA_INACTIVE) updateSettingsState(w, v);
break;
case WM_NOTIFY:
if (((NMHDR *)lp)->idFrom == 731)
{
if (((NMHDR *)lp)->code == LVN_ITEMCHANGING)
return blockMouseProfileStateChange(w, *v, *reinterpret_cast<const NMLISTVIEW *>(lp));
if (((NMHDR *)lp)->code == LVN_ITEMCHANGED)
{
const auto &change = *reinterpret_cast<const NMLISTVIEW *>(lp);
if (!(change.uOldState & LVIS_SELECTED) && (change.uNewState & LVIS_SELECTED))
fillMouseEvents(w, *v);
}
return 0;
}
if (((NMHDR *)lp)->idFrom == 733 && ((NMHDR *)lp)->code == NM_DBLCLK)
{
const auto &click = *reinterpret_cast<const NMITEMACTIVATE *>(lp);
showMouseActionEditor(w, *v, click.iItem);
return 0;
}
if (((NMHDR *)lp)->idFrom == 260 && ((NMHDR *)lp)->code == TCN_SELCHANGE)
{
selectSettingsPage(w, v);
@@ -1746,6 +1829,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (id == 258) { showSyncOptions(w, *v->app); return 0; }
if (id == 262) { v->visitedLan=true; showLan(w, *v->app); return 0; }
if (id == 263) { collectLayout(w, v); showAppLanguages(w, v); return 0; }
if (handleMouseBindings(w, *v, wp)) return 0;
if (id == 259) { collectLayout(w, v); showChatLanguages(w, v); return 0; }
if (id == 230)
{
@@ -1830,6 +1914,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
closeSettings(w, v);
return 0;
case WM_NCDESTROY:
if (v->rawMouseRegistered) registerMouseTestInput(nullptr);
disposeFont(w);
v->app->settingsWindow = nullptr;
delete v;
@@ -2134,13 +2219,16 @@ void showSettings(App &app, bool visible)
return;
}
HWND w = makeWindow(app, L"KSwitcher.Settings", L"KSwitcher settings", settingsProc, 720, 510, 0, false);
auto &layout = *windowLayoutState(w);
layout.fixedClient = true;
layout.mouseSettings = true;
app.settingsWindow = w;
auto *v = (SettingsView *)GetWindowLongPtrW(w, GWLP_USERDATA);
INITCOMMONCONTROLSEX tabsInit{sizeof(tabsInit), ICC_TAB_CLASSES};
InitCommonControlsEx(&tabsInit);
HWND tabs = control(w, WC_TABCONTROLW, L"", WS_TABSTOP, 260, 18, 16, 684, 32);
const wchar_t *tabNames[] = {L"General", L"Languages", L"Shortcuts", L"Clipboard", L"Tools"};
for (int i = 0; i < 5; ++i)
const wchar_t *tabNames[] = {L"General", L"Languages", L"Shortcuts", L"Clipboard", L"Tools", L"Mouse"};
for (int i = 0; i < (int)std::size(tabNames); ++i)
{
TCITEMW item{}; item.mask = TCIF_TEXT; item.pszText = const_cast<wchar_t *>(tabNames[i]);
TabCtrl_InsertItem(tabs, i, &item);
@@ -2216,6 +2304,9 @@ void showSettings(App &app, bool visible)
checkbox(w, L"Write lo&g", 208, 24, 378, 210);
page();
createMousePage(w, *v);
page();
control(w, L"STATIC", L"No unsaved changes.", 0, 261, 18, 430, 684, 24);
button(w, L"Impo&rt...", 250, 18, 466, 125);
button(w, L"&Export...", 251, 155, 466, 125);
+30 -20
View File
@@ -80,15 +80,15 @@ HWND appPickerTarget(HWND button, LPARAM position)
auto dialog = GetParent(button);
return target && target != dialog && target != GetWindow(dialog, GW_OWNER) ? target : nullptr;
}
void updatePickerOutline(HWND dialog, AppLanguagesView &v, HWND target)
void updatePickerOutline(HWND dialog, HWND &outline, HWND target)
{
RECT bounds{};
if (!target || !GetWindowRect(target, &bounds) || IsRectEmpty(&bounds))
{
if (v.pickerOutline) ShowWindow(v.pickerOutline, SW_HIDE);
if (outline) ShowWindow(outline, SW_HIDE);
return;
}
if (!v.pickerOutline)
if (!outline)
{
WNDCLASSW windowClass{};
windowClass.lpfnWndProc = DefWindowProcW;
@@ -96,18 +96,18 @@ void updatePickerOutline(HWND dialog, AppLanguagesView &v, HWND target)
windowClass.hbrBackground = GetSysColorBrush(COLOR_HIGHLIGHT);
windowClass.lpszClassName = L"KSwitcher.AppPickerOutline";
RegisterClassW(&windowClass);
v.pickerOutline = CreateWindowExW(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
outline = CreateWindowExW(WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
windowClass.lpszClassName, nullptr, WS_POPUP, 0, 0, 0, 0, dialog, nullptr, windowClass.hInstance, nullptr);
if (!v.pickerOutline) return;
if (!SetLayeredWindowAttributes(v.pickerOutline, 0, 255, LWA_ALPHA))
if (!outline) return;
if (!SetLayeredWindowAttributes(outline, 0, 255, LWA_ALPHA))
{
DestroyWindow(v.pickerOutline);
v.pickerOutline = nullptr;
DestroyWindow(outline);
outline = nullptr;
return;
}
}
RECT previous{};
if (IsWindowVisible(v.pickerOutline) && GetWindowRect(v.pickerOutline, &previous) && EqualRect(&bounds, &previous)) return;
if (IsWindowVisible(outline) && GetWindowRect(outline, &previous) && EqualRect(&bounds, &previous)) return;
int width = bounds.right - bounds.left, height = bounds.bottom - bounds.top;
int stroke = scale(target, 4);
HRGN frame = CreateRectRgn(0, 0, width, height);
@@ -121,17 +121,17 @@ void updatePickerOutline(HWND dialog, AppLanguagesView &v, HWND target)
DeleteObject(opening);
}
}
if (!SetWindowRgn(v.pickerOutline, frame, TRUE))
if (!SetWindowRgn(outline, frame, TRUE))
{
DeleteObject(frame);
return;
}
SetWindowPos(v.pickerOutline, HWND_TOPMOST, bounds.left, bounds.top, width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW);
SetWindowPos(outline, HWND_TOPMOST, bounds.left, bounds.top, width, height, SWP_NOACTIVATE | SWP_SHOWWINDOW);
}
LRESULT CALLBACK appWindowPickerProc(HWND button, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR)
LRESULT CALLBACK appWindowPickerProc(HWND button, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR data)
{
HWND dialog = GetParent(button);
auto *view = (AppLanguagesView *)GetWindowLongPtrW(dialog, GWLP_USERDATA);
auto &outline = *reinterpret_cast<HWND *>(data);
if (msg == WM_LBUTTONDOWN)
{
SetFocus(button);
@@ -139,16 +139,16 @@ LRESULT CALLBACK appWindowPickerProc(HWND button, UINT msg, WPARAM wp, LPARAM lp
if (GetCapture() == button)
{
SetCursor(LoadCursorW(nullptr, IDC_CROSS));
SetDlgItemTextW(dialog, 721, L"Drag to the application's window and release. Esc cancels.");
SendMessageW(dialog, AppWindowPicked, 0, -1);
}
return 0;
}
if (msg == WM_CAPTURECHANGED || msg == WM_NCDESTROY)
{
if (view && view->pickerOutline)
if (outline)
{
DestroyWindow(view->pickerOutline);
view->pickerOutline = nullptr;
DestroyWindow(outline);
outline = nullptr;
}
if (msg == WM_CAPTURECHANGED) SendMessageW(dialog, AppWindowPicked, 0, 0);
}
@@ -163,7 +163,7 @@ LRESULT CALLBACK appWindowPickerProc(HWND button, UINT msg, WPARAM wp, LPARAM lp
if (msg == WM_MOUSEMOVE || msg == WM_SETCURSOR)
{
SetCursor(LoadCursorW(nullptr, IDC_CROSS));
if (msg == WM_MOUSEMOVE && view) updatePickerOutline(dialog, *view, appPickerTarget(button, lp));
if (msg == WM_MOUSEMOVE) updatePickerOutline(dialog, outline, appPickerTarget(button, lp));
return TRUE;
}
if (msg == WM_LBUTTONUP)
@@ -177,6 +177,12 @@ LRESULT CALLBACK appWindowPickerProc(HWND button, UINT msg, WPARAM wp, LPARAM lp
if (msg == WM_NCDESTROY) RemoveWindowSubclass(button, appWindowPickerProc, id);
return DefSubclassProc(button, msg, wp, lp);
}
void createAppWindowPicker(HWND w, int id, int x, int y, int width, HWND &outline)
{
control(w, L"BUTTON", L" ⊕ Drag to window", WS_TABSTOP | BS_PUSHBUTTON | BS_LEFT,
id, x, y, width, 28);
SetWindowSubclass(GetDlgItem(w, id), appWindowPickerProc, 1, reinterpret_cast<DWORD_PTR>(&outline));
}
LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
auto *v = (AppLanguagesView *)GetWindowLongPtrW(w, GWLP_USERDATA);
@@ -185,6 +191,11 @@ LRESULT CALLBACK appLanguagesProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
if (msg == AppWindowPicked)
{
if (lp == -1)
{
value(w, 721, L"Drag to the application's window and release. Esc cancels.");
return 0;
}
if (!lp) { showAppLanguageStatus(w, *v); return 0; }
HWND target = (HWND)wp;
auto app = target && target != w && target != v->parent ? applicationName(processName(target)) : L"";
@@ -278,8 +289,7 @@ void showAppLanguages(HWND parent, SettingsView *settings)
SetWindowLongPtrW(w, GWLP_HWNDPARENT, (LONG_PTR)parent);
SetWindowLongPtrW(w, GWLP_USERDATA, (LONG_PTR)v);
label(w, L"Application", 18, 16, 108);
button(w, L"⊕ Drag to window", 728, 132, 10, 164);
SetWindowSubclass(GetDlgItem(w, 728), appWindowPickerProc, 1, 0);
createAppWindowPicker(w, 728, 132, 10, 164, v->pickerOutline);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_VSCROLL | LBS_NOTIFY | LBS_NOINTEGRALHEIGHT | WS_BORDER,
720, 18, 44, 278, 280);
label(w, L"Allowed languages (click to include)", 316, 16, 386);
+877
View File
@@ -0,0 +1,877 @@
std::wstring selectedMouseProfile(HWND w, const SettingsView &view)
{
const auto row = ListView_GetNextItem(GetDlgItem(w, 731), -1, LVNI_SELECTED);
return row < 0 || static_cast<size_t>(row) >= view.mouseProfiles.size() ? GlobalMouseProfile : view.mouseProfiles[row];
}
constexpr int RenameMouseProfile = 757, DeleteMouseProfile = 758;
std::wstring mouseActionLabel(const MouseAction &action)
{
if (action.type == MouseActionType::Shortcut) return action.shortcut.format();
if (action.type == MouseActionType::Launch)
return L"Launch " + std::filesystem::path(action.executable).filename().wstring();
return MouseActions[static_cast<size_t>(action.type)];
}
void selectMouseEvent(HWND w, int event)
{
HWND list = GetDlgItem(w, 733);
ListView_SetItemState(list, event, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
ListView_EnsureVisible(list, event, FALSE);
}
void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
{
const auto profile = selectedMouseProfile(w, view);
EnableWindow(GetDlgItem(w, RenameMouseProfile), isNamedMouseProfile(profile));
EnableWindow(GetDlgItem(w, DeleteMouseProfile), isNamedMouseProfile(profile));
HWND list = GetDlgItem(w, 733);
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
{
auto *action = mouseBinding(view.draft, profile, event);
std::wstring text;
if (profile != GlobalMouseProfile && !action)
{
text += L"Global: ";
action = mouseBinding(view.draft, GlobalMouseProfile, event);
}
text += action ? mouseActionLabel(*action) : MouseActions[0];
ListView_SetItemText(list, event, 1, text.data());
}
selectMouseEvent(w, selection);
}
std::set<std::wstring> profileApplications(const Settings &settings, const std::wstring &profile)
{
const auto scope = settings.mouseProfileApplications.find(profile);
if (scope != settings.mouseProfileApplications.end()) return scope->second;
if (profile != GlobalMouseProfile && !isNamedMouseProfile(profile)) return {profile};
std::set<std::wstring> names;
const auto bindings = settings.mouseBindings.find(profile);
if (bindings != settings.mouseBindings.end())
for (const auto &[event, action] : bindings->second)
names.insert(action.applications.begin(), action.applications.end());
return names;
}
void selectMouseProfile(HWND w, int row)
{
auto list = GetDlgItem(w, 731);
ListView_SetItemState(list, row, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
ListView_EnsureVisible(list, row, FALSE);
}
void appendMouseProfile(HWND w, SettingsView &view, const std::wstring &profile)
{
auto list = GetDlgItem(w, 731);
std::wstring text;
for (const auto &app : profile == GlobalMouseProfile ? std::set<std::wstring>{} : profileApplications(view.draft, profile))
{
if (!text.empty()) text += L", ";
text += app + L".exe";
}
if (text.empty()) text = profile == GlobalMouseProfile ? L"Fallback" : L"All applications";
LVITEMW item{LVIF_TEXT};
item.iItem = static_cast<int>(view.mouseProfiles.size());
wchar_t activeLabel[] = L"";
item.pszText = activeLabel;
const int row = ListView_InsertItem(list, &item);
if (row < 0) throw std::runtime_error("Could not add the profile to the list.");
auto label = mouseProfileName(profile);
ListView_SetItemText(list, row, 1, label.data());
ListView_SetItemText(list, row, 2, text.data());
ListView_SetCheckState(list, row, !view.draft.disabledMouseProfiles.contains(profile));
view.mouseProfiles.push_back(profile);
}
bool blockMouseProfileStateChange(HWND w, SettingsView &view, const NMLISTVIEW &change)
{
const UINT previous = change.uOldState & LVIS_STATEIMAGEMASK;
const UINT next = change.uNewState & LVIS_STATEIMAGEMASK;
if (!(change.uChanged & LVIF_STATE) || !previous || !next || previous == next ||
change.iItem < 0 || static_cast<size_t>(change.iItem) >= view.mouseProfiles.size()) return false;
const auto &profile = view.mouseProfiles[change.iItem];
if (!isNamedMouseProfile(profile)) return true;
if (next == INDEXTOSTATEIMAGEMASK(2))
{
Settings candidate;
candidate.mouseBindings = view.draft.mouseBindings;
candidate.disabledMouseProfiles = view.draft.disabledMouseProfiles;
candidate.mouseProfileApplications = view.draft.mouseProfileApplications;
candidate.disabledMouseProfiles.erase(profile);
try { validateSettings(candidate); }
catch (const std::exception &e) { error(w, e); return true; }
view.draft.disabledMouseProfiles.erase(profile);
}
else view.draft.disabledMouseProfiles.insert(profile);
updateSettingsState(w, &view);
return false;
}
void fillMouseProfiles(HWND w, SettingsView &view)
{
const auto previous = selectedMouseProfile(w, view);
ListView_DeleteAllItems(GetDlgItem(w, 731));
view.mouseProfiles.clear();
appendMouseProfile(w, view, GlobalMouseProfile);
for (const auto &[app, bindings] : view.draft.mouseBindings)
if (app != GlobalMouseProfile) appendMouseProfile(w, view, app);
auto selected = std::find(view.mouseProfiles.begin(), view.mouseProfiles.end(), previous);
selectMouseProfile(w, selected == view.mouseProfiles.end() ? 0 : static_cast<int>(selected - view.mouseProfiles.begin()));
fillMouseEvents(w, view);
}
LRESULT CALLBACK mouseButtonCaptureProc(HWND field, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR)
{
const bool doubleClick = msg == WM_LBUTTONDBLCLK || msg == WM_RBUTTONDBLCLK || msg == WM_MBUTTONDBLCLK;
switch (msg)
{
case WM_LBUTTONDBLCLK: msg = WM_LBUTTONDOWN; break;
case WM_RBUTTONDBLCLK: msg = WM_RBUTTONDOWN; break;
case WM_MBUTTONDBLCLK: msg = WM_MBUTTONDOWN; break;
case WM_XBUTTONDBLCLK: msg = WM_XBUTTONDOWN; break;
}
const int button = mouseButton(msg, HIWORD(wp));
if (button >= 0)
{
if (mouseButtonDown(msg))
{
SetFocus(field);
SetCapture(field);
HWND settings = GetParent(field);
auto *view = reinterpret_cast<SettingsView *>(GetWindowLongPtrW(settings, GWLP_USERDATA));
if (view)
selectMouseEvent(settings, doubleClick && button < 3 ? button + 5 : button);
if (view)
value(settings, 738, std::wstring(L"Window event: ") + MouseButtonNames[button] +
L" | Presses: " + std::to_wstring(++view->mouseEvents));
}
else if (GetCapture() == field)
ReleaseCapture();
return msg == WM_XBUTTONDOWN || msg == WM_XBUTTONUP ? TRUE : 0;
}
if (msg == WM_NCDESTROY) RemoveWindowSubclass(field, mouseButtonCaptureProc, id);
return DefSubclassProc(field, msg, wp, lp);
}
bool registerMouseTestInput(HWND w)
{
RAWINPUTDEVICE mouse{0x01, 0x02, w ? DWORD{0} : RIDEV_REMOVE, w};
return RegisterRawInputDevices(&mouse, 1, sizeof(mouse)) != FALSE;
}
void readMouseTestInput(HWND w, SettingsView &view, HRAWINPUT handle)
{
HWND capture = GetDlgItem(w, 737);
if (!IsWindowVisible(capture)) return;
RAWINPUT input{};
UINT size = sizeof(input);
if (GetRawInputData(handle, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER)) == UINT(-1) ||
input.header.dwType != RIM_TYPEMOUSE || !input.data.mouse.usButtonFlags)
return;
POINT point{};
if (!GetCursorPos(&point) || WindowFromPoint(point) != capture) return;
constexpr USHORT downFlags[] = {RI_MOUSE_LEFT_BUTTON_DOWN, RI_MOUSE_RIGHT_BUTTON_DOWN,
RI_MOUSE_MIDDLE_BUTTON_DOWN, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_5_DOWN};
static_assert(std::size(downFlags) == std::size(MouseButtonNames));
for (size_t i = 0; i < std::size(downFlags); ++i)
{
if (!(input.data.mouse.usButtonFlags & downFlags[i])) continue;
value(w, 739, std::wstring(L"Raw Input: ") + MouseButtonNames[i] +
L" | Presses: " + std::to_wstring(++view.rawMouseEvents));
}
}
constexpr int MouseVolumeStepField = 759;
constexpr int MouseVolumeStepSpin = 760, MouseVolumeStepLabel = 761;
constexpr int MouseActionEditorHeight = 194, CompactMouseActionEditorHeight = 138, MaxEditedMouseVolumeStep = 50;
void layoutMouseActionFields(HWND w, bool volume, bool extraRow)
{
auto &layout = *windowLayoutState(w);
const auto combo = GetDlgItem(w, 734);
const auto buttons = GetDlgItem(w, IDOK);
const int height = extraRow ? MouseActionEditorHeight : CompactMouseActionEditorHeight;
const int offset = height - layout.height;
bool footer = false;
for (auto &[child, rect] : layout.controls)
{
if (child == combo) rect.right = volume ? 368 : 462;
if (child == buttons) footer = true;
if (footer) OffsetRect(&rect, 0, offset);
}
if (offset)
{
RECT frame{};
GetWindowRect(w, &frame);
layout.height = height;
fitFixedClient(w, layout, frame);
}
arrangeWindow(w);
}
void updateMouseActionFields(HWND w)
{
const auto index = SendDlgItemMessageW(w, 734, CB_GETCURSEL, 0, 0);
const auto type = SendDlgItemMessageW(w, 734, CB_GETITEMDATA, index, 0);
const bool shortcut = type == static_cast<int>(MouseActionType::Shortcut);
const bool launch = type == static_cast<int>(MouseActionType::Launch);
const bool volume = isMouseVolumeStep(static_cast<MouseActionType>(type));
ShowWindow(GetDlgItem(w, 747), shortcut ? SW_SHOWNA : SW_HIDE);
ShowWindow(GetDlgItem(w, 742), launch ? SW_SHOWNA : SW_HIDE);
ShowWindow(GetDlgItem(w, 743), launch ? SW_SHOWNA : SW_HIDE);
ShowWindow(GetDlgItem(w, MouseVolumeStepField), volume ? SW_SHOWNA : SW_HIDE);
ShowWindow(GetDlgItem(w, MouseVolumeStepSpin), volume ? SW_SHOWNA : SW_HIDE);
ShowWindow(GetDlgItem(w, MouseVolumeStepLabel), volume ? SW_SHOWNA : SW_HIDE);
ShowWindow(GetDlgItem(w, 744), shortcut || launch ? SW_SHOWNA : SW_HIDE);
value(w, 744, shortcut ? L"Click the field and press the key combination" :
launch ? L"Application (e.g. calc.exe)" : L"");
layoutMouseActionFields(w, volume, shortcut || launch);
}
struct MouseActionEditor
{
HWND parent{};
SettingsView *view{};
std::wstring profile;
int event{};
};
struct MouseProfileEditor
{
std::wstring previous;
std::set<std::wstring> applications;
HWND pickerOutline{};
};
constexpr int ActionApplications = 748, ActionApplicationName = 749, AddActionApp = 750, RemoveActionApp = 751;
constexpr int BrowseActionApp = 752;
constexpr int PickActionApp = 753;
constexpr int AddMouseProfile = 754, MouseProfileName = 755;
void drawActionApplicationIcon(HWND editor, const DRAWITEMSTRUCT &item)
{
RECT bounds = item.rcItem;
const bool pressed = (item.itemState & ODS_SELECTED) != 0;
const bool disabled = (item.itemState & ODS_DISABLED) != 0;
DrawFrameControl(item.hDC, &bounds, DFC_BUTTON,
DFCS_BUTTONPUSH | (pressed ? DFCS_PUSHED : 0) | (disabled ? DFCS_INACTIVE : 0));
const int offset = pressed ? scale(editor, 1) : 0;
const int x = (bounds.left + bounds.right) / 2 + offset;
const int y = (bounds.top + bounds.bottom) / 2 + offset;
const int half = scale(editor, 6);
const int weight = std::max(1, scale(editor, 2));
HBRUSH ink = GetSysColorBrush(disabled ? COLOR_GRAYTEXT : COLOR_BTNTEXT);
if (item.CtlID == BrowseActionApp)
{
RECT folder{x - half - 1, y - half + weight, x + half + 1, y + half};
RECT tab{folder.left, folder.top - weight, x, folder.top};
FrameRect(item.hDC, &folder, ink);
FillRect(item.hDC, &tab, ink);
}
else if (item.CtlID == PickActionApp)
{
RECT target{x - half, y - half, x + half + 1, y + half + 1};
RECT horizontal{x - half - weight, y, x + half + weight + 1, y + 1};
RECT vertical{x, y - half - weight, x + 1, y + half + weight + 1};
const auto previousBrush = SelectObject(item.hDC, GetStockObject(HOLLOW_BRUSH));
SetDCPenColor(item.hDC, GetSysColor(disabled ? COLOR_GRAYTEXT : COLOR_BTNTEXT));
const auto previousPen = SelectObject(item.hDC, GetStockObject(DC_PEN));
Ellipse(item.hDC, target.left, target.top, target.right, target.bottom);
SelectObject(item.hDC, previousPen);
SelectObject(item.hDC, previousBrush);
FillRect(item.hDC, &horizontal, ink);
FillRect(item.hDC, &vertical, ink);
}
else if (item.CtlID == RenameMouseProfile)
{
POINT pencil[]{{x - half, y + half}, {x - half + weight, y + 1},
{x + half - weight, y - half}, {x + half, y - half + weight},
{x - 1, y + half - weight}};
const auto brush = SelectObject(item.hDC, ink);
const auto pen = SelectObject(item.hDC, GetStockObject(NULL_PEN));
Polygon(item.hDC, pencil, static_cast<int>(std::size(pencil)));
SelectObject(item.hDC, pen);
SelectObject(item.hDC, brush);
}
else if (item.CtlID == DeleteMouseProfile)
{
RECT bin{x - half + 2, y - half + weight, x + half - 1, y + half};
RECT lid{x - half, y - half, x + half + 1, y - half + 1};
FrameRect(item.hDC, &bin, ink);
FillRect(item.hDC, &lid, ink);
}
else
{
RECT horizontal{x - half, y - weight / 2, x + half, y - weight / 2 + weight};
FillRect(item.hDC, &horizontal, ink);
}
if (item.CtlID == AddActionApp || item.CtlID == AddMouseProfile)
{
RECT vertical{x - weight / 2, y - half, x - weight / 2 + weight, y + half};
FillRect(item.hDC, &vertical, ink);
}
if ((item.itemState & ODS_FOCUS) && !(item.itemState & ODS_NOFOCUSRECT))
{
InflateRect(&bounds, -scale(editor, 4), -scale(editor, 4));
DrawFocusRect(item.hDC, &bounds);
}
}
void addActionApplicationTooltip(HWND tips, HWND editor, int id, const wchar_t *text)
{
TOOLINFOW tool{sizeof(tool), TTF_IDISHWND | TTF_SUBCLASS, editor,
reinterpret_cast<UINT_PTR>(GetDlgItem(editor, id))};
tool.lpszText = const_cast<wchar_t *>(text);
SendMessageW(tips, TTM_ADDTOOLW, 0, reinterpret_cast<LPARAM>(&tool));
}
void fillActionApplications(HWND w, const MouseProfileEditor &editor)
{
SendDlgItemMessageW(w, ActionApplications, LB_RESETCONTENT, 0, 0);
for (const auto &name : editor.applications)
{
const auto label = name + L".exe";
SendDlgItemMessageW(w, ActionApplications, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(label.c_str()));
}
}
void addActionApplication(HWND w, MouseProfileEditor &editor)
{
const auto name = applicationName(trimApplicationText(value(w, ActionApplicationName)));
if (!validMouseApplication(name)) throw std::runtime_error("Enter a valid application executable name.");
if (editor.applications.size() >= 1000 && !editor.applications.contains(name))
throw std::runtime_error("Too many applications in this profile.");
editor.applications.insert(name);
fillActionApplications(w, editor);
value(w, ActionApplicationName, L"");
}
std::wstring actionApplicationExecutable(std::filesystem::path path)
{
if (lower(path.extension().wstring()) == L".lnk")
{
Microsoft::WRL::ComPtr<IShellLinkW> link;
Microsoft::WRL::ComPtr<IPersistFile> file;
wchar_t target[32768]{};
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&link))) ||
FAILED(link.As(&file)) || FAILED(file->Load(path.c_str(), STGM_READ)) ||
link->GetPath(target, static_cast<int>(std::size(target)), nullptr, 0) != S_OK)
throw std::runtime_error("Could not read the shortcut target.");
path = target;
}
if (lower(path.extension().wstring()) != L".exe")
throw std::runtime_error("Choose an executable or a shortcut that points to one.");
return path.filename().wstring();
}
void browseActionApplication(HWND w, MouseProfileEditor &editor)
{
wchar_t path[32768]{};
OPENFILENAMEW picker{sizeof(picker)};
picker.hwndOwner = w;
picker.lpstrFilter = L"Applications and shortcuts (*.exe;*.lnk)\0*.exe;*.lnk\0";
picker.lpstrFile = path;
picker.nMaxFile = static_cast<DWORD>(std::size(path));
picker.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_NODEREFERENCELINKS;
if (!GetOpenFileNameW(&picker)) return;
value(w, ActionApplicationName, actionApplicationExecutable(path));
addActionApplication(w, editor);
}
void saveMouseAction(HWND w, MouseActionEditor &editor)
{
const auto choice = SendDlgItemMessageW(w, 734, CB_GETCURSEL, 0, 0);
if (choice == CB_ERR) throw std::runtime_error("Choose an action.");
const auto type = SendDlgItemMessageW(w, 734, CB_GETITEMDATA, choice, 0);
const auto &profile = editor.profile;
const auto *event = MouseEvents[editor.event];
Settings check;
check.mouseBindings = editor.view->draft.mouseBindings;
check.disabledMouseProfiles = editor.view->draft.disabledMouseProfiles;
check.mouseProfileApplications = editor.view->draft.mouseProfileApplications;
if (type < 0 || (type == static_cast<int>(MouseActionType::Standard) && profile == GlobalMouseProfile))
{
if (auto found = check.mouseBindings.find(profile); found != check.mouseBindings.end())
{
found->second.erase(event);
if (found->second.empty() && !isNamedMouseProfile(profile)) check.mouseBindings.erase(found);
}
}
else
{
MouseAction action{static_cast<MouseActionType>(type)};
if (action.type == MouseActionType::Shortcut)
{
auto shortcut = Hotkey::parse(value(w, 747));
if (!shortcut || !shortcut->key) throw std::runtime_error("Click the field and press a key combination.");
action.shortcut = *shortcut;
}
if (action.type == MouseActionType::Launch) action.executable = trimApplicationText(value(w, 742));
if (isMouseVolumeStep(action.type))
{
const auto text = value(w, MouseVolumeStepField);
if (text.empty() || text.size() > 3 || text.find_first_not_of(L"0123456789") != text.npos)
throw std::runtime_error("Enter a volume step from 1 to 50.");
action.volumeStep = std::stoi(text);
const auto *previous = mouseBinding(editor.view->draft, profile, editor.event);
const bool unchanged = previous && previous->type == action.type && previous->volumeStep == action.volumeStep;
if (action.volumeStep < 1 || (action.volumeStep > MaxEditedMouseVolumeStep && !unchanged))
throw std::runtime_error("Enter a volume step from 1 to 50.");
}
if (const auto *previous = mouseBinding(editor.view->draft, profile, editor.event))
action.applications = previous->applications;
check.mouseBindings[profile][event] = std::move(action);
}
validateSettings(check);
editor.view->draft.mouseBindings = std::move(check.mouseBindings);
fillMouseEvents(editor.parent, *editor.view, editor.event);
}
LRESULT CALLBACK mouseActionEditorProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
auto *editor = reinterpret_cast<MouseActionEditor *>(GetWindowLongPtrW(w, GWLP_USERDATA));
if (!editor) return DefWindowProcW(w, msg, wp, lp);
try
{
if (msg == WM_COMMAND)
{
switch (LOWORD(wp))
{
case 734:
if (HIWORD(wp) == CBN_SELCHANGE)
updateMouseActionFields(w);
return 0;
case 743:
{
wchar_t path[32768]{};
OPENFILENAMEW picker{sizeof(picker)};
picker.hwndOwner = w;
picker.lpstrFilter = L"Applications (*.exe)\0*.exe\0";
picker.lpstrFile = path;
picker.nMaxFile = static_cast<DWORD>(std::size(path));
picker.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
if (GetOpenFileNameW(&picker)) value(w, 742, path);
return 0;
}
case IDOK:
saveMouseAction(w, *editor);
DestroyWindow(w);
return 0;
case IDCANCEL:
DestroyWindow(w);
return 0;
}
}
if (msg == WM_CLOSE) { DestroyWindow(w); return 0; }
if (msg == WM_NCDESTROY)
{
auto parent = editor->parent;
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
disposeFont(w);
delete editor;
if (IsWindow(parent))
{
EnableWindow(parent, TRUE);
SetForegroundWindow(parent);
SetFocus(GetDlgItem(parent, 733));
}
}
}
catch (const std::exception &e) { error(w, e); }
return DefWindowProcW(w, msg, wp, lp);
}
void showMouseActionEditor(HWND parent, SettingsView &view, int event)
{
if (event < 0 || event >= static_cast<int>(std::size(MouseEvents)) || !IsWindowEnabled(parent)) return;
const auto profile = selectedMouseProfile(parent, view);
const bool application = profile != GlobalMouseProfile;
const auto *action = mouseBinding(view.draft, profile, event);
const auto title = std::wstring(L"Edit mouse action");
HWND w = makeWindow(*view.app, L"KSwitcher.MouseAction", title.c_str(), mouseActionEditorProc,
480, MouseActionEditorHeight, 0, false, parent);
auto *editor = new MouseActionEditor{parent, &view, profile, event};
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(editor));
label(w, std::wstring(MouseEvents[event]).append(L" · ").append(mouseProfileName(profile)).c_str(),
18, 14, 444);
label(w, L"&Action", 18, 43, 330);
windowLayoutState(w)->fixedClient = true;
windowLayoutState(w)->rowCombo =
control(w, L"COMBOBOX", L"", WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST, 734, 18, 65, 444, 215);
if (application)
{
SendDlgItemMessageW(w, 734, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(L"Inherit from Global"));
SendDlgItemMessageW(w, 734, CB_SETITEMDATA, 0, -1);
}
for (size_t type = 0; type < std::size(MouseActions); ++type)
{
const auto row = SendDlgItemMessageW(w, 734, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(MouseActions[type]));
if (row >= 0) SendDlgItemMessageW(w, 734, CB_SETITEMDATA, row, type);
}
control(w, L"STATIC", L"", 0, 744, 18, 101, 444, 21);
hotkeyEdit(*view.app, w, 747, 18, 125, 444);
edit(w, 742, 18, 125, 334);
button(w, L"&Browse…", 743, 362, 124, 100);
control(w, L"STATIC", L"Step (%)", 0, MouseVolumeStepLabel, 380, 43, 82, 22);
auto stepField = edit(w, MouseVolumeStepField, 380, 65, 64, 25, ES_NUMBER);
SendMessageW(stepField, EM_SETLIMITTEXT, 3, 0);
INITCOMMONCONTROLSEX spinControls{sizeof(spinControls), ICC_UPDOWN_CLASS};
InitCommonControlsEx(&spinControls);
HWND spin = control(w, UPDOWN_CLASSW, L"", UDS_SETBUDDYINT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,
MouseVolumeStepSpin, 444, 65, 18, 25);
SendMessageW(spin, UDM_SETBUDDY, reinterpret_cast<WPARAM>(stepField), 0);
SendMessageW(spin, UDM_SETRANGE32, 1, MaxEditedMouseVolumeStep);
SetDlgItemInt(w, MouseVolumeStepField,
action && isMouseVolumeStep(action->type) ? action->volumeStep : DefaultMouseVolumeStep, FALSE);
button(w, L"&Apply", IDOK, 248, 154, 100);
button(w, L"Cancel", IDCANCEL, 362, 154, 100);
SendDlgItemMessageW(w, 734, CB_SETCURSEL, action ? static_cast<int>(action->type) + application : 0, 0);
if (action && action->type == MouseActionType::Shortcut) value(w, 747, action->shortcut.format());
if (action && action->type == MouseActionType::Launch) value(w, 742, action->executable);
updateMouseActionFields(w);
EnableWindow(parent, FALSE);
showRequestedWindow(w);
SetFocus(GetDlgItem(w, 734));
}
constexpr int EditMouseEvent = 735, ResetMouseEvent = 746;
constexpr auto MouseEventHint = L"Double-click to edit. Right-click for more actions.";
int mouseProfileRowAt(HWND list, POINT point)
{
LVHITTESTINFO hit{};
hit.pt = point;
const int item = ListView_SubItemHitTest(list, &hit);
if (item >= 0) return item;
RECT client{};
if (!GetClientRect(list, &client) || !PtInRect(&client, point)) return -1;
const int top = ListView_GetTopIndex(list);
const int end = std::min(ListView_GetItemCount(list), top + ListView_GetCountPerPage(list) + 1);
for (int row = top; row < end; ++row)
{
RECT bounds{};
if (ListView_GetItemRect(list, row, &bounds, LVIR_BOUNDS) && point.y >= bounds.top && point.y < bounds.bottom)
return row;
}
return -1;
}
void showMouseListMenu(HWND list, POINT point, bool profiles)
{
HWND w = GetParent(list);
int event;
if (point.x == -1 && point.y == -1)
{
event = ListView_GetNextItem(list, -1, LVNI_SELECTED);
RECT row{};
if (event < 0 || !ListView_GetItemRect(list, event, &row, LVIR_BOUNDS)) return;
point = {row.left, row.bottom};
ClientToScreen(list, &point);
}
else
{
LVHITTESTINFO hit{};
hit.pt = point;
ScreenToClient(list, &hit.pt);
event = ListView_SubItemHitTest(list, &hit);
if (event < 0) return;
}
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
HMENU menu = CreatePopupMenu();
if (!menu) return;
if (profiles)
{
selectMouseProfile(w, event);
const UINT flags = MF_STRING | (isNamedMouseProfile(selectedMouseProfile(w, view)) ? MF_ENABLED : MF_GRAYED);
AppendMenuW(menu, flags, RenameMouseProfile, L"&Edit…");
AppendMenuW(menu, flags, DeleteMouseProfile, L"&Delete…");
}
else
{
selectMouseEvent(w, event);
const bool canReset = mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
AppendMenuW(menu, MF_STRING, EditMouseEvent, L"&Edit…");
AppendMenuW(menu, MF_STRING | (canReset ? MF_ENABLED : MF_GRAYED), ResetMouseEvent, L"&Reset to default");
}
const auto command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON,
point.x, point.y, 0, w, nullptr);
DestroyMenu(menu);
if (command) SendMessageW(w, WM_COMMAND, command, 0);
}
LRESULT CALLBACK mouseEventListProc(HWND list, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR)
{
const bool profiles = GetDlgCtrlID(list) == 731;
if (profiles && msg == WM_LBUTTONDBLCLK)
{
const int row = mouseProfileRowAt(list, {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)});
if (row >= 0)
{
const auto owner = GetParent(list);
selectMouseProfile(owner, row);
SendMessageW(owner, WM_COMMAND, RenameMouseProfile, 0);
}
return 0;
}
if (msg == WM_GETDLGCODE && wp == VK_RETURN) return DLGC_WANTMESSAGE;
if (msg == WM_KEYDOWN && (wp == VK_RETURN || wp == VK_F2))
{
SendMessageW(GetParent(list), WM_COMMAND, profiles ? RenameMouseProfile : EditMouseEvent, 0);
return 0;
}
if (msg == WM_CONTEXTMENU)
{
showMouseListMenu(list, {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}, profiles);
return 0;
}
if (msg == WM_NCDESTROY) RemoveWindowSubclass(list, mouseEventListProc, id);
return DefSubclassProc(list, msg, wp, lp);
}
LRESULT CALLBACK mouseProfileNameProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
const HWND owner = GetWindow(w, GW_OWNER);
auto *editor = reinterpret_cast<MouseProfileEditor *>(GetWindowLongPtrW(w, GWLP_USERDATA));
if (!editor) return DefWindowProcW(w, msg, wp, lp);
try
{
if (msg == AppWindowPicked)
{
if (lp == 1)
{
const auto name = mouseApplicationName(reinterpret_cast<HWND>(wp));
if (name.empty()) throw std::runtime_error("Drop the crosshair onto an application window.");
value(w, ActionApplicationName, name);
addActionApplication(w, *editor);
}
return 0;
}
if (msg == WM_DRAWITEM && (wp == AddActionApp || wp == RemoveActionApp || wp == BrowseActionApp || wp == PickActionApp))
{
drawActionApplicationIcon(w, *reinterpret_cast<const DRAWITEMSTRUCT *>(lp));
return TRUE;
}
if (msg == WM_COMMAND)
{
if (LOWORD(wp) == BrowseActionApp) { browseActionApplication(w, *editor); return 0; }
if (LOWORD(wp) == AddActionApp) { addActionApplication(w, *editor); return 0; }
if (LOWORD(wp) == RemoveActionApp)
{
const auto row = SendDlgItemMessageW(w, ActionApplications, LB_GETCURSEL, 0, 0);
if (row >= 0 && static_cast<size_t>(row) < editor->applications.size())
{
editor->applications.erase(std::next(editor->applications.begin(), row));
fillActionApplications(w, *editor);
}
return 0;
}
}
}
catch (const std::exception &e) { error(w, e); return 0; }
if (msg == WM_COMMAND && LOWORD(wp) == IDOK)
{
try
{
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(owner, GWLP_USERDATA));
const auto &previous = editor->previous;
const auto name = trimApplicationText(value(w, MouseProfileName));
const auto duplicate = std::any_of(view.mouseProfiles.begin(), view.mouseProfiles.end(), [&](const auto &profile)
{
return profile != previous && lower(mouseProfileName(profile)) == lower(name);
});
if (duplicate) throw std::runtime_error("A profile with that name already exists.");
const auto key = std::wstring(NamedMouseProfilePrefix) + name;
Settings check;
check.mouseBindings = view.draft.mouseBindings;
check.disabledMouseProfiles = view.draft.disabledMouseProfiles;
check.mouseProfileApplications = view.draft.mouseProfileApplications;
if (!previous.empty() && key != previous)
{
auto profile = check.mouseBindings.extract(previous);
if (profile.empty()) throw std::runtime_error("The profile no longer exists.");
profile.key() = key;
check.mouseBindings.insert(std::move(profile));
if (check.disabledMouseProfiles.erase(previous)) check.disabledMouseProfiles.insert(key);
}
else check.mouseBindings.try_emplace(key);
check.mouseProfileApplications.erase(previous);
check.mouseProfileApplications[key] = editor->applications;
for (auto &[event, action] : check.mouseBindings.at(key)) action.applications.clear();
validateSettings(check);
view.draft.mouseBindings = std::move(check.mouseBindings);
view.draft.disabledMouseProfiles = std::move(check.disabledMouseProfiles);
view.draft.mouseProfileApplications = std::move(check.mouseProfileApplications);
fillMouseProfiles(owner, view);
const auto selected = std::find(view.mouseProfiles.begin(), view.mouseProfiles.end(), key);
selectMouseProfile(owner, static_cast<int>(selected - view.mouseProfiles.begin()));
fillMouseEvents(owner, view);
updateSettingsState(owner, &view);
DestroyWindow(w);
}
catch (const std::exception &e) { error(w, e); }
return 0;
}
if (msg == WM_CLOSE || (msg == WM_COMMAND && LOWORD(wp) == IDCANCEL))
{
DestroyWindow(w);
return 0;
}
if (msg == WM_NCDESTROY)
{
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
delete editor;
disposeFont(w);
EnableWindow(owner, TRUE);
SetForegroundWindow(owner);
}
return DefWindowProcW(w, msg, wp, lp);
}
void showMouseProfileName(HWND owner, SettingsView &view, bool renaming)
{
HWND w = makeWindow(*view.app, L"KSwitcher.MouseProfileName", renaming ? L"Edit profile" : L"Add profile", mouseProfileNameProc,
480, 296, 0, false, owner);
windowLayoutState(w)->fixedClient = true;
const auto previous = renaming ? selectedMouseProfile(owner, view) : std::wstring{};
auto *editor = new MouseProfileEditor{previous, renaming ? profileApplications(view.draft, previous) : std::set<std::wstring>{}};
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(editor));
label(w, L"&Name", 18, 12, 444);
edit(w, MouseProfileName, 18, 36, 444);
if (renaming) value(w, MouseProfileName, mouseProfileName(previous));
label(w, L"Applications (empty list means all applications)", 18, 75, 444);
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_BORDER | WS_VSCROLL | LBS_NOINTEGRALHEIGHT,
ActionApplications, 18, 100, 404, 96);
control(w, L"BUTTON", L"&Remove application", WS_TABSTOP | BS_OWNERDRAW, RemoveActionApp, 430, 100, 32, 28);
HWND appName = edit(w, ActionApplicationName, 18, 207, 324);
SendMessageW(appName, EM_SETCUEBANNER, FALSE, reinterpret_cast<LPARAM>(L"Application executable (e.g. chrome.exe)"));
HWND picker = control(w, L"BUTTON", L"Drag to application window", WS_TABSTOP | BS_OWNERDRAW,
PickActionApp, 350, 205, 32, 28);
SetWindowSubclass(picker, appWindowPickerProc, 1, reinterpret_cast<DWORD_PTR>(&editor->pickerOutline));
control(w, L"BUTTON", L"&Browse for application", WS_TABSTOP | BS_OWNERDRAW, BrowseActionApp, 390, 205, 32, 28);
control(w, L"BUTTON", L"Add a&pplication", WS_TABSTOP | BS_OWNERDRAW, AddActionApp, 430, 205, 32, 28);
HWND tips = CreateWindowExW(WS_EX_TOPMOST, TOOLTIPS_CLASSW, nullptr, WS_POPUP | TTS_ALWAYSTIP | TTS_NOPREFIX,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, w, nullptr,
view.app->instance, nullptr);
addActionApplicationTooltip(tips, w, RemoveActionApp, L"Remove application");
addActionApplicationTooltip(tips, w, AddActionApp, L"Add application");
addActionApplicationTooltip(tips, w, BrowseActionApp, L"Browse for an executable or shortcut");
addActionApplicationTooltip(tips, w, PickActionApp, L"Drag to an application window");
fillActionApplications(w, *editor);
control(w, L"BUTTON", renaming ? L"&Apply" : L"&Add", WS_TABSTOP | BS_DEFPUSHBUTTON, IDOK, 248, 251, 100, 28);
button(w, L"Cancel", IDCANCEL, 362, 251, 100);
EnableWindow(owner, FALSE);
showRequestedWindow(w);
SetFocus(GetDlgItem(w, MouseProfileName));
}
bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
{
const int id = LOWORD(wp);
if (id == AddMouseProfile) { showMouseProfileName(w, view, false); return true; }
if (id == RenameMouseProfile || id == DeleteMouseProfile)
{
const auto profile = selectedMouseProfile(w, view);
if (!isNamedMouseProfile(profile)) return true;
if (id == RenameMouseProfile) showMouseProfileName(w, view, true);
else if (MessageBoxW(w, (L"Delete " + mouseProfileName(profile) + L" and all its actions?").c_str(),
L"Delete profile", MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2) == IDYES)
{
view.draft.disabledMouseProfiles.erase(profile);
view.draft.mouseBindings.erase(profile);
view.draft.mouseProfileApplications.erase(profile);
fillMouseProfiles(w, view);
}
return true;
}
if (id != EditMouseEvent && id != ResetMouseEvent) return false;
const int event = ListView_GetNextItem(GetDlgItem(w, 733), -1, LVNI_SELECTED);
if (event < 0) return true;
if (id == EditMouseEvent) showMouseActionEditor(w, view, event);
else if (auto profile = view.draft.mouseBindings.find(selectedMouseProfile(w, view));
profile != view.draft.mouseBindings.end())
{
profile->second.erase(MouseEvents[event]);
if (profile->second.empty() && !isNamedMouseProfile(profile->first)) view.draft.mouseBindings.erase(profile);
fillMouseEvents(w, view, event);
}
return true;
}
void scaleMouseTableColumns(HWND w, int previousDpi, int nextDpi)
{
for (int id : {731, 733})
{
const auto list = GetDlgItem(w, id);
const int count = Header_GetItemCount(ListView_GetHeader(list));
for (int column = 0; column < count; ++column)
ListView_SetColumnWidth(list, column, MulDiv(ListView_GetColumnWidth(list, column), nextDpi, previousDpi));
}
}
void createMousePage(HWND w, SettingsView &view)
{
constexpr int profilesLeft = 18, actionsLeft = 369, tableWidth = 333;
constexpr int profilesRight = profilesLeft + tableWidth;
label(w, L"Profiles", 18, 64, 120);
label(w, L"Mouse events", actionsLeft, 64, tableWidth);
control(w, L"BUTTON", L"Delete profile", WS_TABSTOP | BS_OWNERDRAW, DeleteMouseProfile, profilesRight - 80, 61, 24, 24);
control(w, L"BUTTON", L"Edit profile", WS_TABSTOP | BS_OWNERDRAW, RenameMouseProfile, profilesRight - 52, 61, 24, 24);
control(w, L"BUTTON", L"Add profile", WS_TABSTOP | BS_OWNERDRAW, AddMouseProfile, profilesRight - 24, 61, 24, 24);
HWND tips = CreateWindowExW(WS_EX_TOPMOST, TOOLTIPS_CLASSW, nullptr, WS_POPUP | TTS_ALWAYSTIP | TTS_NOPREFIX,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, w, nullptr,
view.app->instance, nullptr);
addActionApplicationTooltip(tips, w, AddMouseProfile, L"Add profile");
addActionApplicationTooltip(tips, w, RenameMouseProfile, L"Edit profile");
addActionApplicationTooltip(tips, w, DeleteMouseProfile, L"Delete profile");
constexpr int listTop = 90, listHeight = 224;
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES};
InitCommonControlsEx(&controls);
constexpr DWORD listStyle = WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER;
HWND profiles = control(w, WC_LISTVIEWW, L"", listStyle, 731, profilesLeft, listTop, tableWidth, listHeight);
SetWindowSubclass(profiles, mouseEventListProc, 3, 0);
ListView_SetExtendedListViewStyle(profiles, LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER | LVS_EX_LABELTIP | LVS_EX_CHECKBOXES);
wchar_t nameTitle[] = L"Name", activeTitle[] = L"Active", applicationsTitle[] = L"Applications";
LVCOLUMNW profileColumn{LVCF_TEXT | LVCF_WIDTH};
profileColumn.pszText = activeTitle;
profileColumn.cx = scale(w, 56);
ListView_InsertColumn(profiles, 0, &profileColumn);
profileColumn.pszText = nameTitle;
profileColumn.cx = scale(w, 100);
ListView_InsertColumn(profiles, 1, &profileColumn);
profileColumn.pszText = applicationsTitle;
profileColumn.cx = scale(w, tableWidth - 160);
ListView_InsertColumn(profiles, 2, &profileColumn);
int order[]{1, 0, 2};
ListView_SetColumnOrderArray(profiles, 3, order);
HWND list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT |
LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER, 733, actionsLeft, listTop, tableWidth, listHeight);
SetWindowSubclass(list, mouseEventListProc, 3, 0);
ListView_SetExtendedListViewStyle(list, LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER | LVS_EX_LABELTIP);
wchar_t actionTitle[] = L"Action";
LVCOLUMNW column{LVCF_TEXT | LVCF_WIDTH};
column.pszText = nameTitle;
column.cx = scale(w, 116);
ListView_InsertColumn(list, 0, &column);
column.pszText = actionTitle;
column.cx = scale(w, tableWidth - 120);
ListView_InsertColumn(list, 1, &column);
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
{
LVITEMW item{LVIF_TEXT};
item.iItem = event;
item.pszText = const_cast<wchar_t *>(MouseEvents[event]);
ListView_InsertItem(list, &item);
}
control(w, L"STATIC", MouseEventHint, 0, 740, actionsLeft, 324, tableWidth, 22);
control(w, L"BUTTON", L"Test mouse buttons here", WS_TABSTOP | BS_PUSHBUTTON,
737, profilesLeft, listTop + listHeight + 10, tableWidth, 56);
SetWindowSubclass(GetDlgItem(w, 737), mouseButtonCaptureProc, 1, 0);
control(w, L"STATIC", L"Window event: waiting for a press", 0, 738, actionsLeft, 370, tableWidth, 20);
view.rawMouseRegistered = registerMouseTestInput(w);
control(w, L"STATIC", view.rawMouseRegistered ? L"Raw Input: waiting for a press" : L"Raw Input: unavailable",
0, 739, actionsLeft, 394, tableWidth, 20);
}
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.63";
inline constexpr wchar_t AppVersion[] = L"0.6.64";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;