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

+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);