feat: release kswitcher 0.6.64
Add mouse button actions and application profiles, with volume controls and per-monitor settings layout fixes.
This commit is contained in:
1 parent
5a2106899d
commit
14f33012a3
23 files changed
+2233
-107
No files matched your search
@@ -0,0 +1,877 @@
|
||||
std::wstring selectedMouseProfile(HWND w, const SettingsView &view)
|
||||
{
|
||||
const auto row = ListView_GetNextItem(GetDlgItem(w, 731), -1, LVNI_SELECTED);
|
||||
return row < 0 || static_cast<size_t>(row) >= view.mouseProfiles.size() ? GlobalMouseProfile : view.mouseProfiles[row];
|
||||
}
|
||||
|
||||
constexpr int RenameMouseProfile = 757, DeleteMouseProfile = 758;
|
||||
|
||||
std::wstring mouseActionLabel(const MouseAction &action)
|
||||
{
|
||||
if (action.type == MouseActionType::Shortcut) return action.shortcut.format();
|
||||
if (action.type == MouseActionType::Launch)
|
||||
return L"Launch " + std::filesystem::path(action.executable).filename().wstring();
|
||||
return MouseActions[static_cast<size_t>(action.type)];
|
||||
}
|
||||
|
||||
void selectMouseEvent(HWND w, int event)
|
||||
{
|
||||
HWND list = GetDlgItem(w, 733);
|
||||
ListView_SetItemState(list, event, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
ListView_EnsureVisible(list, event, FALSE);
|
||||
}
|
||||
|
||||
void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
|
||||
{
|
||||
const auto profile = selectedMouseProfile(w, view);
|
||||
EnableWindow(GetDlgItem(w, RenameMouseProfile), isNamedMouseProfile(profile));
|
||||
EnableWindow(GetDlgItem(w, DeleteMouseProfile), isNamedMouseProfile(profile));
|
||||
HWND list = GetDlgItem(w, 733);
|
||||
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
|
||||
{
|
||||
auto *action = mouseBinding(view.draft, profile, event);
|
||||
std::wstring text;
|
||||
if (profile != GlobalMouseProfile && !action)
|
||||
{
|
||||
text += L"Global: ";
|
||||
action = mouseBinding(view.draft, GlobalMouseProfile, event);
|
||||
}
|
||||
text += action ? mouseActionLabel(*action) : MouseActions[0];
|
||||
ListView_SetItemText(list, event, 1, text.data());
|
||||
}
|
||||
selectMouseEvent(w, selection);
|
||||
}
|
||||
|
||||
std::set<std::wstring> profileApplications(const Settings &settings, const std::wstring &profile)
|
||||
{
|
||||
const auto scope = settings.mouseProfileApplications.find(profile);
|
||||
if (scope != settings.mouseProfileApplications.end()) return scope->second;
|
||||
if (profile != GlobalMouseProfile && !isNamedMouseProfile(profile)) return {profile};
|
||||
std::set<std::wstring> names;
|
||||
const auto bindings = settings.mouseBindings.find(profile);
|
||||
if (bindings != settings.mouseBindings.end())
|
||||
for (const auto &[event, action] : bindings->second)
|
||||
names.insert(action.applications.begin(), action.applications.end());
|
||||
return names;
|
||||
}
|
||||
|
||||
void selectMouseProfile(HWND w, int row)
|
||||
{
|
||||
auto list = GetDlgItem(w, 731);
|
||||
ListView_SetItemState(list, row, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
ListView_EnsureVisible(list, row, FALSE);
|
||||
}
|
||||
|
||||
void appendMouseProfile(HWND w, SettingsView &view, const std::wstring &profile)
|
||||
{
|
||||
auto list = GetDlgItem(w, 731);
|
||||
std::wstring text;
|
||||
for (const auto &app : profile == GlobalMouseProfile ? std::set<std::wstring>{} : profileApplications(view.draft, profile))
|
||||
{
|
||||
if (!text.empty()) text += L", ";
|
||||
text += app + L".exe";
|
||||
}
|
||||
if (text.empty()) text = profile == GlobalMouseProfile ? L"Fallback" : L"All applications";
|
||||
LVITEMW item{LVIF_TEXT};
|
||||
item.iItem = static_cast<int>(view.mouseProfiles.size());
|
||||
wchar_t activeLabel[] = L"";
|
||||
item.pszText = activeLabel;
|
||||
const int row = ListView_InsertItem(list, &item);
|
||||
if (row < 0) throw std::runtime_error("Could not add the profile to the list.");
|
||||
auto label = mouseProfileName(profile);
|
||||
ListView_SetItemText(list, row, 1, label.data());
|
||||
ListView_SetItemText(list, row, 2, text.data());
|
||||
ListView_SetCheckState(list, row, !view.draft.disabledMouseProfiles.contains(profile));
|
||||
view.mouseProfiles.push_back(profile);
|
||||
}
|
||||
|
||||
bool blockMouseProfileStateChange(HWND w, SettingsView &view, const NMLISTVIEW &change)
|
||||
{
|
||||
const UINT previous = change.uOldState & LVIS_STATEIMAGEMASK;
|
||||
const UINT next = change.uNewState & LVIS_STATEIMAGEMASK;
|
||||
if (!(change.uChanged & LVIF_STATE) || !previous || !next || previous == next ||
|
||||
change.iItem < 0 || static_cast<size_t>(change.iItem) >= view.mouseProfiles.size()) return false;
|
||||
const auto &profile = view.mouseProfiles[change.iItem];
|
||||
if (!isNamedMouseProfile(profile)) return true;
|
||||
if (next == INDEXTOSTATEIMAGEMASK(2))
|
||||
{
|
||||
Settings candidate;
|
||||
candidate.mouseBindings = view.draft.mouseBindings;
|
||||
candidate.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
||||
candidate.mouseProfileApplications = view.draft.mouseProfileApplications;
|
||||
candidate.disabledMouseProfiles.erase(profile);
|
||||
try { validateSettings(candidate); }
|
||||
catch (const std::exception &e) { error(w, e); return true; }
|
||||
view.draft.disabledMouseProfiles.erase(profile);
|
||||
}
|
||||
else view.draft.disabledMouseProfiles.insert(profile);
|
||||
updateSettingsState(w, &view);
|
||||
return false;
|
||||
}
|
||||
|
||||
void fillMouseProfiles(HWND w, SettingsView &view)
|
||||
{
|
||||
const auto previous = selectedMouseProfile(w, view);
|
||||
ListView_DeleteAllItems(GetDlgItem(w, 731));
|
||||
view.mouseProfiles.clear();
|
||||
appendMouseProfile(w, view, GlobalMouseProfile);
|
||||
for (const auto &[app, bindings] : view.draft.mouseBindings)
|
||||
if (app != GlobalMouseProfile) appendMouseProfile(w, view, app);
|
||||
auto selected = std::find(view.mouseProfiles.begin(), view.mouseProfiles.end(), previous);
|
||||
selectMouseProfile(w, selected == view.mouseProfiles.end() ? 0 : static_cast<int>(selected - view.mouseProfiles.begin()));
|
||||
fillMouseEvents(w, view);
|
||||
}
|
||||
|
||||
|
||||
LRESULT CALLBACK mouseButtonCaptureProc(HWND field, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR)
|
||||
{
|
||||
const bool doubleClick = msg == WM_LBUTTONDBLCLK || msg == WM_RBUTTONDBLCLK || msg == WM_MBUTTONDBLCLK;
|
||||
switch (msg)
|
||||
{
|
||||
case WM_LBUTTONDBLCLK: msg = WM_LBUTTONDOWN; break;
|
||||
case WM_RBUTTONDBLCLK: msg = WM_RBUTTONDOWN; break;
|
||||
case WM_MBUTTONDBLCLK: msg = WM_MBUTTONDOWN; break;
|
||||
case WM_XBUTTONDBLCLK: msg = WM_XBUTTONDOWN; break;
|
||||
}
|
||||
const int button = mouseButton(msg, HIWORD(wp));
|
||||
if (button >= 0)
|
||||
{
|
||||
if (mouseButtonDown(msg))
|
||||
{
|
||||
SetFocus(field);
|
||||
SetCapture(field);
|
||||
HWND settings = GetParent(field);
|
||||
auto *view = reinterpret_cast<SettingsView *>(GetWindowLongPtrW(settings, GWLP_USERDATA));
|
||||
if (view)
|
||||
selectMouseEvent(settings, doubleClick && button < 3 ? button + 5 : button);
|
||||
if (view)
|
||||
value(settings, 738, std::wstring(L"Window event: ") + MouseButtonNames[button] +
|
||||
L" | Presses: " + std::to_wstring(++view->mouseEvents));
|
||||
}
|
||||
else if (GetCapture() == field)
|
||||
ReleaseCapture();
|
||||
return msg == WM_XBUTTONDOWN || msg == WM_XBUTTONUP ? TRUE : 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(field, mouseButtonCaptureProc, id);
|
||||
return DefSubclassProc(field, msg, wp, lp);
|
||||
}
|
||||
|
||||
bool registerMouseTestInput(HWND w)
|
||||
{
|
||||
RAWINPUTDEVICE mouse{0x01, 0x02, w ? DWORD{0} : RIDEV_REMOVE, w};
|
||||
return RegisterRawInputDevices(&mouse, 1, sizeof(mouse)) != FALSE;
|
||||
}
|
||||
|
||||
void readMouseTestInput(HWND w, SettingsView &view, HRAWINPUT handle)
|
||||
{
|
||||
HWND capture = GetDlgItem(w, 737);
|
||||
if (!IsWindowVisible(capture)) return;
|
||||
RAWINPUT input{};
|
||||
UINT size = sizeof(input);
|
||||
if (GetRawInputData(handle, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER)) == UINT(-1) ||
|
||||
input.header.dwType != RIM_TYPEMOUSE || !input.data.mouse.usButtonFlags)
|
||||
return;
|
||||
POINT point{};
|
||||
if (!GetCursorPos(&point) || WindowFromPoint(point) != capture) return;
|
||||
constexpr USHORT downFlags[] = {RI_MOUSE_LEFT_BUTTON_DOWN, RI_MOUSE_RIGHT_BUTTON_DOWN,
|
||||
RI_MOUSE_MIDDLE_BUTTON_DOWN, RI_MOUSE_BUTTON_4_DOWN, RI_MOUSE_BUTTON_5_DOWN};
|
||||
static_assert(std::size(downFlags) == std::size(MouseButtonNames));
|
||||
for (size_t i = 0; i < std::size(downFlags); ++i)
|
||||
{
|
||||
if (!(input.data.mouse.usButtonFlags & downFlags[i])) continue;
|
||||
|
||||
value(w, 739, std::wstring(L"Raw Input: ") + MouseButtonNames[i] +
|
||||
L" | Presses: " + std::to_wstring(++view.rawMouseEvents));
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int MouseVolumeStepField = 759;
|
||||
constexpr int MouseVolumeStepSpin = 760, MouseVolumeStepLabel = 761;
|
||||
constexpr int MouseActionEditorHeight = 194, CompactMouseActionEditorHeight = 138, MaxEditedMouseVolumeStep = 50;
|
||||
|
||||
void layoutMouseActionFields(HWND w, bool volume, bool extraRow)
|
||||
{
|
||||
auto &layout = *windowLayoutState(w);
|
||||
const auto combo = GetDlgItem(w, 734);
|
||||
const auto buttons = GetDlgItem(w, IDOK);
|
||||
const int height = extraRow ? MouseActionEditorHeight : CompactMouseActionEditorHeight;
|
||||
const int offset = height - layout.height;
|
||||
bool footer = false;
|
||||
for (auto &[child, rect] : layout.controls)
|
||||
{
|
||||
if (child == combo) rect.right = volume ? 368 : 462;
|
||||
if (child == buttons) footer = true;
|
||||
if (footer) OffsetRect(&rect, 0, offset);
|
||||
}
|
||||
if (offset)
|
||||
{
|
||||
RECT frame{};
|
||||
GetWindowRect(w, &frame);
|
||||
layout.height = height;
|
||||
fitFixedClient(w, layout, frame);
|
||||
}
|
||||
arrangeWindow(w);
|
||||
}
|
||||
|
||||
void updateMouseActionFields(HWND w)
|
||||
{
|
||||
const auto index = SendDlgItemMessageW(w, 734, CB_GETCURSEL, 0, 0);
|
||||
const auto type = SendDlgItemMessageW(w, 734, CB_GETITEMDATA, index, 0);
|
||||
const bool shortcut = type == static_cast<int>(MouseActionType::Shortcut);
|
||||
const bool launch = type == static_cast<int>(MouseActionType::Launch);
|
||||
const bool volume = isMouseVolumeStep(static_cast<MouseActionType>(type));
|
||||
ShowWindow(GetDlgItem(w, 747), shortcut ? SW_SHOWNA : SW_HIDE);
|
||||
ShowWindow(GetDlgItem(w, 742), launch ? SW_SHOWNA : SW_HIDE);
|
||||
ShowWindow(GetDlgItem(w, 743), launch ? SW_SHOWNA : SW_HIDE);
|
||||
ShowWindow(GetDlgItem(w, MouseVolumeStepField), volume ? SW_SHOWNA : SW_HIDE);
|
||||
ShowWindow(GetDlgItem(w, MouseVolumeStepSpin), volume ? SW_SHOWNA : SW_HIDE);
|
||||
ShowWindow(GetDlgItem(w, MouseVolumeStepLabel), volume ? SW_SHOWNA : SW_HIDE);
|
||||
ShowWindow(GetDlgItem(w, 744), shortcut || launch ? SW_SHOWNA : SW_HIDE);
|
||||
value(w, 744, shortcut ? L"Click the field and press the key combination" :
|
||||
launch ? L"Application (e.g. calc.exe)" : L"");
|
||||
layoutMouseActionFields(w, volume, shortcut || launch);
|
||||
}
|
||||
|
||||
struct MouseActionEditor
|
||||
{
|
||||
HWND parent{};
|
||||
SettingsView *view{};
|
||||
std::wstring profile;
|
||||
int event{};
|
||||
};
|
||||
|
||||
struct MouseProfileEditor
|
||||
{
|
||||
std::wstring previous;
|
||||
std::set<std::wstring> applications;
|
||||
HWND pickerOutline{};
|
||||
};
|
||||
|
||||
constexpr int ActionApplications = 748, ActionApplicationName = 749, AddActionApp = 750, RemoveActionApp = 751;
|
||||
constexpr int BrowseActionApp = 752;
|
||||
constexpr int PickActionApp = 753;
|
||||
constexpr int AddMouseProfile = 754, MouseProfileName = 755;
|
||||
|
||||
void drawActionApplicationIcon(HWND editor, const DRAWITEMSTRUCT &item)
|
||||
{
|
||||
RECT bounds = item.rcItem;
|
||||
const bool pressed = (item.itemState & ODS_SELECTED) != 0;
|
||||
const bool disabled = (item.itemState & ODS_DISABLED) != 0;
|
||||
DrawFrameControl(item.hDC, &bounds, DFC_BUTTON,
|
||||
DFCS_BUTTONPUSH | (pressed ? DFCS_PUSHED : 0) | (disabled ? DFCS_INACTIVE : 0));
|
||||
const int offset = pressed ? scale(editor, 1) : 0;
|
||||
const int x = (bounds.left + bounds.right) / 2 + offset;
|
||||
const int y = (bounds.top + bounds.bottom) / 2 + offset;
|
||||
const int half = scale(editor, 6);
|
||||
const int weight = std::max(1, scale(editor, 2));
|
||||
HBRUSH ink = GetSysColorBrush(disabled ? COLOR_GRAYTEXT : COLOR_BTNTEXT);
|
||||
if (item.CtlID == BrowseActionApp)
|
||||
{
|
||||
RECT folder{x - half - 1, y - half + weight, x + half + 1, y + half};
|
||||
RECT tab{folder.left, folder.top - weight, x, folder.top};
|
||||
FrameRect(item.hDC, &folder, ink);
|
||||
FillRect(item.hDC, &tab, ink);
|
||||
}
|
||||
else if (item.CtlID == PickActionApp)
|
||||
{
|
||||
RECT target{x - half, y - half, x + half + 1, y + half + 1};
|
||||
RECT horizontal{x - half - weight, y, x + half + weight + 1, y + 1};
|
||||
RECT vertical{x, y - half - weight, x + 1, y + half + weight + 1};
|
||||
const auto previousBrush = SelectObject(item.hDC, GetStockObject(HOLLOW_BRUSH));
|
||||
SetDCPenColor(item.hDC, GetSysColor(disabled ? COLOR_GRAYTEXT : COLOR_BTNTEXT));
|
||||
const auto previousPen = SelectObject(item.hDC, GetStockObject(DC_PEN));
|
||||
Ellipse(item.hDC, target.left, target.top, target.right, target.bottom);
|
||||
SelectObject(item.hDC, previousPen);
|
||||
SelectObject(item.hDC, previousBrush);
|
||||
FillRect(item.hDC, &horizontal, ink);
|
||||
FillRect(item.hDC, &vertical, ink);
|
||||
}
|
||||
else if (item.CtlID == RenameMouseProfile)
|
||||
{
|
||||
POINT pencil[]{{x - half, y + half}, {x - half + weight, y + 1},
|
||||
{x + half - weight, y - half}, {x + half, y - half + weight},
|
||||
{x - 1, y + half - weight}};
|
||||
const auto brush = SelectObject(item.hDC, ink);
|
||||
const auto pen = SelectObject(item.hDC, GetStockObject(NULL_PEN));
|
||||
Polygon(item.hDC, pencil, static_cast<int>(std::size(pencil)));
|
||||
SelectObject(item.hDC, pen);
|
||||
SelectObject(item.hDC, brush);
|
||||
}
|
||||
else if (item.CtlID == DeleteMouseProfile)
|
||||
{
|
||||
RECT bin{x - half + 2, y - half + weight, x + half - 1, y + half};
|
||||
RECT lid{x - half, y - half, x + half + 1, y - half + 1};
|
||||
FrameRect(item.hDC, &bin, ink);
|
||||
FillRect(item.hDC, &lid, ink);
|
||||
}
|
||||
else
|
||||
{
|
||||
RECT horizontal{x - half, y - weight / 2, x + half, y - weight / 2 + weight};
|
||||
FillRect(item.hDC, &horizontal, ink);
|
||||
}
|
||||
if (item.CtlID == AddActionApp || item.CtlID == AddMouseProfile)
|
||||
{
|
||||
RECT vertical{x - weight / 2, y - half, x - weight / 2 + weight, y + half};
|
||||
FillRect(item.hDC, &vertical, ink);
|
||||
}
|
||||
if ((item.itemState & ODS_FOCUS) && !(item.itemState & ODS_NOFOCUSRECT))
|
||||
{
|
||||
InflateRect(&bounds, -scale(editor, 4), -scale(editor, 4));
|
||||
DrawFocusRect(item.hDC, &bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void addActionApplicationTooltip(HWND tips, HWND editor, int id, const wchar_t *text)
|
||||
{
|
||||
TOOLINFOW tool{sizeof(tool), TTF_IDISHWND | TTF_SUBCLASS, editor,
|
||||
reinterpret_cast<UINT_PTR>(GetDlgItem(editor, id))};
|
||||
tool.lpszText = const_cast<wchar_t *>(text);
|
||||
SendMessageW(tips, TTM_ADDTOOLW, 0, reinterpret_cast<LPARAM>(&tool));
|
||||
}
|
||||
|
||||
void fillActionApplications(HWND w, const MouseProfileEditor &editor)
|
||||
{
|
||||
SendDlgItemMessageW(w, ActionApplications, LB_RESETCONTENT, 0, 0);
|
||||
for (const auto &name : editor.applications)
|
||||
{
|
||||
const auto label = name + L".exe";
|
||||
SendDlgItemMessageW(w, ActionApplications, LB_ADDSTRING, 0, reinterpret_cast<LPARAM>(label.c_str()));
|
||||
}
|
||||
}
|
||||
|
||||
void addActionApplication(HWND w, MouseProfileEditor &editor)
|
||||
{
|
||||
const auto name = applicationName(trimApplicationText(value(w, ActionApplicationName)));
|
||||
if (!validMouseApplication(name)) throw std::runtime_error("Enter a valid application executable name.");
|
||||
if (editor.applications.size() >= 1000 && !editor.applications.contains(name))
|
||||
throw std::runtime_error("Too many applications in this profile.");
|
||||
editor.applications.insert(name);
|
||||
fillActionApplications(w, editor);
|
||||
value(w, ActionApplicationName, L"");
|
||||
}
|
||||
|
||||
std::wstring actionApplicationExecutable(std::filesystem::path path)
|
||||
{
|
||||
if (lower(path.extension().wstring()) == L".lnk")
|
||||
{
|
||||
Microsoft::WRL::ComPtr<IShellLinkW> link;
|
||||
Microsoft::WRL::ComPtr<IPersistFile> file;
|
||||
wchar_t target[32768]{};
|
||||
if (FAILED(CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&link))) ||
|
||||
FAILED(link.As(&file)) || FAILED(file->Load(path.c_str(), STGM_READ)) ||
|
||||
link->GetPath(target, static_cast<int>(std::size(target)), nullptr, 0) != S_OK)
|
||||
throw std::runtime_error("Could not read the shortcut target.");
|
||||
path = target;
|
||||
}
|
||||
if (lower(path.extension().wstring()) != L".exe")
|
||||
throw std::runtime_error("Choose an executable or a shortcut that points to one.");
|
||||
return path.filename().wstring();
|
||||
}
|
||||
|
||||
void browseActionApplication(HWND w, MouseProfileEditor &editor)
|
||||
{
|
||||
wchar_t path[32768]{};
|
||||
OPENFILENAMEW picker{sizeof(picker)};
|
||||
picker.hwndOwner = w;
|
||||
picker.lpstrFilter = L"Applications and shortcuts (*.exe;*.lnk)\0*.exe;*.lnk\0";
|
||||
picker.lpstrFile = path;
|
||||
picker.nMaxFile = static_cast<DWORD>(std::size(path));
|
||||
picker.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR | OFN_NODEREFERENCELINKS;
|
||||
if (!GetOpenFileNameW(&picker)) return;
|
||||
value(w, ActionApplicationName, actionApplicationExecutable(path));
|
||||
addActionApplication(w, editor);
|
||||
}
|
||||
|
||||
void saveMouseAction(HWND w, MouseActionEditor &editor)
|
||||
{
|
||||
const auto choice = SendDlgItemMessageW(w, 734, CB_GETCURSEL, 0, 0);
|
||||
if (choice == CB_ERR) throw std::runtime_error("Choose an action.");
|
||||
const auto type = SendDlgItemMessageW(w, 734, CB_GETITEMDATA, choice, 0);
|
||||
const auto &profile = editor.profile;
|
||||
const auto *event = MouseEvents[editor.event];
|
||||
Settings check;
|
||||
check.mouseBindings = editor.view->draft.mouseBindings;
|
||||
check.disabledMouseProfiles = editor.view->draft.disabledMouseProfiles;
|
||||
check.mouseProfileApplications = editor.view->draft.mouseProfileApplications;
|
||||
if (type < 0 || (type == static_cast<int>(MouseActionType::Standard) && profile == GlobalMouseProfile))
|
||||
{
|
||||
if (auto found = check.mouseBindings.find(profile); found != check.mouseBindings.end())
|
||||
{
|
||||
found->second.erase(event);
|
||||
if (found->second.empty() && !isNamedMouseProfile(profile)) check.mouseBindings.erase(found);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
MouseAction action{static_cast<MouseActionType>(type)};
|
||||
if (action.type == MouseActionType::Shortcut)
|
||||
{
|
||||
auto shortcut = Hotkey::parse(value(w, 747));
|
||||
if (!shortcut || !shortcut->key) throw std::runtime_error("Click the field and press a key combination.");
|
||||
action.shortcut = *shortcut;
|
||||
}
|
||||
if (action.type == MouseActionType::Launch) action.executable = trimApplicationText(value(w, 742));
|
||||
if (isMouseVolumeStep(action.type))
|
||||
{
|
||||
const auto text = value(w, MouseVolumeStepField);
|
||||
if (text.empty() || text.size() > 3 || text.find_first_not_of(L"0123456789") != text.npos)
|
||||
throw std::runtime_error("Enter a volume step from 1 to 50.");
|
||||
action.volumeStep = std::stoi(text);
|
||||
const auto *previous = mouseBinding(editor.view->draft, profile, editor.event);
|
||||
const bool unchanged = previous && previous->type == action.type && previous->volumeStep == action.volumeStep;
|
||||
if (action.volumeStep < 1 || (action.volumeStep > MaxEditedMouseVolumeStep && !unchanged))
|
||||
throw std::runtime_error("Enter a volume step from 1 to 50.");
|
||||
}
|
||||
if (const auto *previous = mouseBinding(editor.view->draft, profile, editor.event))
|
||||
action.applications = previous->applications;
|
||||
check.mouseBindings[profile][event] = std::move(action);
|
||||
}
|
||||
validateSettings(check);
|
||||
editor.view->draft.mouseBindings = std::move(check.mouseBindings);
|
||||
fillMouseEvents(editor.parent, *editor.view, editor.event);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK mouseActionEditorProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *editor = reinterpret_cast<MouseActionEditor *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
if (!editor) return DefWindowProcW(w, msg, wp, lp);
|
||||
try
|
||||
{
|
||||
if (msg == WM_COMMAND)
|
||||
{
|
||||
switch (LOWORD(wp))
|
||||
{
|
||||
case 734:
|
||||
if (HIWORD(wp) == CBN_SELCHANGE)
|
||||
updateMouseActionFields(w);
|
||||
return 0;
|
||||
case 743:
|
||||
{
|
||||
wchar_t path[32768]{};
|
||||
OPENFILENAMEW picker{sizeof(picker)};
|
||||
picker.hwndOwner = w;
|
||||
picker.lpstrFilter = L"Applications (*.exe)\0*.exe\0";
|
||||
picker.lpstrFile = path;
|
||||
picker.nMaxFile = static_cast<DWORD>(std::size(path));
|
||||
picker.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
|
||||
if (GetOpenFileNameW(&picker)) value(w, 742, path);
|
||||
return 0;
|
||||
}
|
||||
case IDOK:
|
||||
saveMouseAction(w, *editor);
|
||||
DestroyWindow(w);
|
||||
return 0;
|
||||
case IDCANCEL:
|
||||
DestroyWindow(w);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (msg == WM_CLOSE) { DestroyWindow(w); return 0; }
|
||||
if (msg == WM_NCDESTROY)
|
||||
{
|
||||
auto parent = editor->parent;
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
|
||||
disposeFont(w);
|
||||
delete editor;
|
||||
if (IsWindow(parent))
|
||||
{
|
||||
EnableWindow(parent, TRUE);
|
||||
SetForegroundWindow(parent);
|
||||
SetFocus(GetDlgItem(parent, 733));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e) { error(w, e); }
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
|
||||
void showMouseActionEditor(HWND parent, SettingsView &view, int event)
|
||||
{
|
||||
if (event < 0 || event >= static_cast<int>(std::size(MouseEvents)) || !IsWindowEnabled(parent)) return;
|
||||
const auto profile = selectedMouseProfile(parent, view);
|
||||
const bool application = profile != GlobalMouseProfile;
|
||||
const auto *action = mouseBinding(view.draft, profile, event);
|
||||
const auto title = std::wstring(L"Edit mouse action");
|
||||
HWND w = makeWindow(*view.app, L"KSwitcher.MouseAction", title.c_str(), mouseActionEditorProc,
|
||||
480, MouseActionEditorHeight, 0, false, parent);
|
||||
auto *editor = new MouseActionEditor{parent, &view, profile, event};
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(editor));
|
||||
label(w, std::wstring(MouseEvents[event]).append(L" · ").append(mouseProfileName(profile)).c_str(),
|
||||
18, 14, 444);
|
||||
label(w, L"&Action", 18, 43, 330);
|
||||
windowLayoutState(w)->fixedClient = true;
|
||||
windowLayoutState(w)->rowCombo =
|
||||
control(w, L"COMBOBOX", L"", WS_TABSTOP | WS_VSCROLL | CBS_DROPDOWNLIST, 734, 18, 65, 444, 215);
|
||||
if (application)
|
||||
{
|
||||
SendDlgItemMessageW(w, 734, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(L"Inherit from Global"));
|
||||
SendDlgItemMessageW(w, 734, CB_SETITEMDATA, 0, -1);
|
||||
}
|
||||
for (size_t type = 0; type < std::size(MouseActions); ++type)
|
||||
{
|
||||
const auto row = SendDlgItemMessageW(w, 734, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(MouseActions[type]));
|
||||
if (row >= 0) SendDlgItemMessageW(w, 734, CB_SETITEMDATA, row, type);
|
||||
}
|
||||
control(w, L"STATIC", L"", 0, 744, 18, 101, 444, 21);
|
||||
hotkeyEdit(*view.app, w, 747, 18, 125, 444);
|
||||
edit(w, 742, 18, 125, 334);
|
||||
button(w, L"&Browse…", 743, 362, 124, 100);
|
||||
control(w, L"STATIC", L"Step (%)", 0, MouseVolumeStepLabel, 380, 43, 82, 22);
|
||||
auto stepField = edit(w, MouseVolumeStepField, 380, 65, 64, 25, ES_NUMBER);
|
||||
SendMessageW(stepField, EM_SETLIMITTEXT, 3, 0);
|
||||
INITCOMMONCONTROLSEX spinControls{sizeof(spinControls), ICC_UPDOWN_CLASS};
|
||||
InitCommonControlsEx(&spinControls);
|
||||
HWND spin = control(w, UPDOWN_CLASSW, L"", UDS_SETBUDDYINT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,
|
||||
MouseVolumeStepSpin, 444, 65, 18, 25);
|
||||
SendMessageW(spin, UDM_SETBUDDY, reinterpret_cast<WPARAM>(stepField), 0);
|
||||
SendMessageW(spin, UDM_SETRANGE32, 1, MaxEditedMouseVolumeStep);
|
||||
SetDlgItemInt(w, MouseVolumeStepField,
|
||||
action && isMouseVolumeStep(action->type) ? action->volumeStep : DefaultMouseVolumeStep, FALSE);
|
||||
button(w, L"&Apply", IDOK, 248, 154, 100);
|
||||
button(w, L"Cancel", IDCANCEL, 362, 154, 100);
|
||||
SendDlgItemMessageW(w, 734, CB_SETCURSEL, action ? static_cast<int>(action->type) + application : 0, 0);
|
||||
if (action && action->type == MouseActionType::Shortcut) value(w, 747, action->shortcut.format());
|
||||
if (action && action->type == MouseActionType::Launch) value(w, 742, action->executable);
|
||||
updateMouseActionFields(w);
|
||||
EnableWindow(parent, FALSE);
|
||||
showRequestedWindow(w);
|
||||
SetFocus(GetDlgItem(w, 734));
|
||||
}
|
||||
|
||||
constexpr int EditMouseEvent = 735, ResetMouseEvent = 746;
|
||||
constexpr auto MouseEventHint = L"Double-click to edit. Right-click for more actions.";
|
||||
|
||||
int mouseProfileRowAt(HWND list, POINT point)
|
||||
{
|
||||
LVHITTESTINFO hit{};
|
||||
hit.pt = point;
|
||||
const int item = ListView_SubItemHitTest(list, &hit);
|
||||
if (item >= 0) return item;
|
||||
RECT client{};
|
||||
if (!GetClientRect(list, &client) || !PtInRect(&client, point)) return -1;
|
||||
const int top = ListView_GetTopIndex(list);
|
||||
const int end = std::min(ListView_GetItemCount(list), top + ListView_GetCountPerPage(list) + 1);
|
||||
for (int row = top; row < end; ++row)
|
||||
{
|
||||
RECT bounds{};
|
||||
if (ListView_GetItemRect(list, row, &bounds, LVIR_BOUNDS) && point.y >= bounds.top && point.y < bounds.bottom)
|
||||
return row;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void showMouseListMenu(HWND list, POINT point, bool profiles)
|
||||
{
|
||||
HWND w = GetParent(list);
|
||||
int event;
|
||||
if (point.x == -1 && point.y == -1)
|
||||
{
|
||||
event = ListView_GetNextItem(list, -1, LVNI_SELECTED);
|
||||
RECT row{};
|
||||
if (event < 0 || !ListView_GetItemRect(list, event, &row, LVIR_BOUNDS)) return;
|
||||
point = {row.left, row.bottom};
|
||||
ClientToScreen(list, &point);
|
||||
}
|
||||
else
|
||||
{
|
||||
LVHITTESTINFO hit{};
|
||||
hit.pt = point;
|
||||
ScreenToClient(list, &hit.pt);
|
||||
event = ListView_SubItemHitTest(list, &hit);
|
||||
if (event < 0) return;
|
||||
}
|
||||
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
HMENU menu = CreatePopupMenu();
|
||||
if (!menu) return;
|
||||
if (profiles)
|
||||
{
|
||||
selectMouseProfile(w, event);
|
||||
const UINT flags = MF_STRING | (isNamedMouseProfile(selectedMouseProfile(w, view)) ? MF_ENABLED : MF_GRAYED);
|
||||
AppendMenuW(menu, flags, RenameMouseProfile, L"&Edit…");
|
||||
AppendMenuW(menu, flags, DeleteMouseProfile, L"&Delete…");
|
||||
}
|
||||
else
|
||||
{
|
||||
selectMouseEvent(w, event);
|
||||
const bool canReset = mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
|
||||
AppendMenuW(menu, MF_STRING, EditMouseEvent, L"&Edit…");
|
||||
AppendMenuW(menu, MF_STRING | (canReset ? MF_ENABLED : MF_GRAYED), ResetMouseEvent, L"&Reset to default");
|
||||
}
|
||||
const auto command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON,
|
||||
point.x, point.y, 0, w, nullptr);
|
||||
DestroyMenu(menu);
|
||||
if (command) SendMessageW(w, WM_COMMAND, command, 0);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK mouseEventListProc(HWND list, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR)
|
||||
{
|
||||
const bool profiles = GetDlgCtrlID(list) == 731;
|
||||
if (profiles && msg == WM_LBUTTONDBLCLK)
|
||||
{
|
||||
const int row = mouseProfileRowAt(list, {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)});
|
||||
if (row >= 0)
|
||||
{
|
||||
const auto owner = GetParent(list);
|
||||
selectMouseProfile(owner, row);
|
||||
SendMessageW(owner, WM_COMMAND, RenameMouseProfile, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_GETDLGCODE && wp == VK_RETURN) return DLGC_WANTMESSAGE;
|
||||
if (msg == WM_KEYDOWN && (wp == VK_RETURN || wp == VK_F2))
|
||||
{
|
||||
SendMessageW(GetParent(list), WM_COMMAND, profiles ? RenameMouseProfile : EditMouseEvent, 0);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_CONTEXTMENU)
|
||||
{
|
||||
showMouseListMenu(list, {GET_X_LPARAM(lp), GET_Y_LPARAM(lp)}, profiles);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY) RemoveWindowSubclass(list, mouseEventListProc, id);
|
||||
return DefSubclassProc(list, msg, wp, lp);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK mouseProfileNameProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
const HWND owner = GetWindow(w, GW_OWNER);
|
||||
auto *editor = reinterpret_cast<MouseProfileEditor *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
if (!editor) return DefWindowProcW(w, msg, wp, lp);
|
||||
try
|
||||
{
|
||||
if (msg == AppWindowPicked)
|
||||
{
|
||||
if (lp == 1)
|
||||
{
|
||||
const auto name = mouseApplicationName(reinterpret_cast<HWND>(wp));
|
||||
if (name.empty()) throw std::runtime_error("Drop the crosshair onto an application window.");
|
||||
value(w, ActionApplicationName, name);
|
||||
addActionApplication(w, *editor);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_DRAWITEM && (wp == AddActionApp || wp == RemoveActionApp || wp == BrowseActionApp || wp == PickActionApp))
|
||||
{
|
||||
drawActionApplicationIcon(w, *reinterpret_cast<const DRAWITEMSTRUCT *>(lp));
|
||||
return TRUE;
|
||||
}
|
||||
if (msg == WM_COMMAND)
|
||||
{
|
||||
if (LOWORD(wp) == BrowseActionApp) { browseActionApplication(w, *editor); return 0; }
|
||||
if (LOWORD(wp) == AddActionApp) { addActionApplication(w, *editor); return 0; }
|
||||
if (LOWORD(wp) == RemoveActionApp)
|
||||
{
|
||||
const auto row = SendDlgItemMessageW(w, ActionApplications, LB_GETCURSEL, 0, 0);
|
||||
if (row >= 0 && static_cast<size_t>(row) < editor->applications.size())
|
||||
{
|
||||
editor->applications.erase(std::next(editor->applications.begin(), row));
|
||||
fillActionApplications(w, *editor);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e) { error(w, e); return 0; }
|
||||
if (msg == WM_COMMAND && LOWORD(wp) == IDOK)
|
||||
{
|
||||
try
|
||||
{
|
||||
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(owner, GWLP_USERDATA));
|
||||
const auto &previous = editor->previous;
|
||||
const auto name = trimApplicationText(value(w, MouseProfileName));
|
||||
const auto duplicate = std::any_of(view.mouseProfiles.begin(), view.mouseProfiles.end(), [&](const auto &profile)
|
||||
{
|
||||
return profile != previous && lower(mouseProfileName(profile)) == lower(name);
|
||||
});
|
||||
if (duplicate) throw std::runtime_error("A profile with that name already exists.");
|
||||
const auto key = std::wstring(NamedMouseProfilePrefix) + name;
|
||||
Settings check;
|
||||
check.mouseBindings = view.draft.mouseBindings;
|
||||
check.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
||||
check.mouseProfileApplications = view.draft.mouseProfileApplications;
|
||||
if (!previous.empty() && key != previous)
|
||||
{
|
||||
auto profile = check.mouseBindings.extract(previous);
|
||||
if (profile.empty()) throw std::runtime_error("The profile no longer exists.");
|
||||
profile.key() = key;
|
||||
check.mouseBindings.insert(std::move(profile));
|
||||
if (check.disabledMouseProfiles.erase(previous)) check.disabledMouseProfiles.insert(key);
|
||||
}
|
||||
else check.mouseBindings.try_emplace(key);
|
||||
check.mouseProfileApplications.erase(previous);
|
||||
check.mouseProfileApplications[key] = editor->applications;
|
||||
for (auto &[event, action] : check.mouseBindings.at(key)) action.applications.clear();
|
||||
validateSettings(check);
|
||||
view.draft.mouseBindings = std::move(check.mouseBindings);
|
||||
view.draft.disabledMouseProfiles = std::move(check.disabledMouseProfiles);
|
||||
view.draft.mouseProfileApplications = std::move(check.mouseProfileApplications);
|
||||
fillMouseProfiles(owner, view);
|
||||
const auto selected = std::find(view.mouseProfiles.begin(), view.mouseProfiles.end(), key);
|
||||
selectMouseProfile(owner, static_cast<int>(selected - view.mouseProfiles.begin()));
|
||||
fillMouseEvents(owner, view);
|
||||
updateSettingsState(owner, &view);
|
||||
DestroyWindow(w);
|
||||
}
|
||||
catch (const std::exception &e) { error(w, e); }
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_CLOSE || (msg == WM_COMMAND && LOWORD(wp) == IDCANCEL))
|
||||
{
|
||||
DestroyWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_NCDESTROY)
|
||||
{
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
|
||||
delete editor;
|
||||
disposeFont(w);
|
||||
EnableWindow(owner, TRUE);
|
||||
SetForegroundWindow(owner);
|
||||
}
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
|
||||
void showMouseProfileName(HWND owner, SettingsView &view, bool renaming)
|
||||
{
|
||||
HWND w = makeWindow(*view.app, L"KSwitcher.MouseProfileName", renaming ? L"Edit profile" : L"Add profile", mouseProfileNameProc,
|
||||
480, 296, 0, false, owner);
|
||||
windowLayoutState(w)->fixedClient = true;
|
||||
const auto previous = renaming ? selectedMouseProfile(owner, view) : std::wstring{};
|
||||
auto *editor = new MouseProfileEditor{previous, renaming ? profileApplications(view.draft, previous) : std::set<std::wstring>{}};
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(editor));
|
||||
label(w, L"&Name", 18, 12, 444);
|
||||
edit(w, MouseProfileName, 18, 36, 444);
|
||||
if (renaming) value(w, MouseProfileName, mouseProfileName(previous));
|
||||
label(w, L"Applications (empty list means all applications)", 18, 75, 444);
|
||||
control(w, L"LISTBOX", L"", WS_TABSTOP | WS_BORDER | WS_VSCROLL | LBS_NOINTEGRALHEIGHT,
|
||||
ActionApplications, 18, 100, 404, 96);
|
||||
control(w, L"BUTTON", L"&Remove application", WS_TABSTOP | BS_OWNERDRAW, RemoveActionApp, 430, 100, 32, 28);
|
||||
HWND appName = edit(w, ActionApplicationName, 18, 207, 324);
|
||||
SendMessageW(appName, EM_SETCUEBANNER, FALSE, reinterpret_cast<LPARAM>(L"Application executable (e.g. chrome.exe)"));
|
||||
HWND picker = control(w, L"BUTTON", L"Drag to application window", WS_TABSTOP | BS_OWNERDRAW,
|
||||
PickActionApp, 350, 205, 32, 28);
|
||||
SetWindowSubclass(picker, appWindowPickerProc, 1, reinterpret_cast<DWORD_PTR>(&editor->pickerOutline));
|
||||
control(w, L"BUTTON", L"&Browse for application", WS_TABSTOP | BS_OWNERDRAW, BrowseActionApp, 390, 205, 32, 28);
|
||||
control(w, L"BUTTON", L"Add a&pplication", WS_TABSTOP | BS_OWNERDRAW, AddActionApp, 430, 205, 32, 28);
|
||||
HWND tips = CreateWindowExW(WS_EX_TOPMOST, TOOLTIPS_CLASSW, nullptr, WS_POPUP | TTS_ALWAYSTIP | TTS_NOPREFIX,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, w, nullptr,
|
||||
view.app->instance, nullptr);
|
||||
addActionApplicationTooltip(tips, w, RemoveActionApp, L"Remove application");
|
||||
addActionApplicationTooltip(tips, w, AddActionApp, L"Add application");
|
||||
addActionApplicationTooltip(tips, w, BrowseActionApp, L"Browse for an executable or shortcut");
|
||||
addActionApplicationTooltip(tips, w, PickActionApp, L"Drag to an application window");
|
||||
fillActionApplications(w, *editor);
|
||||
control(w, L"BUTTON", renaming ? L"&Apply" : L"&Add", WS_TABSTOP | BS_DEFPUSHBUTTON, IDOK, 248, 251, 100, 28);
|
||||
button(w, L"Cancel", IDCANCEL, 362, 251, 100);
|
||||
EnableWindow(owner, FALSE);
|
||||
showRequestedWindow(w);
|
||||
SetFocus(GetDlgItem(w, MouseProfileName));
|
||||
}
|
||||
|
||||
bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
|
||||
{
|
||||
const int id = LOWORD(wp);
|
||||
if (id == AddMouseProfile) { showMouseProfileName(w, view, false); return true; }
|
||||
if (id == RenameMouseProfile || id == DeleteMouseProfile)
|
||||
{
|
||||
const auto profile = selectedMouseProfile(w, view);
|
||||
if (!isNamedMouseProfile(profile)) return true;
|
||||
if (id == RenameMouseProfile) showMouseProfileName(w, view, true);
|
||||
else if (MessageBoxW(w, (L"Delete " + mouseProfileName(profile) + L" and all its actions?").c_str(),
|
||||
L"Delete profile", MB_YESNO | MB_ICONQUESTION | MB_DEFBUTTON2) == IDYES)
|
||||
{
|
||||
view.draft.disabledMouseProfiles.erase(profile);
|
||||
view.draft.mouseBindings.erase(profile);
|
||||
view.draft.mouseProfileApplications.erase(profile);
|
||||
fillMouseProfiles(w, view);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (id != EditMouseEvent && id != ResetMouseEvent) return false;
|
||||
const int event = ListView_GetNextItem(GetDlgItem(w, 733), -1, LVNI_SELECTED);
|
||||
if (event < 0) return true;
|
||||
if (id == EditMouseEvent) showMouseActionEditor(w, view, event);
|
||||
else if (auto profile = view.draft.mouseBindings.find(selectedMouseProfile(w, view));
|
||||
profile != view.draft.mouseBindings.end())
|
||||
{
|
||||
profile->second.erase(MouseEvents[event]);
|
||||
if (profile->second.empty() && !isNamedMouseProfile(profile->first)) view.draft.mouseBindings.erase(profile);
|
||||
fillMouseEvents(w, view, event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void scaleMouseTableColumns(HWND w, int previousDpi, int nextDpi)
|
||||
{
|
||||
for (int id : {731, 733})
|
||||
{
|
||||
const auto list = GetDlgItem(w, id);
|
||||
const int count = Header_GetItemCount(ListView_GetHeader(list));
|
||||
for (int column = 0; column < count; ++column)
|
||||
ListView_SetColumnWidth(list, column, MulDiv(ListView_GetColumnWidth(list, column), nextDpi, previousDpi));
|
||||
}
|
||||
}
|
||||
|
||||
void createMousePage(HWND w, SettingsView &view)
|
||||
{
|
||||
constexpr int profilesLeft = 18, actionsLeft = 369, tableWidth = 333;
|
||||
constexpr int profilesRight = profilesLeft + tableWidth;
|
||||
label(w, L"Profiles", 18, 64, 120);
|
||||
label(w, L"Mouse events", actionsLeft, 64, tableWidth);
|
||||
control(w, L"BUTTON", L"Delete profile", WS_TABSTOP | BS_OWNERDRAW, DeleteMouseProfile, profilesRight - 80, 61, 24, 24);
|
||||
control(w, L"BUTTON", L"Edit profile", WS_TABSTOP | BS_OWNERDRAW, RenameMouseProfile, profilesRight - 52, 61, 24, 24);
|
||||
control(w, L"BUTTON", L"Add profile", WS_TABSTOP | BS_OWNERDRAW, AddMouseProfile, profilesRight - 24, 61, 24, 24);
|
||||
HWND tips = CreateWindowExW(WS_EX_TOPMOST, TOOLTIPS_CLASSW, nullptr, WS_POPUP | TTS_ALWAYSTIP | TTS_NOPREFIX,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, w, nullptr,
|
||||
view.app->instance, nullptr);
|
||||
addActionApplicationTooltip(tips, w, AddMouseProfile, L"Add profile");
|
||||
addActionApplicationTooltip(tips, w, RenameMouseProfile, L"Edit profile");
|
||||
addActionApplicationTooltip(tips, w, DeleteMouseProfile, L"Delete profile");
|
||||
constexpr int listTop = 90, listHeight = 224;
|
||||
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES};
|
||||
InitCommonControlsEx(&controls);
|
||||
constexpr DWORD listStyle = WS_TABSTOP | WS_BORDER | LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER;
|
||||
HWND profiles = control(w, WC_LISTVIEWW, L"", listStyle, 731, profilesLeft, listTop, tableWidth, listHeight);
|
||||
SetWindowSubclass(profiles, mouseEventListProc, 3, 0);
|
||||
ListView_SetExtendedListViewStyle(profiles, LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER | LVS_EX_LABELTIP | LVS_EX_CHECKBOXES);
|
||||
wchar_t nameTitle[] = L"Name", activeTitle[] = L"Active", applicationsTitle[] = L"Applications";
|
||||
LVCOLUMNW profileColumn{LVCF_TEXT | LVCF_WIDTH};
|
||||
profileColumn.pszText = activeTitle;
|
||||
profileColumn.cx = scale(w, 56);
|
||||
ListView_InsertColumn(profiles, 0, &profileColumn);
|
||||
profileColumn.pszText = nameTitle;
|
||||
profileColumn.cx = scale(w, 100);
|
||||
ListView_InsertColumn(profiles, 1, &profileColumn);
|
||||
profileColumn.pszText = applicationsTitle;
|
||||
profileColumn.cx = scale(w, tableWidth - 160);
|
||||
ListView_InsertColumn(profiles, 2, &profileColumn);
|
||||
int order[]{1, 0, 2};
|
||||
ListView_SetColumnOrderArray(profiles, 3, order);
|
||||
HWND list = control(w, WC_LISTVIEWW, L"", WS_TABSTOP | WS_BORDER | LVS_REPORT |
|
||||
LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_NOSORTHEADER, 733, actionsLeft, listTop, tableWidth, listHeight);
|
||||
SetWindowSubclass(list, mouseEventListProc, 3, 0);
|
||||
ListView_SetExtendedListViewStyle(list, LVS_EX_FULLROWSELECT | LVS_EX_DOUBLEBUFFER | LVS_EX_LABELTIP);
|
||||
wchar_t actionTitle[] = L"Action";
|
||||
LVCOLUMNW column{LVCF_TEXT | LVCF_WIDTH};
|
||||
column.pszText = nameTitle;
|
||||
column.cx = scale(w, 116);
|
||||
ListView_InsertColumn(list, 0, &column);
|
||||
column.pszText = actionTitle;
|
||||
column.cx = scale(w, tableWidth - 120);
|
||||
ListView_InsertColumn(list, 1, &column);
|
||||
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
|
||||
{
|
||||
LVITEMW item{LVIF_TEXT};
|
||||
item.iItem = event;
|
||||
item.pszText = const_cast<wchar_t *>(MouseEvents[event]);
|
||||
ListView_InsertItem(list, &item);
|
||||
}
|
||||
control(w, L"STATIC", MouseEventHint, 0, 740, actionsLeft, 324, tableWidth, 22);
|
||||
control(w, L"BUTTON", L"Test mouse buttons here", WS_TABSTOP | BS_PUSHBUTTON,
|
||||
737, profilesLeft, listTop + listHeight + 10, tableWidth, 56);
|
||||
SetWindowSubclass(GetDlgItem(w, 737), mouseButtonCaptureProc, 1, 0);
|
||||
control(w, L"STATIC", L"Window event: waiting for a press", 0, 738, actionsLeft, 370, tableWidth, 20);
|
||||
view.rawMouseRegistered = registerMouseTestInput(w);
|
||||
control(w, L"STATIC", view.rawMouseRegistered ? L"Raw Input: waiting for a press" : L"Raw Input: unavailable",
|
||||
0, 739, actionsLeft, 394, tableWidth, 20);
|
||||
}
|
||||
Reference in new issue
Block a user