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

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