1097 lines
49 KiB
C++
1097 lines
49 KiB
C++
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);
|
||
}
|