Files
hrynco-kswitcher/src/ui_mouse_buttons.inl
T
2026-11-12 01:18:21 +02:00

1097 lines
49 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
constexpr int AddMouseEvent = 762, MouseEventCapture = 763, MouseEventNameField = 764, MouseEventStatus = 765;
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)];
}
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);
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)
{
const auto profile = selectedMouseProfile(w, view);
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)
{
text += L"Global: ";
action = mouseBinding(view.draft, GlobalMouseProfile, event);
}
text += action ? mouseActionLabel(*action) : MouseActions[0];
ListView_SetItemText(list, row, 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.customMouseEvents = view.draft.customMouseEvents;
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 || item.CtlID == AddMouseEvent)
{
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.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))
{
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 (!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);
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, 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;
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 && profiles) 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
{
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,
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.customMouseEvents = view.draft.customMouseEvents;
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));
}
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)
{
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 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));
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 - 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);
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");
addActionApplicationTooltip(tips, w, AddMouseEvent, L"Add mouse event");
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);
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);
}