Release KSwitcher v0.6.65
This commit is contained in:
1 parent
14f33012a3
commit
8a65e7f7fb
21 files changed
+570
-111
No files matched your search
+56
-23
@@ -733,6 +733,10 @@ Bytes App::lanCommand(const Bytes &command)
|
||||
return reply;
|
||||
}
|
||||
void App::applySettings(const Settings &requested)
|
||||
{
|
||||
applySettings(requested, true);
|
||||
}
|
||||
void App::applySettings(const Settings &requested, bool validateOcrInstallation)
|
||||
{
|
||||
Settings s = requested;
|
||||
std::set<std::wstring> present, removed(s.snippetTombstones.begin(), s.snippetTombstones.end());
|
||||
@@ -745,7 +749,7 @@ void App::applySettings(const Settings &requested)
|
||||
bool autostartChanged = false;
|
||||
AutoCorrector predictor;
|
||||
validateSettings(s);
|
||||
validateTesseract(s);
|
||||
if (validateOcrInstallation && ocrProfile(s) != ocrProfile(settings)) validateTesseract(s);
|
||||
if (s.autoCorrect)
|
||||
{
|
||||
predictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", s.autoLanguages);
|
||||
@@ -849,6 +853,8 @@ void App::resetLanInput()
|
||||
chatSwitchHook = {};
|
||||
pendingMouseClicks = {};
|
||||
mouseActions.clear();
|
||||
mouseWheelTarget = nullptr;
|
||||
mouseWheelRemainder = 0;
|
||||
resetInput();
|
||||
}
|
||||
void App::resetInput()
|
||||
@@ -1399,6 +1405,8 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
auto &app = *current;
|
||||
const auto &mouse = *reinterpret_cast<const MSLLHOOKSTRUCT *>(lp);
|
||||
const int button = mouse.flags & LLMHF_INJECTED ? -1 : mouseButton(UINT(wp), HIWORD(mouse.mouseData));
|
||||
const auto wheelDelta = static_cast<SHORT>(HIWORD(mouse.mouseData));
|
||||
const int wheel = mouse.flags & LLMHF_INJECTED ? -1 : mouseWheelEvent(UINT(wp), wheelDelta);
|
||||
const bool down = mouseButtonDown(UINT(wp));
|
||||
if (button >= 0 && app.mousePresses[button].held)
|
||||
{
|
||||
@@ -1430,7 +1438,8 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
if (app.lanService->takeInputChange()) app.resetLanInput();
|
||||
if (consumed) return 1;
|
||||
}
|
||||
if (button >= 0 && down && !app.settings.mouseBindings.empty() && !app.busy && !app.suspended &&
|
||||
if (((button >= 0 && down) || mouseEventEnabled(app.settings, wheel)) &&
|
||||
!app.settings.mouseBindings.empty() && !app.busy && !app.suspended &&
|
||||
!app.installingUpdate && !app.hotkeyCapture && modifiersReleased())
|
||||
{
|
||||
const auto hit = WindowFromPoint(mouse.pt);
|
||||
@@ -1442,22 +1451,26 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
const bool shellSurface = target == GetShellWindow() || wcscmp(windowClass, L"Progman") == 0 ||
|
||||
wcscmp(windowClass, L"WorkerW") == 0 || wcscmp(windowClass, L"Shell_TrayWnd") == 0 ||
|
||||
wcscmp(windowClass, L"Shell_SecondaryTrayWnd") == 0;
|
||||
const auto *global = mouseBinding(app.settings, GlobalMouseProfile, button);
|
||||
const bool globalLaunch = button >= 2 && global && global->type == MouseActionType::Launch &&
|
||||
const int event = wheel >= 0 ? wheel : button;
|
||||
const auto *global = mouseBinding(app.settings, GlobalMouseProfile, event);
|
||||
const bool globalLaunch = event >= 2 && global && global->type == MouseActionType::Launch &&
|
||||
mouseActionApplies(*global, name);
|
||||
const bool restricted = ownWindow || shellSurface || name.empty();
|
||||
const bool testButton = name == L"kswitcher.config" && hit == GetDlgItem(target, 737);
|
||||
const auto *fallback = restricted ? effectiveMouseAction(app.settings, L"", button, false) : nullptr;
|
||||
wchar_t targetClass[64]{};
|
||||
if (ownWindow) GetClassNameW(target, targetClass, std::size(targetClass));
|
||||
const bool testButton = ownWindow && (hit == GetDlgItem(target, 737) ||
|
||||
wcscmp(targetClass, L"KSwitcher.AddMouseEvent") == 0);
|
||||
const auto *fallback = restricted ? effectiveMouseAction(app.settings, L"", event, false) : nullptr;
|
||||
const auto *restrictedAction = isMouseVolumeAction(fallback) ? fallback : globalLaunch ? global : nullptr;
|
||||
const bool eligible = target && !testButton && (!restricted || (button >= 2 && restrictedAction)) &&
|
||||
(button >= 2 || target == GetForegroundWindow());
|
||||
const bool eligible = target && !testButton && (!restricted || (event >= 2 && restrictedAction)) &&
|
||||
(event >= 2 || target == GetForegroundWindow());
|
||||
if (eligible)
|
||||
{
|
||||
const bool foreground = target == GetForegroundWindow();
|
||||
bool singleScoped{}, doubleScoped{};
|
||||
auto *single = restricted ? restrictedAction :
|
||||
effectiveMouseAction(app.settings, name, button, foreground, &singleScoped);
|
||||
auto *doubleClick = !restricted && button < 3 ?
|
||||
effectiveMouseAction(app.settings, name, event, foreground, &singleScoped);
|
||||
auto *doubleClick = wheel < 0 && !restricted && button < 3 ?
|
||||
effectiveMouseAction(app.settings, name, 5 + button, foreground, &doubleScoped) : nullptr;
|
||||
if (!foreground)
|
||||
{
|
||||
@@ -1465,25 +1478,45 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
if (doubleClick && doubleClick->type == MouseActionType::Shortcut) doubleClick = nullptr;
|
||||
}
|
||||
const auto now = GetTickCount64();
|
||||
auto &pending = app.pendingMouseClicks[button];
|
||||
const bool second = doubleClick && pending.target == target &&
|
||||
doubleMouseClick(pending.point, mouse.pt, now - pending.started);
|
||||
if (second) pending = {};
|
||||
else if (pending.target) app.queueMouseAction(std::exchange(pending, {}));
|
||||
const auto *action = second ? doubleClick : single;
|
||||
if (action || doubleClick)
|
||||
app.mousePresses[button] = {target, mouse.pt, now, action ? *action : MouseAction{},
|
||||
true, doubleClick && !second};
|
||||
if (action)
|
||||
if (wheel >= 0 && single)
|
||||
{
|
||||
if (second ? doubleScoped : singleScoped) app.mousePresses[button].action.applications.insert(name);
|
||||
if (isMouseVolumeStep(action->type))
|
||||
app.beginMouseVolume(button);
|
||||
if (app.mouseWheelTarget != target || app.mouseWheelSignal != wheel)
|
||||
app.mouseWheelRemainder = 0;
|
||||
app.mouseWheelTarget = target;
|
||||
app.mouseWheelSignal = wheel;
|
||||
const int steps = mouseWheelSteps(app.mouseWheelRemainder, wheelDelta);
|
||||
for (int i = 0; i < steps; ++i)
|
||||
app.queueMouseAction({target, mouse.pt, now, *single});
|
||||
app.resetInput();
|
||||
return 1;
|
||||
}
|
||||
if (wheel < 0)
|
||||
{
|
||||
auto &pending = app.pendingMouseClicks[button];
|
||||
const bool second = doubleClick && pending.target == target &&
|
||||
doubleMouseClick(pending.point, mouse.pt, now - pending.started);
|
||||
if (second) pending = {};
|
||||
else if (pending.target) app.queueMouseAction(std::exchange(pending, {}));
|
||||
const auto *action = second ? doubleClick : single;
|
||||
if (action || doubleClick)
|
||||
app.mousePresses[button] = {target, mouse.pt, now, action ? *action : MouseAction{},
|
||||
true, doubleClick && !second};
|
||||
if (action)
|
||||
{
|
||||
if (second ? doubleScoped : singleScoped) app.mousePresses[button].action.applications.insert(name);
|
||||
if (isMouseVolumeStep(action->type))
|
||||
app.beginMouseVolume(button);
|
||||
app.resetInput();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (wheel >= 0)
|
||||
{
|
||||
app.mouseWheelTarget = nullptr;
|
||||
app.mouseWheelRemainder = 0;
|
||||
}
|
||||
// Remote clicks invalidate a typing range just like local clicks.
|
||||
if (down || wp == WM_MOUSEWHEEL || wp == WM_MOUSEHWHEEL)
|
||||
{
|
||||
|
||||
@@ -106,6 +106,7 @@ class App
|
||||
private:
|
||||
friend struct InputRegressionAccess;
|
||||
void releaseIdleMemory(ULONGLONG now);
|
||||
void applySettings(const Settings &, bool validateOcrInstallation);
|
||||
HHOOK keyboardHook{}, mouseHook{};
|
||||
struct MousePress
|
||||
{
|
||||
@@ -118,6 +119,8 @@ class App
|
||||
std::array<MousePress, std::size(MouseButtonNames)> mousePresses{};
|
||||
std::array<MousePress, std::size(MouseButtonNames)> pendingMouseClicks{};
|
||||
std::deque<MousePress> mouseActions;
|
||||
HWND mouseWheelTarget{};
|
||||
int mouseWheelSignal{-1}, mouseWheelRemainder{};
|
||||
void queueMouseAction(MousePress);
|
||||
void flushMouseClicks();
|
||||
void beginMouseVolume(int button);
|
||||
|
||||
+2
-1
@@ -79,7 +79,8 @@ void App::pollSync()
|
||||
if (encodeSettings(next) != snapshot)
|
||||
{
|
||||
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
|
||||
applySettings(next);
|
||||
// A restored OCR configuration can precede the optional local installation.
|
||||
applySettings(next, false);
|
||||
syncStatus += L" Changes applied on this PC; local backup saved.";
|
||||
}
|
||||
else syncStatus += result.conflicts.empty() ? L" This PC's settings are already up to date; no local changes needed." :
|
||||
|
||||
@@ -101,6 +101,7 @@ struct Settings
|
||||
std::map<std::wstring, std::vector<std::wstring>> chatCycles;
|
||||
std::map<std::wstring, AppLanguageSet> appLanguageSets;
|
||||
std::map<std::wstring, std::map<std::wstring, MouseAction>> mouseBindings;
|
||||
std::map<std::wstring, std::wstring> customMouseEvents;
|
||||
std::set<std::wstring> disabledMouseProfiles;
|
||||
std::map<std::wstring, std::set<std::wstring>> mouseProfileApplications;
|
||||
std::vector<std::wstring> excluded{L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
||||
|
||||
+49
-1
@@ -6,7 +6,54 @@ namespace ks
|
||||
{
|
||||
inline constexpr const wchar_t *MouseButtonNames[] = {L"Left", L"Right", L"Middle", L"XButton1", L"XButton2"};
|
||||
inline constexpr const wchar_t *MouseEvents[] = {L"Left", L"Right", L"Middle", L"XButton1", L"XButton2",
|
||||
L"Double left", L"Double right", L"Double middle"};
|
||||
L"Double left", L"Double right", L"Double middle",
|
||||
L"Wheel up", L"Wheel down", L"Wheel left", L"Wheel right"};
|
||||
inline constexpr int BuiltInMouseEventCount = 8;
|
||||
|
||||
inline int mouseWheelEvent(UINT message, SHORT delta)
|
||||
{
|
||||
if (!delta) return -1;
|
||||
if (message == WM_MOUSEWHEEL) return BuiltInMouseEventCount + (delta < 0);
|
||||
if (message == WM_MOUSEHWHEEL) return BuiltInMouseEventCount + 2 + (delta > 0);
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline int rawMouseSignal(const RAWMOUSE &mouse)
|
||||
{
|
||||
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};
|
||||
int signal = -1;
|
||||
for (int button = 0; button < static_cast<int>(std::size(downFlags)); ++button)
|
||||
{
|
||||
if (!(mouse.usButtonFlags & downFlags[button])) continue;
|
||||
if (signal >= 0) return -1;
|
||||
signal = button;
|
||||
}
|
||||
const auto wheel = mouse.usButtonFlags & (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL);
|
||||
if (!wheel) return signal;
|
||||
if (signal >= 0 || wheel == (RI_MOUSE_WHEEL | RI_MOUSE_HWHEEL)) return -1;
|
||||
return mouseWheelEvent(wheel == RI_MOUSE_WHEEL ? WM_MOUSEWHEEL : WM_MOUSEHWHEEL,
|
||||
static_cast<SHORT>(mouse.usButtonData));
|
||||
}
|
||||
|
||||
inline int mouseWheelSteps(int &remainder, SHORT delta)
|
||||
{
|
||||
const int total = remainder + std::abs(static_cast<int>(delta));
|
||||
remainder = total % WHEEL_DELTA;
|
||||
return total / WHEEL_DELTA;
|
||||
}
|
||||
|
||||
inline bool mouseEventEnabled(const Settings &settings, int event)
|
||||
{
|
||||
return event >= 0 && event < static_cast<int>(std::size(MouseEvents)) &&
|
||||
(event < BuiltInMouseEventCount || settings.customMouseEvents.contains(MouseEvents[event]));
|
||||
}
|
||||
|
||||
inline std::wstring mouseEventName(const Settings &settings, int event)
|
||||
{
|
||||
auto name = settings.customMouseEvents.find(MouseEvents[event]);
|
||||
return name == settings.customMouseEvents.end() ? MouseEvents[event] : name->second;
|
||||
}
|
||||
inline constexpr const wchar_t *MouseActions[] = {L"Standard behavior", L"Send key combination", L"Back", L"Forward",
|
||||
L"Volume up", L"Volume down", L"Mute", L"Launch application"};
|
||||
inline constexpr wchar_t GlobalMouseProfile[] = L"*";
|
||||
@@ -67,6 +114,7 @@ inline const MouseAction *effectiveMouseAction(const Settings &settings, const s
|
||||
bool foreground = true, bool *profileScoped = nullptr)
|
||||
{
|
||||
if (profileScoped) *profileScoped = false;
|
||||
if (!mouseEventEnabled(settings, event)) return nullptr;
|
||||
auto *action = mouseBinding(settings, app, event);
|
||||
if (!action)
|
||||
{
|
||||
|
||||
+20
-1
@@ -108,6 +108,15 @@ static void validateMouseProfileConflicts(const Settings &s)
|
||||
|
||||
void validateSettings(const Settings &s)
|
||||
{
|
||||
std::set<std::wstring> eventNames;
|
||||
for (int event = 0; event < BuiltInMouseEventCount; ++event) eventNames.insert(lower(MouseEvents[event]));
|
||||
for (const auto &[signal, name] : s.customMouseEvents)
|
||||
{
|
||||
if (std::find(std::begin(MouseEvents) + BuiltInMouseEventCount, std::end(MouseEvents), signal) == std::end(MouseEvents) ||
|
||||
name.empty() || name.size() > 80 || name.find_first_of(L"\r\n\t") != name.npos ||
|
||||
!eventNames.insert(lower(name)).second)
|
||||
throw std::runtime_error("Enter a unique event name (up to 80 characters) for a supported signal.");
|
||||
}
|
||||
if (s.mouseBindings.size() > 1000) throw std::runtime_error("Too many mouse profiles.");
|
||||
for (const auto &[app, bindings] : s.mouseBindings)
|
||||
{
|
||||
@@ -124,7 +133,8 @@ void validateSettings(const Settings &s)
|
||||
if (action.applications.size() > 1000 ||
|
||||
!std::all_of(action.applications.begin(), action.applications.end(), validMouseApplication))
|
||||
throw std::runtime_error("Enter valid target application executable names.");
|
||||
if (std::find(std::begin(MouseEvents), std::end(MouseEvents), event) == std::end(MouseEvents) ||
|
||||
const auto signal = std::find(std::begin(MouseEvents), std::end(MouseEvents), event);
|
||||
if (!mouseEventEnabled(s, static_cast<int>(signal - std::begin(MouseEvents))) ||
|
||||
static_cast<size_t>(action.type) >= std::size(MouseActions))
|
||||
throw std::runtime_error("Invalid mouse event or action.");
|
||||
if (isMouseVolumeStep(action.type) && (action.volumeStep < 1 || action.volumeStep > 100))
|
||||
@@ -382,6 +392,13 @@ Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
||||
s.appLanguageSets[wide(name)] = std::move(set);
|
||||
}
|
||||
}
|
||||
if (j.contains("customMouseEvents"))
|
||||
{
|
||||
const auto &events = j.at("customMouseEvents");
|
||||
if (!events.is_object()) throw std::runtime_error("Invalid custom mouse events.");
|
||||
for (const auto &[signal, name] : events.items())
|
||||
s.customMouseEvents.emplace(wide(signal), wide(name.get<std::string>()));
|
||||
}
|
||||
if (j.contains("mouseBindings"))
|
||||
{
|
||||
const auto &profiles = j.at("mouseBindings");
|
||||
@@ -520,6 +537,8 @@ Bytes encodeSettings(const Settings &s)
|
||||
for (const auto &id : set.languages) item["languages"].push_back(utf8(id));
|
||||
for (const auto &app : set.applications) item["applications"].push_back(utf8(app));
|
||||
}
|
||||
j["customMouseEvents"] = Json::object();
|
||||
for (const auto &[signal, name] : s.customMouseEvents) j["customMouseEvents"][utf8(signal)] = utf8(name);
|
||||
j["mouseBindings"] = Json::object();
|
||||
for (const auto &[app, bindings] : s.mouseBindings)
|
||||
{
|
||||
|
||||
+19
-6
@@ -216,7 +216,20 @@ std::unique_ptr<SyncProvider> makeSyncProvider(const SyncConfig &c, SyncTranspor
|
||||
{
|
||||
return std::make_unique<OpenBao>(c, std::move(transport));
|
||||
}
|
||||
namespace { Json canonicalProfile(Json); }
|
||||
namespace {
|
||||
Json canonicalProfile(Json);
|
||||
constexpr const char *PreviouslyLocalSettings[] = {
|
||||
"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
|
||||
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock",
|
||||
"appLanguageSets", "mouseBindings", "disabledMouseProfiles", "mouseProfileApplications", "customMouseEvents"
|
||||
};
|
||||
|
||||
void preserveMissingSettings(Json &profile, const Json &fallback)
|
||||
{
|
||||
for (auto key : PreviouslyLocalSettings)
|
||||
if (!profile.contains(key) && fallback.contains(key)) profile[key] = fallback.at(key);
|
||||
}
|
||||
}
|
||||
Bytes sharedSyncSettings(const Settings &s)
|
||||
{
|
||||
auto j = parse(encodeSettings(s));
|
||||
@@ -226,14 +239,12 @@ Bytes sharedSyncSettings(const Settings &s)
|
||||
std::sort(bindings.begin(), bindings.end(), [](const Json &a, const Json &b) {
|
||||
return a.at("layoutId").get<std::string>() < b.at("layoutId").get<std::string>();
|
||||
});
|
||||
for (auto key : {"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
|
||||
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock",
|
||||
"appLanguageSets", "mouseBindings", "disabledMouseProfiles", "mouseProfileApplications"}) j.erase(key);
|
||||
return bytes(canonicalProfile(j));
|
||||
}
|
||||
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
|
||||
{
|
||||
auto j = parse(shared), original = parse(encodeSettings(local));
|
||||
preserveMissingSettings(j, original);
|
||||
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
|
||||
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
|
||||
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = false;
|
||||
@@ -245,7 +256,7 @@ Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const s
|
||||
if (!j.is_object() || j.value("format", "") != "kswitcher-settings" || j.value("schemaVersion", 0) != 1)
|
||||
throw std::runtime_error("Unsupported synchronized settings format.");
|
||||
for (auto it = j.begin(); it != j.end(); ++it)
|
||||
if (!allowed.contains(it.key())) throw std::runtime_error("Remote profile contains unsupported or local-only fields.");
|
||||
if (!allowed.contains(it.key())) throw std::runtime_error("Remote profile contains unsupported fields.");
|
||||
if (j.size() != allowed.size()) throw std::runtime_error("Remote profile is incomplete.");
|
||||
std::set<std::string> installed;
|
||||
for (const auto &layout : layouts) installed.insert(utf8(layout.id));
|
||||
@@ -307,7 +318,9 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
|
||||
else
|
||||
{
|
||||
auto profile = parse(action == SyncAction::Download ? remote.settings : mergeLocal);
|
||||
if (!profile.contains("chatCycles")) profile["chatCycles"] = parse(local).value("chatCycles", Json::object());
|
||||
const auto current = parse(local);
|
||||
if (!profile.contains("chatCycles")) profile["chatCycles"] = current.value("chatCycles", Json::object());
|
||||
preserveMissingSettings(profile, current);
|
||||
auto selected = bytes(normalizeProfile(profile));
|
||||
merged = {selected, selected, selected, {}};
|
||||
}
|
||||
|
||||
@@ -72,6 +72,14 @@ Json flatProfile(Json j)
|
||||
else ++profile;
|
||||
}
|
||||
}
|
||||
if (j.contains("customMouseEvents"))
|
||||
{
|
||||
auto &events = j.at("customMouseEvents");
|
||||
for (auto event = events.begin(); event != events.end();)
|
||||
if (event.value().is_null()) event = events.erase(event);
|
||||
else ++event;
|
||||
if (events.is_null()) j.erase("customMouseEvents");
|
||||
}
|
||||
Json snippets = Json::array(), deleted = Json::array();
|
||||
for (const auto &[id, item] : j.at("snippets").items())
|
||||
{
|
||||
@@ -215,6 +223,10 @@ SyncMerge mergeSyncProfile(const Bytes &base, const Bytes &local, const Bytes &r
|
||||
// client's chat profiles until both copies understand this field.
|
||||
if (!remoteProfile.contains("chatCycles")) remoteProfile["chatCycles"] = localProfile.value("chatCycles", Json::object());
|
||||
if (!localProfile.contains("chatCycles")) localProfile["chatCycles"] = remoteProfile.at("chatCycles");
|
||||
// Older server backups omit the newly synchronized fields. An explicit
|
||||
// empty value from a current client still means the user cleared it.
|
||||
preserveMissingSettings(remoteProfile, localProfile);
|
||||
preserveMissingSettings(localProfile, remoteProfile);
|
||||
auto l = treeProfile(localProfile), r = treeProfile(remoteProfile);
|
||||
// With no common history, union independent entries; differing scalar values
|
||||
// remain on their respective devices until the user resolves the conflict.
|
||||
|
||||
+2
-2
@@ -1766,7 +1766,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
switch (msg)
|
||||
{
|
||||
case WM_DRAWITEM:
|
||||
if (wp == AddMouseProfile || wp == RenameMouseProfile || wp == DeleteMouseProfile)
|
||||
if (wp == AddMouseProfile || wp == RenameMouseProfile || wp == DeleteMouseProfile || wp == AddMouseEvent)
|
||||
{
|
||||
drawActionApplicationIcon(w, *reinterpret_cast<const DRAWITEMSTRUCT *>(lp));
|
||||
return TRUE;
|
||||
@@ -1801,7 +1801,7 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
if (((NMHDR *)lp)->idFrom == 733 && ((NMHDR *)lp)->code == NM_DBLCLK)
|
||||
{
|
||||
const auto &click = *reinterpret_cast<const NMITEMACTIVATE *>(lp);
|
||||
showMouseActionEditor(w, *v, click.iItem);
|
||||
showMouseActionEditor(w, *v, mouseEventAtRow(click.hdr.hwndFrom, click.iItem));
|
||||
return 0;
|
||||
}
|
||||
if (((NMHDR *)lp)->idFrom == 260 && ((NMHDR *)lp)->code == TCN_SELCHANGE)
|
||||
|
||||
+238
-19
@@ -5,6 +5,7 @@ std::wstring selectedMouseProfile(HWND w, const SettingsView &view)
|
||||
}
|
||||
|
||||
constexpr int RenameMouseProfile = 757, DeleteMouseProfile = 758;
|
||||
constexpr int AddMouseEvent = 762, MouseEventCapture = 763, MouseEventNameField = 764, MouseEventStatus = 765;
|
||||
|
||||
std::wstring mouseActionLabel(const MouseAction &action)
|
||||
{
|
||||
@@ -14,11 +15,23 @@ std::wstring mouseActionLabel(const MouseAction &action)
|
||||
return MouseActions[static_cast<size_t>(action.type)];
|
||||
}
|
||||
|
||||
int mouseEventAtRow(HWND list, int row)
|
||||
{
|
||||
if (row < 0) return -1;
|
||||
LVITEMW item{LVIF_PARAM};
|
||||
item.iItem = row;
|
||||
return ListView_GetItem(list, &item) ? static_cast<int>(item.lParam) : -1;
|
||||
}
|
||||
|
||||
void selectMouseEvent(HWND w, int event)
|
||||
{
|
||||
HWND list = GetDlgItem(w, 733);
|
||||
ListView_SetItemState(list, event, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
ListView_EnsureVisible(list, event, FALSE);
|
||||
LVFINDINFOW search{LVFI_PARAM};
|
||||
search.lParam = event;
|
||||
const int row = ListView_FindItem(list, -1, &search);
|
||||
if (row < 0) return;
|
||||
ListView_SetItemState(list, row, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
||||
ListView_EnsureVisible(list, row, FALSE);
|
||||
}
|
||||
|
||||
void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
|
||||
@@ -27,8 +40,17 @@ void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
|
||||
EnableWindow(GetDlgItem(w, RenameMouseProfile), isNamedMouseProfile(profile));
|
||||
EnableWindow(GetDlgItem(w, DeleteMouseProfile), isNamedMouseProfile(profile));
|
||||
HWND list = GetDlgItem(w, 733);
|
||||
ListView_DeleteAllItems(list);
|
||||
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
|
||||
{
|
||||
if (!mouseEventEnabled(view.draft, event)) continue;
|
||||
auto name = mouseEventName(view.draft, event);
|
||||
LVITEMW item{LVIF_TEXT | LVIF_PARAM};
|
||||
item.iItem = ListView_GetItemCount(list);
|
||||
item.lParam = event;
|
||||
item.pszText = name.data();
|
||||
const int row = ListView_InsertItem(list, &item);
|
||||
if (row < 0) continue;
|
||||
auto *action = mouseBinding(view.draft, profile, event);
|
||||
std::wstring text;
|
||||
if (profile != GlobalMouseProfile && !action)
|
||||
@@ -37,7 +59,7 @@ void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
|
||||
action = mouseBinding(view.draft, GlobalMouseProfile, event);
|
||||
}
|
||||
text += action ? mouseActionLabel(*action) : MouseActions[0];
|
||||
ListView_SetItemText(list, event, 1, text.data());
|
||||
ListView_SetItemText(list, row, 1, text.data());
|
||||
}
|
||||
selectMouseEvent(w, selection);
|
||||
}
|
||||
@@ -97,6 +119,7 @@ bool blockMouseProfileStateChange(HWND w, SettingsView &view, const NMLISTVIEW &
|
||||
{
|
||||
Settings candidate;
|
||||
candidate.mouseBindings = view.draft.mouseBindings;
|
||||
candidate.customMouseEvents = view.draft.customMouseEvents;
|
||||
candidate.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
||||
candidate.mouseProfileApplications = view.draft.mouseProfileApplications;
|
||||
candidate.disabledMouseProfiles.erase(profile);
|
||||
@@ -309,7 +332,7 @@ void drawActionApplicationIcon(HWND editor, const DRAWITEMSTRUCT &item)
|
||||
RECT horizontal{x - half, y - weight / 2, x + half, y - weight / 2 + weight};
|
||||
FillRect(item.hDC, &horizontal, ink);
|
||||
}
|
||||
if (item.CtlID == AddActionApp || item.CtlID == AddMouseProfile)
|
||||
if (item.CtlID == AddActionApp || item.CtlID == AddMouseProfile || item.CtlID == AddMouseEvent)
|
||||
{
|
||||
RECT vertical{x - weight / 2, y - half, x - weight / 2 + weight, y + half};
|
||||
FillRect(item.hDC, &vertical, ink);
|
||||
@@ -391,6 +414,7 @@ void saveMouseAction(HWND w, MouseActionEditor &editor)
|
||||
const auto *event = MouseEvents[editor.event];
|
||||
Settings check;
|
||||
check.mouseBindings = editor.view->draft.mouseBindings;
|
||||
check.customMouseEvents = editor.view->draft.customMouseEvents;
|
||||
check.disabledMouseProfiles = editor.view->draft.disabledMouseProfiles;
|
||||
check.mouseProfileApplications = editor.view->draft.mouseProfileApplications;
|
||||
if (type < 0 || (type == static_cast<int>(MouseActionType::Standard) && profile == GlobalMouseProfile))
|
||||
@@ -487,7 +511,7 @@ LRESULT CALLBACK mouseActionEditorProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
|
||||
void showMouseActionEditor(HWND parent, SettingsView &view, int event)
|
||||
{
|
||||
if (event < 0 || event >= static_cast<int>(std::size(MouseEvents)) || !IsWindowEnabled(parent)) return;
|
||||
if (!mouseEventEnabled(view.draft, event) || !IsWindowEnabled(parent)) return;
|
||||
const auto profile = selectedMouseProfile(parent, view);
|
||||
const bool application = profile != GlobalMouseProfile;
|
||||
const auto *action = mouseBinding(view.draft, profile, event);
|
||||
@@ -496,7 +520,7 @@ void showMouseActionEditor(HWND parent, SettingsView &view, int event)
|
||||
480, MouseActionEditorHeight, 0, false, parent);
|
||||
auto *editor = new MouseActionEditor{parent, &view, profile, event};
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(editor));
|
||||
label(w, std::wstring(MouseEvents[event]).append(L" · ").append(mouseProfileName(profile)).c_str(),
|
||||
label(w, mouseEventName(view.draft, event).append(L" · ").append(mouseProfileName(profile)).c_str(),
|
||||
18, 14, 444);
|
||||
label(w, L"&Action", 18, 43, 330);
|
||||
windowLayoutState(w)->fixedClient = true;
|
||||
@@ -578,7 +602,7 @@ void showMouseListMenu(HWND list, POINT point, bool profiles)
|
||||
hit.pt = point;
|
||||
ScreenToClient(list, &hit.pt);
|
||||
event = ListView_SubItemHitTest(list, &hit);
|
||||
if (event < 0) return;
|
||||
if (event < 0 && profiles) return;
|
||||
}
|
||||
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
HMENU menu = CreatePopupMenu();
|
||||
@@ -592,9 +616,11 @@ void showMouseListMenu(HWND list, POINT point, bool profiles)
|
||||
}
|
||||
else
|
||||
{
|
||||
selectMouseEvent(w, event);
|
||||
const bool canReset = mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
|
||||
AppendMenuW(menu, MF_STRING, EditMouseEvent, L"&Edit…");
|
||||
event = mouseEventAtRow(list, event);
|
||||
if (event >= 0) selectMouseEvent(w, event);
|
||||
const bool canReset = event >= 0 && mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
|
||||
AppendMenuW(menu, MF_STRING, AddMouseEvent, L"+ &Add mouse event…");
|
||||
AppendMenuW(menu, MF_STRING | (event >= 0 ? MF_ENABLED : MF_GRAYED), EditMouseEvent, L"✎ &Edit…");
|
||||
AppendMenuW(menu, MF_STRING | (canReset ? MF_ENABLED : MF_GRAYED), ResetMouseEvent, L"&Reset to default");
|
||||
}
|
||||
const auto command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_NONOTIFY | TPM_RIGHTBUTTON,
|
||||
@@ -687,6 +713,7 @@ LRESULT CALLBACK mouseProfileNameProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
const auto key = std::wstring(NamedMouseProfilePrefix) + name;
|
||||
Settings check;
|
||||
check.mouseBindings = view.draft.mouseBindings;
|
||||
check.customMouseEvents = view.draft.customMouseEvents;
|
||||
check.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
||||
check.mouseProfileApplications = view.draft.mouseProfileApplications;
|
||||
if (!previous.empty() && key != previous)
|
||||
@@ -768,9 +795,204 @@ void showMouseProfileName(HWND owner, SettingsView &view, bool renaming)
|
||||
SetFocus(GetDlgItem(w, MouseProfileName));
|
||||
}
|
||||
|
||||
struct AddMouseEventView
|
||||
{
|
||||
HWND parent{};
|
||||
SettingsView *settings{};
|
||||
int signal{-1}, lastButton{-1};
|
||||
DWORD lastPress{};
|
||||
POINT lastPoint{};
|
||||
bool rawRegistered{};
|
||||
HHOOK mouseHook{};
|
||||
int rawWheel{-1}, hookWheel{-1};
|
||||
ULONGLONG rawWheelAt{}, hookWheelAt{};
|
||||
};
|
||||
|
||||
constexpr UINT CapturedMouseWheel = WM_APP + 86;
|
||||
constexpr UINT_PTR MouseSignalTimeout = 1;
|
||||
HWND mouseEventCaptureWindow{};
|
||||
|
||||
bool inMouseEventCapture(HWND w, POINT point)
|
||||
{
|
||||
RECT area{};
|
||||
return GetWindowRect(GetDlgItem(w, MouseEventCapture), &area) && PtInRect(&area, point);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK captureMouseWheelProc(int code, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
if (code == HC_ACTION && mouseEventCaptureWindow && GetForegroundWindow() == mouseEventCaptureWindow)
|
||||
{
|
||||
const auto &mouse = *reinterpret_cast<const MSLLHOOKSTRUCT *>(lp);
|
||||
const int signal = mouseWheelEvent(static_cast<UINT>(wp), static_cast<SHORT>(HIWORD(mouse.mouseData)));
|
||||
if (signal >= 0 && !(mouse.flags & LLMHF_INJECTED) && inMouseEventCapture(mouseEventCaptureWindow, mouse.pt))
|
||||
PostMessageW(mouseEventCaptureWindow, CapturedMouseWheel, signal, 0);
|
||||
}
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
}
|
||||
|
||||
void showCapturedMouseSignal(HWND w, AddMouseEventView &view, int signal)
|
||||
{
|
||||
view.signal = signal;
|
||||
const auto &draft = view.settings->draft;
|
||||
const bool duplicate = mouseEventEnabled(draft, signal);
|
||||
EnableWindow(GetDlgItem(w, IDOK), !duplicate);
|
||||
if (duplicate)
|
||||
{
|
||||
selectMouseEvent(view.parent, signal);
|
||||
value(w, MouseEventStatus, L"Already in the list: " + mouseEventName(draft, signal));
|
||||
value(w, IDCANCEL, L"Close");
|
||||
}
|
||||
else
|
||||
{
|
||||
value(w, MouseEventStatus, std::wstring(L"Detected: ") + MouseEvents[signal]);
|
||||
if (value(w, MouseEventNameField).empty()) value(w, MouseEventNameField, MouseEvents[signal]);
|
||||
value(w, IDCANCEL, L"Cancel");
|
||||
}
|
||||
}
|
||||
|
||||
void matchCapturedMouseWheel(HWND w, AddMouseEventView &view, ULONGLONG now)
|
||||
{
|
||||
if (view.rawWheel >= 0 && view.rawWheel == view.hookWheel &&
|
||||
now - view.rawWheelAt <= 500 && now - view.hookWheelAt <= 500)
|
||||
{
|
||||
showCapturedMouseSignal(w, view, view.rawWheel);
|
||||
view.rawWheel = view.hookWheel = -1;
|
||||
KillTimer(w, MouseSignalTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
void readNewMouseSignal(HWND w, AddMouseEventView &view, HRAWINPUT handle)
|
||||
{
|
||||
POINT point{};
|
||||
if (!GetCursorPos(&point) || !inMouseEventCapture(w, point)) return;
|
||||
RAWINPUT input{};
|
||||
UINT size = sizeof(input);
|
||||
const auto bytes = GetRawInputData(handle, RID_INPUT, &input, &size, sizeof(RAWINPUTHEADER));
|
||||
if (bytes == UINT(-1) || bytes < offsetof(RAWINPUT, data) + sizeof(RAWMOUSE) ||
|
||||
input.header.dwType != RIM_TYPEMOUSE || !input.header.hDevice)
|
||||
return;
|
||||
int signal = rawMouseSignal(input.data.mouse);
|
||||
if (signal < 0) return;
|
||||
if (signal >= BuiltInMouseEventCount)
|
||||
{
|
||||
view.signal = -1;
|
||||
EnableWindow(GetDlgItem(w, IDOK), FALSE);
|
||||
value(w, MouseEventStatus, view.mouseHook ? L"Checking whether Windows delivers this wheel signal…" :
|
||||
L"Windows mouse hook is unavailable.");
|
||||
view.rawWheel = signal;
|
||||
view.rawWheelAt = GetTickCount64();
|
||||
if (view.mouseHook) SetTimer(w, MouseSignalTimeout, 550, nullptr);
|
||||
matchCapturedMouseWheel(w, view, view.rawWheelAt);
|
||||
return;
|
||||
}
|
||||
view.rawWheel = view.hookWheel = -1;
|
||||
KillTimer(w, MouseSignalTimeout);
|
||||
const DWORD now = static_cast<DWORD>(GetMessageTime());
|
||||
const bool second = signal < 3 && view.lastButton == signal &&
|
||||
doubleMouseClick(view.lastPoint, point, now - view.lastPress);
|
||||
view.lastButton = second ? -1 : signal;
|
||||
view.lastPress = now;
|
||||
view.lastPoint = point;
|
||||
if (second) signal += 5;
|
||||
showCapturedMouseSignal(w, view, signal);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK addMouseEventProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||
{
|
||||
auto *view = reinterpret_cast<AddMouseEventView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||
if (!view) return DefWindowProcW(w, msg, wp, lp);
|
||||
try
|
||||
{
|
||||
if (msg == WM_INPUT)
|
||||
readNewMouseSignal(w, *view, reinterpret_cast<HRAWINPUT>(lp));
|
||||
else if (msg == CapturedMouseWheel)
|
||||
{
|
||||
view->hookWheel = static_cast<int>(wp);
|
||||
view->hookWheelAt = GetTickCount64();
|
||||
matchCapturedMouseWheel(w, *view, view->hookWheelAt);
|
||||
return 0;
|
||||
}
|
||||
else if (msg == WM_TIMER && wp == MouseSignalTimeout)
|
||||
{
|
||||
KillTimer(w, MouseSignalTimeout);
|
||||
view->rawWheel = view->hookWheel = -1;
|
||||
value(w, MouseEventStatus, L"Raw Input saw this signal, but the Windows mouse hook did not.");
|
||||
return 0;
|
||||
}
|
||||
else if (msg == WM_COMMAND)
|
||||
{
|
||||
if (LOWORD(wp) == IDOK)
|
||||
{
|
||||
const int signal = view->signal;
|
||||
auto &settings = *view->settings;
|
||||
if (signal < BuiltInMouseEventCount || signal >= static_cast<int>(std::size(MouseEvents)) ||
|
||||
mouseEventEnabled(settings.draft, signal)) return 0;
|
||||
auto candidate = settings.draft;
|
||||
candidate.customMouseEvents.emplace(MouseEvents[signal], trimApplicationText(value(w, MouseEventNameField)));
|
||||
validateSettings(candidate);
|
||||
settings.draft.customMouseEvents = std::move(candidate.customMouseEvents);
|
||||
fillMouseEvents(view->parent, settings, signal);
|
||||
DestroyWindow(w);
|
||||
return 0;
|
||||
}
|
||||
if (LOWORD(wp) == IDCANCEL) { DestroyWindow(w); return 0; }
|
||||
}
|
||||
else if (msg == WM_CLOSE) { DestroyWindow(w); return 0; }
|
||||
else if (msg == WM_DPICHANGED) { resizeForDpi(w, wp, lp); return 0; }
|
||||
else if (msg == WM_NCDESTROY)
|
||||
{
|
||||
const HWND parent = view->parent;
|
||||
mouseEventCaptureWindow = nullptr;
|
||||
if (view->mouseHook) UnhookWindowsHookEx(view->mouseHook);
|
||||
if (view->rawRegistered)
|
||||
{
|
||||
if (IsWindow(parent) && view->settings->rawMouseRegistered) registerMouseTestInput(parent);
|
||||
else registerMouseTestInput(nullptr);
|
||||
}
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, 0);
|
||||
delete view;
|
||||
disposeFont(w);
|
||||
if (IsWindow(parent))
|
||||
{
|
||||
EnableWindow(parent, TRUE);
|
||||
SetForegroundWindow(parent);
|
||||
SetFocus(GetDlgItem(parent, 733));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const std::exception &e) { error(w, e); }
|
||||
return DefWindowProcW(w, msg, wp, lp);
|
||||
}
|
||||
|
||||
void showAddMouseEvent(HWND parent, SettingsView &settings)
|
||||
{
|
||||
if (!IsWindowEnabled(parent)) return;
|
||||
HWND w = makeWindow(*settings.app, L"KSwitcher.AddMouseEvent", L"Add mouse event", addMouseEventProc, 500, 275);
|
||||
auto *view = new AddMouseEventView{parent, &settings};
|
||||
SetWindowLongPtrW(w, GWLP_HWNDPARENT, reinterpret_cast<LONG_PTR>(parent));
|
||||
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(view));
|
||||
label(w, L"&Name", 18, 16, 464);
|
||||
edit(w, MouseEventNameField, 18, 39, 464);
|
||||
control(w, L"STATIC", L"Press a mouse button or move the wheel here",
|
||||
WS_BORDER | SS_CENTER | SS_CENTERIMAGE, MouseEventCapture, 18, 78, 464, 92);
|
||||
view->rawRegistered = registerMouseTestInput(w);
|
||||
mouseEventCaptureWindow = w;
|
||||
view->mouseHook = SetWindowsHookExW(WH_MOUSE_LL, captureMouseWheelProc, settings.app->instance, 0);
|
||||
control(w, L"STATIC", view->rawRegistered ? L"Waiting for a supported mouse signal…" : L"Windows Raw Input is unavailable.",
|
||||
0, MouseEventStatus, 18, 181, 464, 22);
|
||||
label(w, L"Buttons that send no Windows mouse event cannot be added.", 18, 205, 464);
|
||||
control(w, L"BUTTON", L"&Add", WS_TABSTOP | BS_DEFPUSHBUTTON, IDOK, 270, 239, 100, 28);
|
||||
button(w, L"Cancel", IDCANCEL, 382, 239, 100);
|
||||
EnableWindow(GetDlgItem(w, IDOK), FALSE);
|
||||
EnableWindow(parent, FALSE);
|
||||
showRequestedWindow(w);
|
||||
SetFocus(GetDlgItem(w, MouseEventNameField));
|
||||
}
|
||||
|
||||
bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
|
||||
{
|
||||
const int id = LOWORD(wp);
|
||||
if (id == AddMouseEvent) { showAddMouseEvent(w, view); return true; }
|
||||
if (id == AddMouseProfile) { showMouseProfileName(w, view, false); return true; }
|
||||
if (id == RenameMouseProfile || id == DeleteMouseProfile)
|
||||
{
|
||||
@@ -788,7 +1010,8 @@ bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
|
||||
return true;
|
||||
}
|
||||
if (id != EditMouseEvent && id != ResetMouseEvent) return false;
|
||||
const int event = ListView_GetNextItem(GetDlgItem(w, 733), -1, LVNI_SELECTED);
|
||||
const HWND list = GetDlgItem(w, 733);
|
||||
const int event = mouseEventAtRow(list, ListView_GetNextItem(list, -1, LVNI_SELECTED));
|
||||
if (event < 0) return true;
|
||||
if (id == EditMouseEvent) showMouseActionEditor(w, view, event);
|
||||
else if (auto profile = view.draft.mouseBindings.find(selectedMouseProfile(w, view));
|
||||
@@ -817,7 +1040,9 @@ void createMousePage(HWND w, SettingsView &view)
|
||||
constexpr int profilesLeft = 18, actionsLeft = 369, tableWidth = 333;
|
||||
constexpr int profilesRight = profilesLeft + tableWidth;
|
||||
label(w, L"Profiles", 18, 64, 120);
|
||||
label(w, L"Mouse events", actionsLeft, 64, tableWidth);
|
||||
label(w, L"Mouse events", actionsLeft, 64, tableWidth - 30);
|
||||
control(w, L"BUTTON", L"Add mouse event", WS_TABSTOP | BS_OWNERDRAW, AddMouseEvent,
|
||||
actionsLeft + tableWidth - 24, 61, 24, 24);
|
||||
control(w, L"BUTTON", L"Delete profile", WS_TABSTOP | BS_OWNERDRAW, DeleteMouseProfile, profilesRight - 80, 61, 24, 24);
|
||||
control(w, L"BUTTON", L"Edit profile", WS_TABSTOP | BS_OWNERDRAW, RenameMouseProfile, profilesRight - 52, 61, 24, 24);
|
||||
control(w, L"BUTTON", L"Add profile", WS_TABSTOP | BS_OWNERDRAW, AddMouseProfile, profilesRight - 24, 61, 24, 24);
|
||||
@@ -827,6 +1052,7 @@ void createMousePage(HWND w, SettingsView &view)
|
||||
addActionApplicationTooltip(tips, w, AddMouseProfile, L"Add profile");
|
||||
addActionApplicationTooltip(tips, w, RenameMouseProfile, L"Edit profile");
|
||||
addActionApplicationTooltip(tips, w, DeleteMouseProfile, L"Delete profile");
|
||||
addActionApplicationTooltip(tips, w, AddMouseEvent, L"Add mouse event");
|
||||
constexpr int listTop = 90, listHeight = 224;
|
||||
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES};
|
||||
InitCommonControlsEx(&controls);
|
||||
@@ -859,13 +1085,6 @@ void createMousePage(HWND w, SettingsView &view)
|
||||
column.pszText = actionTitle;
|
||||
column.cx = scale(w, tableWidth - 120);
|
||||
ListView_InsertColumn(list, 1, &column);
|
||||
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
|
||||
{
|
||||
LVITEMW item{LVIF_TEXT};
|
||||
item.iItem = event;
|
||||
item.pszText = const_cast<wchar_t *>(MouseEvents[event]);
|
||||
ListView_InsertItem(list, &item);
|
||||
}
|
||||
control(w, L"STATIC", MouseEventHint, 0, 740, actionsLeft, 324, tableWidth, 22);
|
||||
control(w, L"BUTTON", L"Test mouse buttons here", WS_TABSTOP | BS_PUSHBUTTON,
|
||||
737, profilesLeft, listTop + listHeight + 10, tableWidth, 56);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.64";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.65";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
Reference in new issue
Block a user