Release KSwitcher v0.6.65

This commit is contained in:
agrynco committed 2026-11-12 01:18:21 +02:00
1 parent 14f33012a3
commit 8a65e7f7fb
21 files changed
+570 -111

No files matched your search

+56 -23
View File
@@ -733,6 +733,10 @@ Bytes App::lanCommand(const Bytes &command)
return reply;
}
void App::applySettings(const Settings &requested)
{
applySettings(requested, true);
}
void App::applySettings(const Settings &requested, bool validateOcrInstallation)
{
Settings s = requested;
std::set<std::wstring> present, removed(s.snippetTombstones.begin(), s.snippetTombstones.end());
@@ -745,7 +749,7 @@ void App::applySettings(const Settings &requested)
bool autostartChanged = false;
AutoCorrector predictor;
validateSettings(s);
validateTesseract(s);
if (validateOcrInstallation && ocrProfile(s) != ocrProfile(settings)) validateTesseract(s);
if (s.autoCorrect)
{
predictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", s.autoLanguages);
@@ -849,6 +853,8 @@ void App::resetLanInput()
chatSwitchHook = {};
pendingMouseClicks = {};
mouseActions.clear();
mouseWheelTarget = nullptr;
mouseWheelRemainder = 0;
resetInput();
}
void App::resetInput()
@@ -1399,6 +1405,8 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
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 auto wheelDelta = static_cast<SHORT>(HIWORD(mouse.mouseData));
const int wheel = mouse.flags & LLMHF_INJECTED ? -1 : mouseWheelEvent(UINT(wp), wheelDelta);
const bool down = mouseButtonDown(UINT(wp));
if (button >= 0 && app.mousePresses[button].held)
{
@@ -1430,7 +1438,8 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
if (app.lanService->takeInputChange()) app.resetLanInput();
if (consumed) return 1;
}
if (button >= 0 && down && !app.settings.mouseBindings.empty() && !app.busy && !app.suspended &&
if (((button >= 0 && down) || mouseEventEnabled(app.settings, wheel)) &&
!app.settings.mouseBindings.empty() && !app.busy && !app.suspended &&
!app.installingUpdate && !app.hotkeyCapture && modifiersReleased())
{
const auto hit = WindowFromPoint(mouse.pt);
@@ -1442,22 +1451,26 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
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 &&
const int event = wheel >= 0 ? wheel : button;
const auto *global = mouseBinding(app.settings, GlobalMouseProfile, event);
const bool globalLaunch = event >= 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;
wchar_t targetClass[64]{};
if (ownWindow) GetClassNameW(target, targetClass, std::size(targetClass));
const bool testButton = ownWindow && (hit == GetDlgItem(target, 737) ||
wcscmp(targetClass, L"KSwitcher.AddMouseEvent") == 0);
const auto *fallback = restricted ? effectiveMouseAction(app.settings, L"", event, false) : nullptr;
const auto *restrictedAction = isMouseVolumeAction(fallback) ? fallback : globalLaunch ? global : nullptr;
const bool eligible = target && !testButton && (!restricted || (button >= 2 && restrictedAction)) &&
(button >= 2 || target == GetForegroundWindow());
const bool eligible = target && !testButton && (!restricted || (event >= 2 && restrictedAction)) &&
(event >= 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, event, foreground, &singleScoped);
auto *doubleClick = wheel < 0 && !restricted && button < 3 ?
effectiveMouseAction(app.settings, name, 5 + button, foreground, &doubleScoped) : nullptr;
if (!foreground)
{
@@ -1465,25 +1478,45 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
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 (wheel >= 0 && single)
{
if (second ? doubleScoped : singleScoped) app.mousePresses[button].action.applications.insert(name);
if (isMouseVolumeStep(action->type))
app.beginMouseVolume(button);
if (app.mouseWheelTarget != target || app.mouseWheelSignal != wheel)
app.mouseWheelRemainder = 0;
app.mouseWheelTarget = target;
app.mouseWheelSignal = wheel;
const int steps = mouseWheelSteps(app.mouseWheelRemainder, wheelDelta);
for (int i = 0; i < steps; ++i)
app.queueMouseAction({target, mouse.pt, now, *single});
app.resetInput();
return 1;
}
if (wheel < 0)
{
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;
}
}
}
}
if (wheel >= 0)
{
app.mouseWheelTarget = nullptr;
app.mouseWheelRemainder = 0;
}
// Remote clicks invalidate a typing range just like local clicks.
if (down || wp == WM_MOUSEWHEEL || wp == WM_MOUSEHWHEEL)
{
+3
View File
@@ -106,6 +106,7 @@ class App
private:
friend struct InputRegressionAccess;
void releaseIdleMemory(ULONGLONG now);
void applySettings(const Settings &, bool validateOcrInstallation);
HHOOK keyboardHook{}, mouseHook{};
struct MousePress
{
@@ -118,6 +119,8 @@ class App
std::array<MousePress, std::size(MouseButtonNames)> mousePresses{};
std::array<MousePress, std::size(MouseButtonNames)> pendingMouseClicks{};
std::deque<MousePress> mouseActions;
HWND mouseWheelTarget{};
int mouseWheelSignal{-1}, mouseWheelRemainder{};
void queueMouseAction(MousePress);
void flushMouseClicks();
void beginMouseVolume(int button);
+2 -1
View File
@@ -79,7 +79,8 @@ void App::pollSync()
if (encodeSettings(next) != snapshot)
{
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
applySettings(next);
// A restored OCR configuration can precede the optional local installation.
applySettings(next, false);
syncStatus += L" Changes applied on this PC; local backup saved.";
}
else syncStatus += result.conflicts.empty() ? L" This PC's settings are already up to date; no local changes needed." :
+1
View File
@@ -101,6 +101,7 @@ struct Settings
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::map<std::wstring, std::wstring> customMouseEvents;
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"};
+49 -1
View File
@@ -6,7 +6,54 @@ 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"};
L"Double left", L"Double right", L"Double middle",
L"Wheel up", L"Wheel down", L"Wheel left", L"Wheel right"};
inline constexpr int BuiltInMouseEventCount = 8;
inline int mouseWheelEvent(UINT message, SHORT delta)
{
if (!delta) return -1;
if (message == WM_MOUSEWHEEL) return BuiltInMouseEventCount + (delta < 0);
if (message == WM_MOUSEHWHEEL) return BuiltInMouseEventCount + 2 + (delta > 0);
return -1;
}
inline int rawMouseSignal(const RAWMOUSE &mouse)
{
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};
int signal = -1;
for (int button = 0; button < static_cast<int>(std::size(downFlags)); ++button)
{
if (!(mouse.usButtonFlags & downFlags[button])) continue;
if (signal >= 0) return -1;
signal = button;
}
const auto wheel = mouse.usButtonFlags & (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL);
if (!wheel) return signal;
if (signal >= 0 || wheel == (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL)) return -1;
return mouseWheelEvent(wheel == RI_MOUSE_WHEEL ? WM_MOUSEWHEEL : WM_MOUSEHWHEEL,
static_cast<SHORT>(mouse.usButtonData));
}
inline int mouseWheelSteps(int &remainder, SHORT delta)
{
const int total = remainder + std::abs(static_cast<int>(delta));
remainder = total % WHEEL_DELTA;
return total / WHEEL_DELTA;
}
inline bool mouseEventEnabled(const Settings &settings, int event)
{
return event >= 0 && event < static_cast<int>(std::size(MouseEvents)) &&
(event < BuiltInMouseEventCount || settings.customMouseEvents.contains(MouseEvents[event]));
}
inline std::wstring mouseEventName(const Settings &settings, int event)
{
auto name = settings.customMouseEvents.find(MouseEvents[event]);
return name == settings.customMouseEvents.end() ? MouseEvents[event] : name->second;
}
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"*";
@@ -67,6 +114,7 @@ inline const MouseAction *effectiveMouseAction(const Settings &settings, const s
bool foreground = true, bool *profileScoped = nullptr)
{
if (profileScoped) *profileScoped = false;
if (!mouseEventEnabled(settings, event)) return nullptr;
auto *action = mouseBinding(settings, app, event);
if (!action)
{
+20 -1
View File
@@ -108,6 +108,15 @@ static void validateMouseProfileConflicts(const Settings &s)
void validateSettings(const Settings &s)
{
std::set<std::wstring> eventNames;
for (int event = 0; event < BuiltInMouseEventCount; ++event) eventNames.insert(lower(MouseEvents[event]));
for (const auto &[signal, name] : s.customMouseEvents)
{
if (std::find(std::begin(MouseEvents) + BuiltInMouseEventCount, std::end(MouseEvents), signal) == std::end(MouseEvents) ||
name.empty() || name.size() > 80 || name.find_first_of(L"\r\n\t") != name.npos ||
!eventNames.insert(lower(name)).second)
throw std::runtime_error("Enter a unique event name (up to 80 characters) for a supported signal.");
}
if (s.mouseBindings.size() > 1000) throw std::runtime_error("Too many mouse profiles.");
for (const auto &[app, bindings] : s.mouseBindings)
{
@@ -124,7 +133,8 @@ void validateSettings(const Settings &s)
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) ||
const auto signal = std::find(std::begin(MouseEvents), std::end(MouseEvents), event);
if (!mouseEventEnabled(s, static_cast<int>(signal - std::begin(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))
@@ -382,6 +392,13 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
s.appLanguageSets[wide(name)] = std::move(set);
}
}
if (j.contains("customMouseEvents"))
{
const auto &events = j.at("customMouseEvents");
if (!events.is_object()) throw std::runtime_error("Invalid custom mouse events.");
for (const auto &[signal, name] : events.items())
s.customMouseEvents.emplace(wide(signal), wide(name.get<std::string>()));
}
if (j.contains("mouseBindings"))
{
const auto &profiles = j.at("mouseBindings");
@@ -520,6 +537,8 @@ 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["customMouseEvents"] = Json::object();
for (const auto &[signal, name] : s.customMouseEvents) j["customMouseEvents"][utf8(signal)] = utf8(name);
j["mouseBindings"] = Json::object();
for (const auto &[app, bindings] : s.mouseBindings)
{
+19 -6
View File
@@ -216,7 +216,20 @@ std::unique_ptr<SyncProvider> makeSyncProvider(const SyncConfig &c, SyncTranspor
{
return std::make_unique<OpenBao>(c, std::move(transport));
}
namespace { Json canonicalProfile(Json); }
namespace {
Json canonicalProfile(Json);
constexpr const char *PreviouslyLocalSettings[] = {
"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock",
"appLanguageSets", "mouseBindings", "disabledMouseProfiles", "mouseProfileApplications", "customMouseEvents"
};
void preserveMissingSettings(Json &profile, const Json &fallback)
{
for (auto key : PreviouslyLocalSettings)
if (!profile.contains(key) && fallback.contains(key)) profile[key] = fallback.at(key);
}
}
Bytes sharedSyncSettings(const Settings &s)
{
auto j = parse(encodeSettings(s));
@@ -226,14 +239,12 @@ Bytes sharedSyncSettings(const Settings &s)
std::sort(bindings.begin(), bindings.end(), [](const Json &a, const Json &b) {
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", "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)
{
auto j = parse(shared), original = parse(encodeSettings(local));
preserveMissingSettings(j, original);
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
@@ -245,7 +256,7 @@ Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const s
if (!j.is_object() || j.value("format", "") != "kswitcher-settings" || j.value("schemaVersion", 0) != 1)
throw std::runtime_error("Unsupported synchronized settings format.");
for (auto it = j.begin(); it != j.end(); ++it)
if (!allowed.contains(it.key())) throw std::runtime_error("Remote profile contains unsupported or local-only fields.");
if (!allowed.contains(it.key())) throw std::runtime_error("Remote profile contains unsupported fields.");
if (j.size() != allowed.size()) throw std::runtime_error("Remote profile is incomplete.");
std::set<std::string> installed;
for (const auto &layout : layouts) installed.insert(utf8(layout.id));
@@ -307,7 +318,9 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
else
{
auto profile = parse(action == SyncAction::Download ? remote.settings : mergeLocal);
if (!profile.contains("chatCycles")) profile["chatCycles"] = parse(local).value("chatCycles", Json::object());
const auto current = parse(local);
if (!profile.contains("chatCycles")) profile["chatCycles"] = current.value("chatCycles", Json::object());
preserveMissingSettings(profile, current);
auto selected = bytes(normalizeProfile(profile));
merged = {selected, selected, selected, {}};
}
+12
View File
@@ -72,6 +72,14 @@ Json flatProfile(Json j)
else ++profile;
}
}
if (j.contains("customMouseEvents"))
{
auto &events = j.at("customMouseEvents");
for (auto event = events.begin(); event != events.end();)
if (event.value().is_null()) event = events.erase(event);
else ++event;
if (events.is_null()) j.erase("customMouseEvents");
}
Json snippets = Json::array(), deleted = Json::array();
for (const auto &[id, item] : j.at("snippets").items())
{
@@ -215,6 +223,10 @@ SyncMerge mergeSyncProfile(const Bytes &base, const Bytes &local, const Bytes &r
// client's chat profiles until both copies understand this field.
if (!remoteProfile.contains("chatCycles")) remoteProfile["chatCycles"] = localProfile.value("chatCycles", Json::object());
if (!localProfile.contains("chatCycles")) localProfile["chatCycles"] = remoteProfile.at("chatCycles");
// Older server backups omit the newly synchronized fields. An explicit
// empty value from a current client still means the user cleared it.
preserveMissingSettings(remoteProfile, localProfile);
preserveMissingSettings(localProfile, remoteProfile);
auto l = treeProfile(localProfile), r = treeProfile(remoteProfile);
// With no common history, union independent entries; differing scalar values
// remain on their respective devices until the user resolves the conflict.
+2 -2
View File
@@ -1766,7 +1766,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
switch (msg)
{
case WM_DRAWITEM:
if (wp == AddMouseProfile || wp == RenameMouseProfile || wp == DeleteMouseProfile)
if (wp == AddMouseProfile || wp == RenameMouseProfile || wp == DeleteMouseProfile || wp == AddMouseEvent)
{
drawActionApplicationIcon(w, *reinterpret_cast<const DRAWITEMSTRUCT *>(lp));
return TRUE;
@@ -1801,7 +1801,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (((NMHDR *)lp)->idFrom == 733 && ((NMHDR *)lp)->code == NM_DBLCLK)
{
const auto &click = *reinterpret_cast<const NMITEMACTIVATE *>(lp);
showMouseActionEditor(w, *v, click.iItem);
showMouseActionEditor(w, *v, mouseEventAtRow(click.hdr.hwndFrom, click.iItem));
return 0;
}
if (((NMHDR *)lp)->idFrom == 260 && ((NMHDR *)lp)->code == TCN_SELCHANGE)
+238 -19
View File
@@ -5,6 +5,7 @@ std::wstring selectedMouseProfile(HWND w, const SettingsView &view)
}
constexpr int RenameMouseProfile = 757, DeleteMouseProfile = 758;
constexpr int AddMouseEvent = 762, MouseEventCapture = 763, MouseEventNameField = 764, MouseEventStatus = 765;
std::wstring mouseActionLabel(const MouseAction &action)
{
@@ -14,11 +15,23 @@ std::wstring mouseActionLabel(const MouseAction &action)
return MouseActions[static_cast<size_t>(action.type)];
}
int mouseEventAtRow(HWND list, int row)
{
if (row < 0) return -1;
LVITEMW item{LVIF_PARAM};
item.iItem = row;
return ListView_GetItem(list, &item) ? static_cast<int>(item.lParam) : -1;
}
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);
LVFINDINFOW search{LVFI_PARAM};
search.lParam = event;
const int row = ListView_FindItem(list, -1, &search);
if (row < 0) return;
ListView_SetItemState(list, row, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
ListView_EnsureVisible(list, row, FALSE);
}
void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
@@ -27,8 +40,17 @@ void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
EnableWindow(GetDlgItem(w, RenameMouseProfile), isNamedMouseProfile(profile));
EnableWindow(GetDlgItem(w, DeleteMouseProfile), isNamedMouseProfile(profile));
HWND list = GetDlgItem(w, 733);
ListView_DeleteAllItems(list);
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
{
if (!mouseEventEnabled(view.draft, event)) continue;
auto name = mouseEventName(view.draft, event);
LVITEMW item{LVIF_TEXT | LVIF_PARAM};
item.iItem = ListView_GetItemCount(list);
item.lParam = event;
item.pszText = name.data();
const int row = ListView_InsertItem(list, &item);
if (row < 0) continue;
auto *action = mouseBinding(view.draft, profile, event);
std::wstring text;
if (profile != GlobalMouseProfile && !action)
@@ -37,7 +59,7 @@ void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
action = mouseBinding(view.draft, GlobalMouseProfile, event);
}
text += action ? mouseActionLabel(*action) : MouseActions[0];
ListView_SetItemText(list, event, 1, text.data());
ListView_SetItemText(list, row, 1, text.data());
}
selectMouseEvent(w, selection);
}
@@ -97,6 +119,7 @@ bool blockMouseProfileStateChange(HWND w, SettingsView &view, const NMLISTVIEW &
{
Settings candidate;
candidate.mouseBindings = view.draft.mouseBindings;
candidate.customMouseEvents = view.draft.customMouseEvents;
candidate.disabledMouseProfiles = view.draft.disabledMouseProfiles;
candidate.mouseProfileApplications = view.draft.mouseProfileApplications;
candidate.disabledMouseProfiles.erase(profile);
@@ -309,7 +332,7 @@ void drawActionApplicationIcon(HWND editor, const DRAWITEMSTRUCT &item)
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)
if (item.CtlID == AddActionApp || item.CtlID == AddMouseProfile || item.CtlID == AddMouseEvent)
{
RECT vertical{x - weight / 2, y - half, x - weight / 2 + weight, y + half};
FillRect(item.hDC, &vertical, ink);
@@ -391,6 +414,7 @@ void saveMouseAction(HWND w, MouseActionEditor &editor)
const auto *event = MouseEvents[editor.event];
Settings check;
check.mouseBindings = editor.view->draft.mouseBindings;
check.customMouseEvents = editor.view->draft.customMouseEvents;
check.disabledMouseProfiles = editor.view->draft.disabledMouseProfiles;
check.mouseProfileApplications = editor.view->draft.mouseProfileApplications;
if (type < 0 || (type == static_cast<int>(MouseActionType::Standard) && profile == GlobalMouseProfile))
@@ -487,7 +511,7 @@ LRESULT CALLBACK mouseActionEditorProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
void showMouseActionEditor(HWND parent, SettingsView &view, int event)
{
if (event < 0 || event >= static_cast<int>(std::size(MouseEvents)) || !IsWindowEnabled(parent)) return;
if (!mouseEventEnabled(view.draft, event) || !IsWindowEnabled(parent)) return;
const auto profile = selectedMouseProfile(parent, view);
const bool application = profile != GlobalMouseProfile;
const auto *action = mouseBinding(view.draft, profile, event);
@@ -496,7 +520,7 @@ void showMouseActionEditor(HWND parent, SettingsView &view, int event)
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(),
label(w, mouseEventName(view.draft, event).append(L" · ").append(mouseProfileName(profile)).c_str(),
18, 14, 444);
label(w, L"&Action", 18, 43, 330);
windowLayoutState(w)->fixedClient = true;
@@ -578,7 +602,7 @@ void showMouseListMenu(HWND list, POINT point, bool profiles)
hit.pt = point;
ScreenToClient(list, &hit.pt);
event = ListView_SubItemHitTest(list, &hit);
if (event < 0) return;
if (event < 0 && profiles) return;
}
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
HMENU menu = CreatePopupMenu();
@@ -592,9 +616,11 @@ void showMouseListMenu(HWND list, POINT point, bool profiles)
}
else
{
selectMouseEvent(w, event);
const bool canReset = mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
AppendMenuW(menu, MF_STRING, EditMouseEvent, L"&Edit…");
event = mouseEventAtRow(list, event);
if (event >= 0) selectMouseEvent(w, event);
const bool canReset = event >= 0 && mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
AppendMenuW(menu, MF_STRING, AddMouseEvent, L"+ &Add mouse event…");
AppendMenuW(menu, MF_STRING | (event >= 0 ? MF_ENABLED : MF_GRAYED), 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,
@@ -687,6 +713,7 @@ LRESULT CALLBACK mouseProfileNameProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
const auto key = std::wstring(NamedMouseProfilePrefix) + name;
Settings check;
check.mouseBindings = view.draft.mouseBindings;
check.customMouseEvents = view.draft.customMouseEvents;
check.disabledMouseProfiles = view.draft.disabledMouseProfiles;
check.mouseProfileApplications = view.draft.mouseProfileApplications;
if (!previous.empty() && key != previous)
@@ -768,9 +795,204 @@ void showMouseProfileName(HWND owner, SettingsView &view, bool renaming)
SetFocus(GetDlgItem(w, MouseProfileName));
}
struct AddMouseEventView
{
HWND parent{};
SettingsView *settings{};
int signal{-1}, lastButton{-1};
DWORD lastPress{};
POINT lastPoint{};
bool rawRegistered{};
HHOOK mouseHook{};
int rawWheel{-1}, hookWheel{-1};
ULONGLONG rawWheelAt{}, hookWheelAt{};
};
constexpr UINT CapturedMouseWheel = WM_APP + 86;
constexpr UINT_PTR MouseSignalTimeout = 1;
HWND mouseEventCaptureWindow{};
bool inMouseEventCapture(HWND w, POINT point)
{
RECT area{};
return GetWindowRect(GetDlgItem(w, MouseEventCapture), &area) && PtInRect(&area, point);
}
LRESULT CALLBACK captureMouseWheelProc(int code, WPARAM wp, LPARAM lp)
{
if (code == HC_ACTION && mouseEventCaptureWindow && GetForegroundWindow() == mouseEventCaptureWindow)
{
const auto &mouse = *reinterpret_cast<const MSLLHOOKSTRUCT *>(lp);
const int signal = mouseWheelEvent(static_cast<UINT>(wp), static_cast<SHORT>(HIWORD(mouse.mouseData)));
if (signal >= 0 && !(mouse.flags & LLMHF_INJECTED) && inMouseEventCapture(mouseEventCaptureWindow, mouse.pt))
PostMessageW(mouseEventCaptureWindow, CapturedMouseWheel, signal, 0);
}
return CallNextHookEx(nullptr, code, wp, lp);
}
void showCapturedMouseSignal(HWND w, AddMouseEventView &view, int signal)
{
view.signal = signal;
const auto &draft = view.settings->draft;
const bool duplicate = mouseEventEnabled(draft, signal);
EnableWindow(GetDlgItem(w, IDOK), !duplicate);
if (duplicate)
{
selectMouseEvent(view.parent, signal);
value(w, MouseEventStatus, L"Already in the list: " + mouseEventName(draft, signal));
value(w, IDCANCEL, L"Close");
}
else
{
value(w, MouseEventStatus, std::wstring(L"Detected: ") + MouseEvents[signal]);
if (value(w, MouseEventNameField).empty()) value(w, MouseEventNameField, MouseEvents[signal]);
value(w, IDCANCEL, L"Cancel");
}
}
void matchCapturedMouseWheel(HWND w, AddMouseEventView &view, ULONGLONG now)
{
if (view.rawWheel >= 0 && view.rawWheel == view.hookWheel &&
now - view.rawWheelAt <= 500 && now - view.hookWheelAt <= 500)
{
showCapturedMouseSignal(w, view, view.rawWheel);
view.rawWheel = view.hookWheel = -1;
KillTimer(w, MouseSignalTimeout);
}
}
void readNewMouseSignal(HWND w, AddMouseEventView &view, HRAWINPUT handle)
{
POINT point{};
if (!GetCursorPos(&point) || !inMouseEventCapture(w, point)) return;
RAWINPUT input{};
UINT size = sizeof(input);
const auto bytes = GetRawInputData(handle, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER));
if (bytes == UINT(-1) || bytes < offsetof(RAWINPUT, data) + sizeof(RAWMOUSE) ||
input.header.dwType != RIM_TYPEMOUSE || !input.header.hDevice)
return;
int signal = rawMouseSignal(input.data.mouse);
if (signal < 0) return;
if (signal >= BuiltInMouseEventCount)
{
view.signal = -1;
EnableWindow(GetDlgItem(w, IDOK), FALSE);
value(w, MouseEventStatus, view.mouseHook ? L"Checking whether Windows delivers this wheel signal…" :
L"Windows mouse hook is unavailable.");
view.rawWheel = signal;
view.rawWheelAt = GetTickCount64();
if (view.mouseHook) SetTimer(w, MouseSignalTimeout, 550, nullptr);
matchCapturedMouseWheel(w, view, view.rawWheelAt);
return;
}
view.rawWheel = view.hookWheel = -1;
KillTimer(w, MouseSignalTimeout);
const DWORD now = static_cast<DWORD>(GetMessageTime());
const bool second = signal < 3 && view.lastButton == signal &&
doubleMouseClick(view.lastPoint, point, now - view.lastPress);
view.lastButton = second ? -1 : signal;
view.lastPress = now;
view.lastPoint = point;
if (second) signal += 5;
showCapturedMouseSignal(w, view, signal);
}
LRESULT CALLBACK addMouseEventProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
{
auto *view = reinterpret_cast<AddMouseEventView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
if (!view) return DefWindowProcW(w, msg, wp, lp);
try
{
if (msg == WM_INPUT)
readNewMouseSignal(w, *view, reinterpret_cast<HRAWINPUT>(lp));
else if (msg == CapturedMouseWheel)
{
view->hookWheel = static_cast<int>(wp);
view->hookWheelAt = GetTickCount64();
matchCapturedMouseWheel(w, *view, view->hookWheelAt);
return 0;
}
else if (msg == WM_TIMER && wp == MouseSignalTimeout)
{
KillTimer(w, MouseSignalTimeout);
view->rawWheel = view->hookWheel = -1;
value(w, MouseEventStatus, L"Raw Input saw this signal, but the Windows mouse hook did not.");
return 0;
}
else if (msg == WM_COMMAND)
{
if (LOWORD(wp) == IDOK)
{
const int signal = view->signal;
auto &settings = *view->settings;
if (signal < BuiltInMouseEventCount || signal >= static_cast<int>(std::size(MouseEvents)) ||
mouseEventEnabled(settings.draft, signal)) return 0;
auto candidate = settings.draft;
candidate.customMouseEvents.emplace(MouseEvents[signal], trimApplicationText(value(w, MouseEventNameField)));
validateSettings(candidate);
settings.draft.customMouseEvents = std::move(candidate.customMouseEvents);
fillMouseEvents(view->parent, settings, signal);
DestroyWindow(w);
return 0;
}
if (LOWORD(wp) == IDCANCEL) { DestroyWindow(w); return 0; }
}
else if (msg == WM_CLOSE) { DestroyWindow(w); return 0; }
else if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; }
else if (msg == WM_NCDESTROY)
{
const HWND parent = view->parent;
mouseEventCaptureWindow = nullptr;
if (view->mouseHook) UnhookWindowsHookEx(view->mouseHook);
if (view->rawRegistered)
{
if (IsWindow(parent) && view->settings->rawMouseRegistered) registerMouseTestInput(parent);
else registerMouseTestInput(nullptr);
}
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
delete view;
disposeFont(w);
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 showAddMouseEvent(HWND parent, SettingsView &settings)
{
if (!IsWindowEnabled(parent)) return;
HWND w = makeWindow(*settings.app, L"KSwitcher.AddMouseEvent", L"Add mouse event", addMouseEventProc, 500, 275);
auto *view = new AddMouseEventView{parent, &settings};
SetWindowLongPtrW(w, GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(parent));
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(view));
label(w, L"&Name", 18, 16, 464);
edit(w, MouseEventNameField, 18, 39, 464);
control(w, L"STATIC", L"Press a mouse button or move the wheel here",
WS_BORDER | SS_CENTER | SS_CENTERIMAGE, MouseEventCapture, 18, 78, 464, 92);
view->rawRegistered = registerMouseTestInput(w);
mouseEventCaptureWindow = w;
view->mouseHook = SetWindowsHookExW(WH_MOUSE_LL, captureMouseWheelProc, settings.app->instance, 0);
control(w, L"STATIC", view->rawRegistered ? L"Waiting for a supported mouse signal…" : L"Windows Raw Input is unavailable.",
0, MouseEventStatus, 18, 181, 464, 22);
label(w, L"Buttons that send no Windows mouse event cannot be added.", 18, 205, 464);
control(w, L"BUTTON", L"&Add", WS_TABSTOP | BS_DEFPUSHBUTTON, IDOK, 270, 239, 100, 28);
button(w, L"Cancel", IDCANCEL, 382, 239, 100);
EnableWindow(GetDlgItem(w, IDOK), FALSE);
EnableWindow(parent, FALSE);
showRequestedWindow(w);
SetFocus(GetDlgItem(w, MouseEventNameField));
}
bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
{
const int id = LOWORD(wp);
if (id == AddMouseEvent) { showAddMouseEvent(w, view); return true; }
if (id == AddMouseProfile) { showMouseProfileName(w, view, false); return true; }
if (id == RenameMouseProfile || id == DeleteMouseProfile)
{
@@ -788,7 +1010,8 @@ bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
return true;
}
if (id != EditMouseEvent && id != ResetMouseEvent) return false;
const int event = ListView_GetNextItem(GetDlgItem(w, 733), -1, LVNI_SELECTED);
const HWND list = GetDlgItem(w, 733);
const int event = mouseEventAtRow(list, ListView_GetNextItem(list, -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));
@@ -817,7 +1040,9 @@ 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);
label(w, L"Mouse events", actionsLeft, 64, tableWidth - 30);
control(w, L"BUTTON", L"Add mouse event", WS_TABSTOP | BS_OWNERDRAW, AddMouseEvent,
actionsLeft + tableWidth - 24, 61, 24, 24);
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);
@@ -827,6 +1052,7 @@ void createMousePage(HWND w, SettingsView &view)
addActionApplicationTooltip(tips, w, AddMouseProfile, L"Add profile");
addActionApplicationTooltip(tips, w, RenameMouseProfile, L"Edit profile");
addActionApplicationTooltip(tips, w, DeleteMouseProfile, L"Delete profile");
addActionApplicationTooltip(tips, w, AddMouseEvent, L"Add mouse event");
constexpr int listTop = 90, listHeight = 224;
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES};
InitCommonControlsEx(&controls);
@@ -859,13 +1085,6 @@ void createMousePage(HWND w, SettingsView &view)
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);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.64";
inline constexpr wchar_t AppVersion[] = L"0.6.65";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;