Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a65e7f7fb |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.64 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.65 LANGUAGES CXX RC)
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ In **Settings → Mouse**, put the pointer over **Test mouse buttons here** and
|
|||||||
press the buttons you want to test. The page shows the last detected button and
|
press the buttons you want to test. The page shows the last detected button and
|
||||||
counts presses separately for ordinary window events and Windows Raw Input.
|
counts presses separately for ordinary window events and Windows Raw Input.
|
||||||
|
|
||||||
|
Use **+** beside **Mouse events** to capture and name a wheel direction.
|
||||||
|
If Windows reports an existing button or an already added direction, the
|
||||||
|
dialog selects that event in the list. Extra device controls need to emit a
|
||||||
|
supported Windows mouse signal before KSwitcher can bind them.
|
||||||
|
|
||||||
Choose **Global** to set up fallback actions. Use **+** beside **Profiles** to create
|
Choose **Global** to set up fallback actions. Use **+** beside **Profiles** to create
|
||||||
a named profile and set its applications. Double-click a profile or choose
|
a named profile and set its applications. Double-click a profile or choose
|
||||||
**Edit…** in its context menu to change its name or applications. The
|
**Edit…** in its context menu to change its name or applications. The
|
||||||
@@ -680,10 +685,13 @@ required for ordinary conflicts. Every server write uses the version just read a
|
|||||||
condition, including explicit Upload. A racing write is rejected rather than silently overwritten.
|
condition, including explicit Upload. A racing write is rejected rather than silently overwritten.
|
||||||
Deleted server versions are not silently recreated.
|
Deleted server versions are not silently recreated.
|
||||||
|
|
||||||
Hotkeys, snippets, application exclusions, layout order, conversion, screenshot and autocorrection
|
The server profile includes all settings from KSwitcher Settings, including mouse events and
|
||||||
preferences are shared. Startup/elevation, keep-awake, logging, history enablement/limits,
|
bindings, application language profiles, startup, keep-awake, logging, history limits and OCR.
|
||||||
Tesseract executable/engine selection, connection details and credentials stay local. Clipboard
|
Update the clients that use the same server profile before syncing the expanded format.
|
||||||
history, screenshots and dictionary files are never uploaded. Full synchronization identifies
|
Older server backups preserve the local values for fields they never stored. Restored executable
|
||||||
|
paths refer to the same locations and may need updating on a fresh installation.
|
||||||
|
Connection details and credentials stay local so you can sign in again after reinstalling.
|
||||||
|
Clipboard history, screenshots and dictionary files are never uploaded. Full synchronization identifies
|
||||||
missing keyboard layouts by language (when Windows can identify it) and exact ID.
|
missing keyboard layouts by language (when Windows can identify it) and exact ID.
|
||||||
Install the same layout variant, or select **Keep this PC's layouts and layout shortcuts (sync
|
Install the same layout variant, or select **Keep this PC's layouts and layout shortcuts (sync
|
||||||
other settings)** and click **Save / Sign in / Sync**. This device-local option keeps the entire
|
other settings)** and click **Save / Sign in / Sync**. This device-local option keeps the entire
|
||||||
@@ -988,8 +996,8 @@ subset is returned to the first available allowed language by a periodic check
|
|||||||
with an excluded language active or selecting one via Windows or a direct shortcut.
|
with an excluded language active or selecting one via Windows or a direct shortcut.
|
||||||
Languages remain installed and visible in Windows' language picker. Unavailable
|
Languages remain installed and visible in Windows' language picker. Unavailable
|
||||||
layouts are skipped; if the entire selection is unavailable, the default cycle is
|
layouts are skipped; if the entire selection is unavailable, the default cycle is
|
||||||
used. Profiles stay local to this PC, survive cloud sync and are included in manual
|
used. Application language profiles are included in server synchronization and
|
||||||
settings export/import.
|
manual settings export/import.
|
||||||
|
|
||||||
**Українською.** Відкрий **Settings → Languages → Languages for applications…**.
|
**Українською.** Відкрий **Settings → Languages → Languages for applications…**.
|
||||||
Обери програму зліва та познач дозволені мови справа. Вибір для кожної програми
|
Обери програму зліва та познач дозволені мови справа. Вибір для кожної програми
|
||||||
@@ -1012,5 +1020,5 @@ settings export/import.
|
|||||||
Це діє і при вході в програму, і після ручного перемикання через Windows чи пряму
|
Це діє і при вході в програму, і після ручного перемикання через Windows чи пряму
|
||||||
комбінацію. Мови залишаються встановленими й видимими в меню Windows.
|
комбінацію. Мови залишаються встановленими й видимими в меню Windows.
|
||||||
Недоступні розкладки пропускаються; якщо недоступні всі вибрані мови, діє
|
Недоступні розкладки пропускаються; якщо недоступні всі вибрані мови, діє
|
||||||
загальний цикл. Профілі локальні, зберігаються під час хмарної синхронізації
|
загальний цикл. Профілі входять до серверної синхронізації
|
||||||
та входять до ручного експорту/імпорту налаштувань.
|
та входять до ручного експорту/імпорту налаштувань.
|
||||||
Binary file not shown.
@@ -1,5 +1,29 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
|
## v0.6.65 — 2026-11-12
|
||||||
|
|
||||||
|
Use **Settings → Mouse → +** beside **Mouse events** to capture and name a
|
||||||
|
vertical or horizontal wheel direction. Capturing a known button or an
|
||||||
|
already registered direction selects its existing row. A new signal is
|
||||||
|
accepted only when Windows delivers it through both the test and runtime
|
||||||
|
input paths. A configured wheel action replaces scrolling for that direction
|
||||||
|
where the profile applies.
|
||||||
|
|
||||||
|
OpenBao now backs up all KSwitcher Settings, including mouse events, bindings,
|
||||||
|
application profiles, startup, clipboard history limits, and OCR preferences.
|
||||||
|
Update all clients that share a server profile before syncing. Downloading an
|
||||||
|
older backup preserves local settings for fields it never included. After
|
||||||
|
reinstalling Windows, reconnect to OpenBao and choose **Download remote**.
|
||||||
|
Executable paths and optional OCR software must still be available on the
|
||||||
|
restored PC. Clipboard contents and Windows credentials stay on the device.
|
||||||
|
|
||||||
|
**Українською.** У **Settings → Mouse** натисни **+** біля **Mouse events**,
|
||||||
|
щоб додати напрямок прокручування. Відомий сигнал виділить наявний рядок.
|
||||||
|
OpenBao тепер зберігає всі налаштування KSwitcher, зокрема профілі миші,
|
||||||
|
OCR й автозапуск. Перед синхронізацією онови всі клієнти спільного профілю.
|
||||||
|
Після перевстановлення Windows підключись до OpenBao й натисни
|
||||||
|
**Download remote**.
|
||||||
|
|
||||||
## v0.6.64 — 2026-11-12
|
## v0.6.64 — 2026-11-12
|
||||||
|
|
||||||
The new **Settings → Mouse** tab lets you assign actions to mouse buttons and supported double-clicks. Use **Global** for fallback actions and create profiles for one or more applications. Edit a profile with a double-click, and enable or disable it with the **Active** checkbox. Choose applications by pointing at a window or opening an executable or shortcut.
|
The new **Settings → Mouse** tab lets you assign actions to mouse buttons and supported double-clicks. Use **Global** for fallback actions and create profiles for one or more applications. Edit a profile with a double-click, and enable or disable it with the **Active** checkbox. Choose applications by pointing at a window or opening an executable or shortcut.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
8fdd701d2ed9223056f7e624e888ef459c919f605c624d5f35c3dbd3652a430b KSwitcher-win-x64.zip
|
e60ca342737155dce14a2bb1dd43ec1cef890cf7d9e5efb2e15a5b9629eaf1af KSwitcher-win-x64.zip
|
||||||
+37
-37
@@ -1,86 +1,86 @@
|
|||||||
{
|
{
|
||||||
"version": "0.6.64",
|
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "KSwitcher.exe",
|
"size": 2190336,
|
||||||
"size": 2185728,
|
"sha256": "9d268cbb00fb9db3e52bc68994cefb50cee707f9dc1b940d876314ba1b6a06b3",
|
||||||
"sha256": "81ccb50fd3ac8a4768f0be9a4b70ef21c2a1f11e73b57871cb5123f29bfbd503"
|
"path": "KSwitcher.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "KSwitcher.Config.exe",
|
"size": 2129408,
|
||||||
"size": 2120192,
|
"sha256": "d83fad30539e7b974bb8012859f3fee8143c5e4cc25684643b16e2fc34b97a5e",
|
||||||
"sha256": "83d76af6ff44f56532d2537e57dfb7e3859d72f34180d3c7c02497e71763378e"
|
"path": "KSwitcher.Config.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "KSwitcher.Screenshot.exe",
|
"size": 3210240,
|
||||||
"size": 3207168,
|
"sha256": "29f7846ee4d3c469779b66acf99c90a4173e1e1577020398d6e1dc4eae492e06",
|
||||||
"sha256": "6e93963d4252a87bd6ea9d53ccd2e602c9c2709ca3fe835dc90a633b77b00193"
|
"path": "KSwitcher.Screenshot.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "LICENSE",
|
|
||||||
"size": 4401,
|
"size": 4401,
|
||||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
|
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||||
|
"path": "LICENSE"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "json-LICENSE.txt",
|
|
||||||
"size": 1075,
|
"size": 1075,
|
||||||
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
|
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
|
||||||
|
"path": "json-LICENSE.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "README.md",
|
"size": 90358,
|
||||||
"size": 89826,
|
"sha256": "435daae289ca64dd9130ed772125de3f3303f1dbb80d1cbf7a9913058ded47ec",
|
||||||
"sha256": "9148049aa80595e0de30ecc9af1478572f85a8076da981ea80dafd5bb9c4815b"
|
"path": "README.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "LAN-PREVIEW.md",
|
|
||||||
"size": 9900,
|
"size": 9900,
|
||||||
"sha256": "6dcb0efabec6898965bbbd31ba68e06276fd0f098ac9a8649bc1f7d7de21d63e"
|
"sha256": "6dcb0efabec6898965bbbd31ba68e06276fd0f098ac9a8649bc1f7d7de21d63e",
|
||||||
|
"path": "LAN-PREVIEW.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dictionaries/en.txt",
|
|
||||||
"size": 682867,
|
"size": 682867,
|
||||||
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
|
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
|
||||||
|
"path": "dictionaries/en.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
|
||||||
"size": 20138,
|
"size": 20138,
|
||||||
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
|
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
|
||||||
|
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dictionaries/README.md",
|
|
||||||
"size": 2230,
|
"size": 2230,
|
||||||
"sha256": "2df17fe4e17d527c67ab57269b07dce59a49c1fb128c429a1408decbb47109ec"
|
"sha256": "2df17fe4e17d527c67ab57269b07dce59a49c1fb128c429a1408decbb47109ec",
|
||||||
|
"path": "dictionaries/README.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dictionaries/ru.txt",
|
|
||||||
"size": 1060364,
|
"size": 1060364,
|
||||||
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
|
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
|
||||||
|
"path": "dictionaries/ru.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dictionaries/sources.json",
|
|
||||||
"size": 1279,
|
"size": 1279,
|
||||||
"sha256": "d6e11a3aaba22a8a7eaba19f778d8a5bd66653ff306810e85a1b0e7fff3a6b86"
|
"sha256": "d6e11a3aaba22a8a7eaba19f778d8a5bd66653ff306810e85a1b0e7fff3a6b86",
|
||||||
|
"path": "dictionaries/sources.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "dictionaries/uk.txt",
|
|
||||||
"size": 898248,
|
"size": 898248,
|
||||||
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86"
|
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86",
|
||||||
|
"path": "dictionaries/uk.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/README.txt",
|
|
||||||
"size": 993,
|
"size": 993,
|
||||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
|
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||||
|
"path": "tools/README.txt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/StayHere.exe",
|
|
||||||
"size": 134656,
|
"size": 134656,
|
||||||
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
|
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
|
||||||
|
"path": "tools/StayHere.exe"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "tools/StayHere-README.md",
|
|
||||||
"size": 2796,
|
"size": 2796,
|
||||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
|
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||||
|
"path": "tools/StayHere-README.md"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"version": "0.6.65",
|
||||||
"schema": 1
|
"schema": 1
|
||||||
}
|
}
|
||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,64,0
|
FILEVERSION 0,6,65,0
|
||||||
PRODUCTVERSION 0,6,64,0
|
PRODUCTVERSION 0,6,65,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.64"
|
VALUE "FileVersion", "0.6.65"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+43
-10
@@ -733,6 +733,10 @@ Bytes App::lanCommand(const Bytes &command)
|
|||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
void App::applySettings(const Settings &requested)
|
void App::applySettings(const Settings &requested)
|
||||||
|
{
|
||||||
|
applySettings(requested, true);
|
||||||
|
}
|
||||||
|
void App::applySettings(const Settings &requested, bool validateOcrInstallation)
|
||||||
{
|
{
|
||||||
Settings s = requested;
|
Settings s = requested;
|
||||||
std::set<std::wstring> present, removed(s.snippetTombstones.begin(), s.snippetTombstones.end());
|
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;
|
bool autostartChanged = false;
|
||||||
AutoCorrector predictor;
|
AutoCorrector predictor;
|
||||||
validateSettings(s);
|
validateSettings(s);
|
||||||
validateTesseract(s);
|
if (validateOcrInstallation && ocrProfile(s) != ocrProfile(settings)) validateTesseract(s);
|
||||||
if (s.autoCorrect)
|
if (s.autoCorrect)
|
||||||
{
|
{
|
||||||
predictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", s.autoLanguages);
|
predictor.configure(std::filesystem::path(executable()).parent_path() / L"dictionaries", s.autoLanguages);
|
||||||
@@ -849,6 +853,8 @@ void App::resetLanInput()
|
|||||||
chatSwitchHook = {};
|
chatSwitchHook = {};
|
||||||
pendingMouseClicks = {};
|
pendingMouseClicks = {};
|
||||||
mouseActions.clear();
|
mouseActions.clear();
|
||||||
|
mouseWheelTarget = nullptr;
|
||||||
|
mouseWheelRemainder = 0;
|
||||||
resetInput();
|
resetInput();
|
||||||
}
|
}
|
||||||
void App::resetInput()
|
void App::resetInput()
|
||||||
@@ -1399,6 +1405,8 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
|||||||
auto &app = *current;
|
auto &app = *current;
|
||||||
const auto &mouse = *reinterpret_cast<const MSLLHOOKSTRUCT *>(lp);
|
const auto &mouse = *reinterpret_cast<const MSLLHOOKSTRUCT *>(lp);
|
||||||
const int button = mouse.flags & LLMHF_INJECTED ? -1 : mouseButton(UINT(wp), HIWORD(mouse.mouseData));
|
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));
|
const bool down = mouseButtonDown(UINT(wp));
|
||||||
if (button >= 0 && app.mousePresses[button].held)
|
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 (app.lanService->takeInputChange()) app.resetLanInput();
|
||||||
if (consumed) return 1;
|
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())
|
!app.installingUpdate && !app.hotkeyCapture && modifiersReleased())
|
||||||
{
|
{
|
||||||
const auto hit = WindowFromPoint(mouse.pt);
|
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 ||
|
const bool shellSurface = target == GetShellWindow() || wcscmp(windowClass, L"Progman") == 0 ||
|
||||||
wcscmp(windowClass, L"WorkerW") == 0 || wcscmp(windowClass, L"Shell_TrayWnd") == 0 ||
|
wcscmp(windowClass, L"WorkerW") == 0 || wcscmp(windowClass, L"Shell_TrayWnd") == 0 ||
|
||||||
wcscmp(windowClass, L"Shell_SecondaryTrayWnd") == 0;
|
wcscmp(windowClass, L"Shell_SecondaryTrayWnd") == 0;
|
||||||
const auto *global = mouseBinding(app.settings, GlobalMouseProfile, button);
|
const int event = wheel >= 0 ? wheel : button;
|
||||||
const bool globalLaunch = button >= 2 && global && global->type == MouseActionType::Launch &&
|
const auto *global = mouseBinding(app.settings, GlobalMouseProfile, event);
|
||||||
|
const bool globalLaunch = event >= 2 && global && global->type == MouseActionType::Launch &&
|
||||||
mouseActionApplies(*global, name);
|
mouseActionApplies(*global, name);
|
||||||
const bool restricted = ownWindow || shellSurface || name.empty();
|
const bool restricted = ownWindow || shellSurface || name.empty();
|
||||||
const bool testButton = name == L"kswitcher.config" && hit == GetDlgItem(target, 737);
|
wchar_t targetClass[64]{};
|
||||||
const auto *fallback = restricted ? effectiveMouseAction(app.settings, L"", button, false) : nullptr;
|
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 auto *restrictedAction = isMouseVolumeAction(fallback) ? fallback : globalLaunch ? global : nullptr;
|
||||||
const bool eligible = target && !testButton && (!restricted || (button >= 2 && restrictedAction)) &&
|
const bool eligible = target && !testButton && (!restricted || (event >= 2 && restrictedAction)) &&
|
||||||
(button >= 2 || target == GetForegroundWindow());
|
(event >= 2 || target == GetForegroundWindow());
|
||||||
if (eligible)
|
if (eligible)
|
||||||
{
|
{
|
||||||
const bool foreground = target == GetForegroundWindow();
|
const bool foreground = target == GetForegroundWindow();
|
||||||
bool singleScoped{}, doubleScoped{};
|
bool singleScoped{}, doubleScoped{};
|
||||||
auto *single = restricted ? restrictedAction :
|
auto *single = restricted ? restrictedAction :
|
||||||
effectiveMouseAction(app.settings, name, button, foreground, &singleScoped);
|
effectiveMouseAction(app.settings, name, event, foreground, &singleScoped);
|
||||||
auto *doubleClick = !restricted && button < 3 ?
|
auto *doubleClick = wheel < 0 && !restricted && button < 3 ?
|
||||||
effectiveMouseAction(app.settings, name, 5 + button, foreground, &doubleScoped) : nullptr;
|
effectiveMouseAction(app.settings, name, 5 + button, foreground, &doubleScoped) : nullptr;
|
||||||
if (!foreground)
|
if (!foreground)
|
||||||
{
|
{
|
||||||
@@ -1465,6 +1478,20 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
|||||||
if (doubleClick && doubleClick->type == MouseActionType::Shortcut) doubleClick = nullptr;
|
if (doubleClick && doubleClick->type == MouseActionType::Shortcut) doubleClick = nullptr;
|
||||||
}
|
}
|
||||||
const auto now = GetTickCount64();
|
const auto now = GetTickCount64();
|
||||||
|
if (wheel >= 0 && single)
|
||||||
|
{
|
||||||
|
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];
|
auto &pending = app.pendingMouseClicks[button];
|
||||||
const bool second = doubleClick && pending.target == target &&
|
const bool second = doubleClick && pending.target == target &&
|
||||||
doubleMouseClick(pending.point, mouse.pt, now - pending.started);
|
doubleMouseClick(pending.point, mouse.pt, now - pending.started);
|
||||||
@@ -1484,6 +1511,12 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (wheel >= 0)
|
||||||
|
{
|
||||||
|
app.mouseWheelTarget = nullptr;
|
||||||
|
app.mouseWheelRemainder = 0;
|
||||||
|
}
|
||||||
// Remote clicks invalidate a typing range just like local clicks.
|
// Remote clicks invalidate a typing range just like local clicks.
|
||||||
if (down || wp == WM_MOUSEWHEEL || wp == WM_MOUSEHWHEEL)
|
if (down || wp == WM_MOUSEWHEEL || wp == WM_MOUSEHWHEEL)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ class App
|
|||||||
private:
|
private:
|
||||||
friend struct InputRegressionAccess;
|
friend struct InputRegressionAccess;
|
||||||
void releaseIdleMemory(ULONGLONG now);
|
void releaseIdleMemory(ULONGLONG now);
|
||||||
|
void applySettings(const Settings &, bool validateOcrInstallation);
|
||||||
HHOOK keyboardHook{}, mouseHook{};
|
HHOOK keyboardHook{}, mouseHook{};
|
||||||
struct MousePress
|
struct MousePress
|
||||||
{
|
{
|
||||||
@@ -118,6 +119,8 @@ class App
|
|||||||
std::array<MousePress, std::size(MouseButtonNames)> mousePresses{};
|
std::array<MousePress, std::size(MouseButtonNames)> mousePresses{};
|
||||||
std::array<MousePress, std::size(MouseButtonNames)> pendingMouseClicks{};
|
std::array<MousePress, std::size(MouseButtonNames)> pendingMouseClicks{};
|
||||||
std::deque<MousePress> mouseActions;
|
std::deque<MousePress> mouseActions;
|
||||||
|
HWND mouseWheelTarget{};
|
||||||
|
int mouseWheelSignal{-1}, mouseWheelRemainder{};
|
||||||
void queueMouseAction(MousePress);
|
void queueMouseAction(MousePress);
|
||||||
void flushMouseClicks();
|
void flushMouseClicks();
|
||||||
void beginMouseVolume(int button);
|
void beginMouseVolume(int button);
|
||||||
|
|||||||
+2
-1
@@ -79,7 +79,8 @@ void App::pollSync()
|
|||||||
if (encodeSettings(next) != snapshot)
|
if (encodeSettings(next) != snapshot)
|
||||||
{
|
{
|
||||||
atomicWrite(data / L"settings-before-sync.bin", protect(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.";
|
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." :
|
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, std::vector<std::wstring>> chatCycles;
|
||||||
std::map<std::wstring, AppLanguageSet> appLanguageSets;
|
std::map<std::wstring, AppLanguageSet> appLanguageSets;
|
||||||
std::map<std::wstring, std::map<std::wstring, MouseAction>> mouseBindings;
|
std::map<std::wstring, std::map<std::wstring, MouseAction>> mouseBindings;
|
||||||
|
std::map<std::wstring, std::wstring> customMouseEvents;
|
||||||
std::set<std::wstring> disabledMouseProfiles;
|
std::set<std::wstring> disabledMouseProfiles;
|
||||||
std::map<std::wstring, std::set<std::wstring>> mouseProfileApplications;
|
std::map<std::wstring, std::set<std::wstring>> mouseProfileApplications;
|
||||||
std::vector<std::wstring> excluded{L"KeePass", L"KeePassXC", L"1Password", L"Bitwarden"};
|
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 *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",
|
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",
|
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"};
|
L"Volume up", L"Volume down", L"Mute", L"Launch application"};
|
||||||
inline constexpr wchar_t GlobalMouseProfile[] = L"*";
|
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)
|
bool foreground = true, bool *profileScoped = nullptr)
|
||||||
{
|
{
|
||||||
if (profileScoped) *profileScoped = false;
|
if (profileScoped) *profileScoped = false;
|
||||||
|
if (!mouseEventEnabled(settings, event)) return nullptr;
|
||||||
auto *action = mouseBinding(settings, app, event);
|
auto *action = mouseBinding(settings, app, event);
|
||||||
if (!action)
|
if (!action)
|
||||||
{
|
{
|
||||||
|
|||||||
+20
-1
@@ -108,6 +108,15 @@ static void validateMouseProfileConflicts(const Settings &s)
|
|||||||
|
|
||||||
void validateSettings(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.");
|
if (s.mouseBindings.size() > 1000) throw std::runtime_error("Too many mouse profiles.");
|
||||||
for (const auto &[app, bindings] : s.mouseBindings)
|
for (const auto &[app, bindings] : s.mouseBindings)
|
||||||
{
|
{
|
||||||
@@ -124,7 +133,8 @@ void validateSettings(const Settings &s)
|
|||||||
if (action.applications.size() > 1000 ||
|
if (action.applications.size() > 1000 ||
|
||||||
!std::all_of(action.applications.begin(), action.applications.end(), validMouseApplication))
|
!std::all_of(action.applications.begin(), action.applications.end(), validMouseApplication))
|
||||||
throw std::runtime_error("Enter valid target application executable names.");
|
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))
|
static_cast<size_t>(action.type) >= std::size(MouseActions))
|
||||||
throw std::runtime_error("Invalid mouse event or action.");
|
throw std::runtime_error("Invalid mouse event or action.");
|
||||||
if (isMouseVolumeStep(action.type) && (action.volumeStep < 1 || action.volumeStep > 100))
|
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);
|
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"))
|
if (j.contains("mouseBindings"))
|
||||||
{
|
{
|
||||||
const auto &profiles = j.at("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 &id : set.languages) item["languages"].push_back(utf8(id));
|
||||||
for (const auto &app : set.applications) item["applications"].push_back(utf8(app));
|
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();
|
j["mouseBindings"] = Json::object();
|
||||||
for (const auto &[app, bindings] : s.mouseBindings)
|
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));
|
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)
|
Bytes sharedSyncSettings(const Settings &s)
|
||||||
{
|
{
|
||||||
auto j = parse(encodeSettings(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) {
|
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>();
|
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));
|
return bytes(canonicalProfile(j));
|
||||||
}
|
}
|
||||||
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
|
Settings restoreSyncSettings(const Bytes &shared, const Settings &local, const std::vector<Layout> &layouts, bool keepLocalLayouts)
|
||||||
{
|
{
|
||||||
auto j = parse(shared), original = parse(encodeSettings(local));
|
auto j = parse(shared), original = parse(encodeSettings(local));
|
||||||
|
preserveMissingSettings(j, original);
|
||||||
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
|
if (!j.contains("snippetTombstones")) j["snippetTombstones"] = Json::array();
|
||||||
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
|
if (!j.contains("autoCorrectReplace")) j["autoCorrectReplace"] = false;
|
||||||
if (!j.contains("autoCorrectTypos")) j["autoCorrectTypos"] = 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)
|
if (!j.is_object() || j.value("format", "") != "kswitcher-settings" || j.value("schemaVersion", 0) != 1)
|
||||||
throw std::runtime_error("Unsupported synchronized settings format.");
|
throw std::runtime_error("Unsupported synchronized settings format.");
|
||||||
for (auto it = j.begin(); it != j.end(); ++it)
|
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.");
|
if (j.size() != allowed.size()) throw std::runtime_error("Remote profile is incomplete.");
|
||||||
std::set<std::string> installed;
|
std::set<std::string> installed;
|
||||||
for (const auto &layout : layouts) installed.insert(utf8(layout.id));
|
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
|
else
|
||||||
{
|
{
|
||||||
auto profile = parse(action == SyncAction::Download ? remote.settings : mergeLocal);
|
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));
|
auto selected = bytes(normalizeProfile(profile));
|
||||||
merged = {selected, selected, selected, {}};
|
merged = {selected, selected, selected, {}};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,14 @@ Json flatProfile(Json j)
|
|||||||
else ++profile;
|
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();
|
Json snippets = Json::array(), deleted = Json::array();
|
||||||
for (const auto &[id, item] : j.at("snippets").items())
|
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.
|
// client's chat profiles until both copies understand this field.
|
||||||
if (!remoteProfile.contains("chatCycles")) remoteProfile["chatCycles"] = localProfile.value("chatCycles", Json::object());
|
if (!remoteProfile.contains("chatCycles")) remoteProfile["chatCycles"] = localProfile.value("chatCycles", Json::object());
|
||||||
if (!localProfile.contains("chatCycles")) localProfile["chatCycles"] = remoteProfile.at("chatCycles");
|
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);
|
auto l = treeProfile(localProfile), r = treeProfile(remoteProfile);
|
||||||
// With no common history, union independent entries; differing scalar values
|
// With no common history, union independent entries; differing scalar values
|
||||||
// remain on their respective devices until the user resolves the conflict.
|
// 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)
|
switch (msg)
|
||||||
{
|
{
|
||||||
case WM_DRAWITEM:
|
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));
|
drawActionApplicationIcon(w, *reinterpret_cast<const DRAWITEMSTRUCT *>(lp));
|
||||||
return TRUE;
|
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)
|
if (((NMHDR *)lp)->idFrom == 733 && ((NMHDR *)lp)->code == NM_DBLCLK)
|
||||||
{
|
{
|
||||||
const auto &click = *reinterpret_cast<const NMITEMACTIVATE *>(lp);
|
const auto &click = *reinterpret_cast<const NMITEMACTIVATE *>(lp);
|
||||||
showMouseActionEditor(w, *v, click.iItem);
|
showMouseActionEditor(w, *v, mouseEventAtRow(click.hdr.hwndFrom, click.iItem));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (((NMHDR *)lp)->idFrom == 260 && ((NMHDR *)lp)->code == TCN_SELCHANGE)
|
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 RenameMouseProfile = 757, DeleteMouseProfile = 758;
|
||||||
|
constexpr int AddMouseEvent = 762, MouseEventCapture = 763, MouseEventNameField = 764, MouseEventStatus = 765;
|
||||||
|
|
||||||
std::wstring mouseActionLabel(const MouseAction &action)
|
std::wstring mouseActionLabel(const MouseAction &action)
|
||||||
{
|
{
|
||||||
@@ -14,11 +15,23 @@ std::wstring mouseActionLabel(const MouseAction &action)
|
|||||||
return MouseActions[static_cast<size_t>(action.type)];
|
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)
|
void selectMouseEvent(HWND w, int event)
|
||||||
{
|
{
|
||||||
HWND list = GetDlgItem(w, 733);
|
HWND list = GetDlgItem(w, 733);
|
||||||
ListView_SetItemState(list, event, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
|
LVFINDINFOW search{LVFI_PARAM};
|
||||||
ListView_EnsureVisible(list, event, FALSE);
|
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)
|
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, RenameMouseProfile), isNamedMouseProfile(profile));
|
||||||
EnableWindow(GetDlgItem(w, DeleteMouseProfile), isNamedMouseProfile(profile));
|
EnableWindow(GetDlgItem(w, DeleteMouseProfile), isNamedMouseProfile(profile));
|
||||||
HWND list = GetDlgItem(w, 733);
|
HWND list = GetDlgItem(w, 733);
|
||||||
|
ListView_DeleteAllItems(list);
|
||||||
for (int event = 0; event < static_cast<int>(std::size(MouseEvents)); ++event)
|
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);
|
auto *action = mouseBinding(view.draft, profile, event);
|
||||||
std::wstring text;
|
std::wstring text;
|
||||||
if (profile != GlobalMouseProfile && !action)
|
if (profile != GlobalMouseProfile && !action)
|
||||||
@@ -37,7 +59,7 @@ void fillMouseEvents(HWND w, SettingsView &view, int selection = 0)
|
|||||||
action = mouseBinding(view.draft, GlobalMouseProfile, event);
|
action = mouseBinding(view.draft, GlobalMouseProfile, event);
|
||||||
}
|
}
|
||||||
text += action ? mouseActionLabel(*action) : MouseActions[0];
|
text += action ? mouseActionLabel(*action) : MouseActions[0];
|
||||||
ListView_SetItemText(list, event, 1, text.data());
|
ListView_SetItemText(list, row, 1, text.data());
|
||||||
}
|
}
|
||||||
selectMouseEvent(w, selection);
|
selectMouseEvent(w, selection);
|
||||||
}
|
}
|
||||||
@@ -97,6 +119,7 @@ bool blockMouseProfileStateChange(HWND w, SettingsView &view, const NMLISTVIEW &
|
|||||||
{
|
{
|
||||||
Settings candidate;
|
Settings candidate;
|
||||||
candidate.mouseBindings = view.draft.mouseBindings;
|
candidate.mouseBindings = view.draft.mouseBindings;
|
||||||
|
candidate.customMouseEvents = view.draft.customMouseEvents;
|
||||||
candidate.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
candidate.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
||||||
candidate.mouseProfileApplications = view.draft.mouseProfileApplications;
|
candidate.mouseProfileApplications = view.draft.mouseProfileApplications;
|
||||||
candidate.disabledMouseProfiles.erase(profile);
|
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};
|
RECT horizontal{x - half, y - weight / 2, x + half, y - weight / 2 + weight};
|
||||||
FillRect(item.hDC, &horizontal, ink);
|
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};
|
RECT vertical{x - weight / 2, y - half, x - weight / 2 + weight, y + half};
|
||||||
FillRect(item.hDC, &vertical, ink);
|
FillRect(item.hDC, &vertical, ink);
|
||||||
@@ -391,6 +414,7 @@ void saveMouseAction(HWND w, MouseActionEditor &editor)
|
|||||||
const auto *event = MouseEvents[editor.event];
|
const auto *event = MouseEvents[editor.event];
|
||||||
Settings check;
|
Settings check;
|
||||||
check.mouseBindings = editor.view->draft.mouseBindings;
|
check.mouseBindings = editor.view->draft.mouseBindings;
|
||||||
|
check.customMouseEvents = editor.view->draft.customMouseEvents;
|
||||||
check.disabledMouseProfiles = editor.view->draft.disabledMouseProfiles;
|
check.disabledMouseProfiles = editor.view->draft.disabledMouseProfiles;
|
||||||
check.mouseProfileApplications = editor.view->draft.mouseProfileApplications;
|
check.mouseProfileApplications = editor.view->draft.mouseProfileApplications;
|
||||||
if (type < 0 || (type == static_cast<int>(MouseActionType::Standard) && profile == GlobalMouseProfile))
|
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)
|
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 auto profile = selectedMouseProfile(parent, view);
|
||||||
const bool application = profile != GlobalMouseProfile;
|
const bool application = profile != GlobalMouseProfile;
|
||||||
const auto *action = mouseBinding(view.draft, profile, event);
|
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);
|
480, MouseActionEditorHeight, 0, false, parent);
|
||||||
auto *editor = new MouseActionEditor{parent, &view, profile, event};
|
auto *editor = new MouseActionEditor{parent, &view, profile, event};
|
||||||
SetWindowLongPtrW(w, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(editor));
|
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);
|
18, 14, 444);
|
||||||
label(w, L"&Action", 18, 43, 330);
|
label(w, L"&Action", 18, 43, 330);
|
||||||
windowLayoutState(w)->fixedClient = true;
|
windowLayoutState(w)->fixedClient = true;
|
||||||
@@ -578,7 +602,7 @@ void showMouseListMenu(HWND list, POINT point, bool profiles)
|
|||||||
hit.pt = point;
|
hit.pt = point;
|
||||||
ScreenToClient(list, &hit.pt);
|
ScreenToClient(list, &hit.pt);
|
||||||
event = ListView_SubItemHitTest(list, &hit);
|
event = ListView_SubItemHitTest(list, &hit);
|
||||||
if (event < 0) return;
|
if (event < 0 && profiles) return;
|
||||||
}
|
}
|
||||||
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
auto &view = *reinterpret_cast<SettingsView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||||
HMENU menu = CreatePopupMenu();
|
HMENU menu = CreatePopupMenu();
|
||||||
@@ -592,9 +616,11 @@ void showMouseListMenu(HWND list, POINT point, bool profiles)
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
selectMouseEvent(w, event);
|
event = mouseEventAtRow(list, event);
|
||||||
const bool canReset = mouseBinding(view.draft, selectedMouseProfile(w, view), event) != nullptr;
|
if (event >= 0) selectMouseEvent(w, event);
|
||||||
AppendMenuW(menu, MF_STRING, EditMouseEvent, L"&Edit…");
|
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");
|
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,
|
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;
|
const auto key = std::wstring(NamedMouseProfilePrefix) + name;
|
||||||
Settings check;
|
Settings check;
|
||||||
check.mouseBindings = view.draft.mouseBindings;
|
check.mouseBindings = view.draft.mouseBindings;
|
||||||
|
check.customMouseEvents = view.draft.customMouseEvents;
|
||||||
check.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
check.disabledMouseProfiles = view.draft.disabledMouseProfiles;
|
||||||
check.mouseProfileApplications = view.draft.mouseProfileApplications;
|
check.mouseProfileApplications = view.draft.mouseProfileApplications;
|
||||||
if (!previous.empty() && key != previous)
|
if (!previous.empty() && key != previous)
|
||||||
@@ -768,9 +795,204 @@ void showMouseProfileName(HWND owner, SettingsView &view, bool renaming)
|
|||||||
SetFocus(GetDlgItem(w, MouseProfileName));
|
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)
|
bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
|
||||||
{
|
{
|
||||||
const int id = LOWORD(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 == AddMouseProfile) { showMouseProfileName(w, view, false); return true; }
|
||||||
if (id == RenameMouseProfile || id == DeleteMouseProfile)
|
if (id == RenameMouseProfile || id == DeleteMouseProfile)
|
||||||
{
|
{
|
||||||
@@ -788,7 +1010,8 @@ bool handleMouseBindings(HWND w, SettingsView &view, WPARAM wp)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (id != EditMouseEvent && id != ResetMouseEvent) return false;
|
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 (event < 0) return true;
|
||||||
if (id == EditMouseEvent) showMouseActionEditor(w, view, event);
|
if (id == EditMouseEvent) showMouseActionEditor(w, view, event);
|
||||||
else if (auto profile = view.draft.mouseBindings.find(selectedMouseProfile(w, view));
|
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 profilesLeft = 18, actionsLeft = 369, tableWidth = 333;
|
||||||
constexpr int profilesRight = profilesLeft + tableWidth;
|
constexpr int profilesRight = profilesLeft + tableWidth;
|
||||||
label(w, L"Profiles", 18, 64, 120);
|
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"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"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);
|
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, AddMouseProfile, L"Add profile");
|
||||||
addActionApplicationTooltip(tips, w, RenameMouseProfile, L"Edit profile");
|
addActionApplicationTooltip(tips, w, RenameMouseProfile, L"Edit profile");
|
||||||
addActionApplicationTooltip(tips, w, DeleteMouseProfile, L"Delete profile");
|
addActionApplicationTooltip(tips, w, DeleteMouseProfile, L"Delete profile");
|
||||||
|
addActionApplicationTooltip(tips, w, AddMouseEvent, L"Add mouse event");
|
||||||
constexpr int listTop = 90, listHeight = 224;
|
constexpr int listTop = 90, listHeight = 224;
|
||||||
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES};
|
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_LISTVIEW_CLASSES};
|
||||||
InitCommonControlsEx(&controls);
|
InitCommonControlsEx(&controls);
|
||||||
@@ -859,13 +1085,6 @@ void createMousePage(HWND w, SettingsView &view)
|
|||||||
column.pszText = actionTitle;
|
column.pszText = actionTitle;
|
||||||
column.cx = scale(w, tableWidth - 120);
|
column.cx = scale(w, tableWidth - 120);
|
||||||
ListView_InsertColumn(list, 1, &column);
|
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"STATIC", MouseEventHint, 0, 740, actionsLeft, 324, tableWidth, 22);
|
||||||
control(w, L"BUTTON", L"Test mouse buttons here", WS_TABSTOP | BS_PUSHBUTTON,
|
control(w, L"BUTTON", L"Test mouse buttons here", WS_TABSTOP | BS_PUSHBUTTON,
|
||||||
737, profilesLeft, listTop + listHeight + 10, tableWidth, 56);
|
737, profilesLeft, listTop + listHeight + 10, tableWidth, 56);
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
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 UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -125,6 +125,27 @@ struct InputRegressionAccess {
|
|||||||
return a.notifiedUpdate;
|
return a.notifiedUpdate;
|
||||||
}
|
}
|
||||||
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
|
static void click() { MSLLHOOKSTRUCT m{}; m.flags=LLMHF_INJECTED; App::mouseProc(HC_ACTION,WM_LBUTTONDOWN,(LPARAM)&m); }
|
||||||
|
static bool wheelQueuesAction(App &app)
|
||||||
|
{
|
||||||
|
MSLLHOOKSTRUCT mouse{};
|
||||||
|
if (!GetCursorPos(&mouse.pt) || !WindowFromPoint(mouse.pt)) return false;
|
||||||
|
auto previous = app.settings;
|
||||||
|
app.settings.customMouseEvents[L"Wheel down"] = L"Wheel test";
|
||||||
|
app.settings.mouseBindings[GlobalMouseProfile][L"Wheel down"] = {MouseActionType::Mute};
|
||||||
|
mouse.mouseData = MAKEWPARAM(0, static_cast<WORD>(-30));
|
||||||
|
const auto wheel = [&] { return App::mouseProc(HC_ACTION, WM_MOUSEWHEEL, reinterpret_cast<LPARAM>(&mouse)); };
|
||||||
|
mouse.flags = LLMHF_INJECTED;
|
||||||
|
wheel();
|
||||||
|
const bool ignoredInjection = app.mouseActions.empty();
|
||||||
|
mouse.flags = 0;
|
||||||
|
const bool partial = wheel() == 1 && app.mouseActions.empty();
|
||||||
|
mouse.mouseData = MAKEWPARAM(0, static_cast<WORD>(-90));
|
||||||
|
const bool queued = wheel() == 1 && app.mouseActions.size() == 1 &&
|
||||||
|
app.mouseActions.front().action.type == MouseActionType::Mute;
|
||||||
|
app.mouseActions.clear();
|
||||||
|
app.settings = std::move(previous);
|
||||||
|
return ignoredInjection && partial && queued;
|
||||||
|
}
|
||||||
static bool volumeHoldStopsOnRelease(App &app)
|
static bool volumeHoldStopsOnRelease(App &app)
|
||||||
{
|
{
|
||||||
constexpr int button = 3;
|
constexpr int button = 3;
|
||||||
@@ -203,6 +224,8 @@ int main(int argc, char **argv) {
|
|||||||
require(app.window!=nullptr,"message-only test window");
|
require(app.window!=nullptr,"message-only test window");
|
||||||
require(ks::InputRegressionAccess::volumeHoldStopsOnRelease(app),
|
require(ks::InputRegressionAccess::volumeHoldStopsOnRelease(app),
|
||||||
"volume hold repeats after its delay and stops on release without an extra step");
|
"volume hold repeats after its delay and stops on release without an extra step");
|
||||||
|
require(ks::InputRegressionAccess::wheelQueuesAction(app),
|
||||||
|
"a registered physical wheel signal queues one action per detent");
|
||||||
require(ks::mouseVolumeRepeatInterval(350) > ks::mouseVolumeRepeatInterval(1000) &&
|
require(ks::mouseVolumeRepeatInterval(350) > ks::mouseVolumeRepeatInterval(1000) &&
|
||||||
ks::mouseVolumeRepeatInterval(1000) > ks::mouseVolumeRepeatInterval(2000) &&
|
ks::mouseVolumeRepeatInterval(1000) > ks::mouseVolumeRepeatInterval(2000) &&
|
||||||
!ks::isMouseVolumeStep(ks::MouseActionType::Mute),
|
!ks::isMouseVolumeStep(ks::MouseActionType::Mute),
|
||||||
@@ -828,6 +851,14 @@ int main(int argc, char **argv) {
|
|||||||
require(!IsWindowEnabled(GetDlgItem(settings, IDOK)), "button detection is not a settings edit");
|
require(!IsWindowEnabled(GetDlgItem(settings, IDOK)), "button detection is not a settings edit");
|
||||||
require(ListView_GetItemCount(events) == 8 && Header_GetItemCount(ListView_GetHeader(events)) == 2,
|
require(ListView_GetItemCount(events) == 8 && Header_GetItemCount(ListView_GetHeader(events)) == 2,
|
||||||
"mouse events have eight rows and two columns");
|
"mouse events have eight rows and two columns");
|
||||||
|
SendMessageW(settings, WM_COMMAND, 762, 0);
|
||||||
|
const auto addEvent = FindWindowW(L"KSwitcher.AddMouseEvent", nullptr);
|
||||||
|
require(addEvent && GetDlgItem(addEvent, 764) && GetDlgItem(addEvent, 763) &&
|
||||||
|
!IsWindowEnabled(GetDlgItem(addEvent, IDOK)), "Add mouse event waits for a physical signal");
|
||||||
|
SendMessageW(addEvent, WM_COMMAND, IDOK, 0);
|
||||||
|
require(IsWindow(addEvent) && ListView_GetItemCount(events) == 8,
|
||||||
|
"the event editor cannot save an uncaptured signal");
|
||||||
|
SendMessageW(addEvent, WM_COMMAND, IDCANCEL, 0);
|
||||||
NMITEMACTIVATE click{};
|
NMITEMACTIVATE click{};
|
||||||
click.hdr = {events, 733, NM_DBLCLK};
|
click.hdr = {events, 733, NM_DBLCLK};
|
||||||
click.iItem = 7;
|
click.iItem = 7;
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ int main(){try{
|
|||||||
if (merged.mouseBindings.at(L"chrome").size() != 2)
|
if (merged.mouseBindings.at(L"chrome").size() != 2)
|
||||||
throw std::runtime_error("independent mouse bindings lost");
|
throw std::runtime_error("independent mouse bindings lost");
|
||||||
auto shared = sharedSyncSettings(merged);
|
auto shared = sharedSyncSettings(merged);
|
||||||
if (shared != sharedSyncSettings(base) ||
|
if (shared == sharedSyncSettings(base) ||
|
||||||
restoreSyncSettings(shared, merged, {}, false).mouseBindings != merged.mouseBindings)
|
restoreSyncSettings(shared, Settings{}, {}, false).mouseBindings != merged.mouseBindings)
|
||||||
throw std::runtime_error("mouse profiles must stay local during sync");
|
throw std::runtime_error("mouse profiles must restore from a synchronized backup");
|
||||||
auto withButtons = merged;
|
auto withButtons = merged;
|
||||||
draft = current = withButtons;
|
draft = current = withButtons;
|
||||||
draft.mouseBindings.at(L"chrome").erase(L"XButton1");
|
draft.mouseBindings.at(L"chrome").erase(L"XButton1");
|
||||||
@@ -100,5 +100,38 @@ int main(){try{
|
|||||||
try { validateSettings(profiles); }
|
try { validateSettings(profiles); }
|
||||||
catch (const std::runtime_error &e) { rejected = std::string(e.what()).starts_with("Mouse action conflict:"); }
|
catch (const std::runtime_error &e) { rejected = std::string(e.what()).starts_with("Mouse action conflict:"); }
|
||||||
if (!rejected) throw std::runtime_error("overlapping active profiles must be rejected");
|
if (!rejected) throw std::runtime_error("overlapping active profiles must be rejected");
|
||||||
|
Settings wheel;
|
||||||
|
const int down = mouseWheelEvent(WM_MOUSEWHEEL, -WHEEL_DELTA);
|
||||||
|
RAWMOUSE raw{};
|
||||||
|
raw.usButtonFlags = RI_MOUSE_WHEEL;
|
||||||
|
raw.usButtonData = static_cast<USHORT>(-WHEEL_DELTA);
|
||||||
|
if (rawMouseSignal(raw) != down || mouseEventEnabled(wheel, down))
|
||||||
|
throw std::runtime_error("wheel capture must match the runtime signal and be opt-in");
|
||||||
|
raw.usButtonFlags = RI_MOUSE_HWHEEL;
|
||||||
|
if (rawMouseSignal(raw) != mouseWheelEvent(WM_MOUSEHWHEEL, -WHEEL_DELTA))
|
||||||
|
throw std::runtime_error("horizontal wheel capture must match the runtime signal");
|
||||||
|
int remainder{};
|
||||||
|
if (mouseWheelSteps(remainder, -30) != 0 || mouseWheelSteps(remainder, -90) != 1 ||
|
||||||
|
mouseWheelSteps(remainder, 240) != 2 || remainder != 0)
|
||||||
|
throw std::runtime_error("high-resolution wheel deltas must accumulate by detent");
|
||||||
|
wheel.customMouseEvents.emplace(MouseEvents[down], L"Lower volume");
|
||||||
|
wheel.mouseBindings[GlobalMouseProfile][MouseEvents[down]] = {MouseActionType::VolumeDown};
|
||||||
|
wheel.mouseBindings[L"name:Browser"][L"XButton1"] = {MouseActionType::Back};
|
||||||
|
wheel.mouseProfileApplications[L"name:Browser"] = {L"chrome"};
|
||||||
|
wheel.disabledMouseProfiles.insert(L"name:Browser");
|
||||||
|
wheel.appLanguageSets[L"Browser"] = {{L"win:00000409"}, {L"chrome"}};
|
||||||
|
const auto restoredWheel = decodeSettings(encodeSettings(wheel), {});
|
||||||
|
const auto syncedWheel = restoreSyncSettings(sharedSyncSettings(wheel), Settings{}, {});
|
||||||
|
if (mouseEventName(restoredWheel, down) != L"Lower volume" ||
|
||||||
|
!effectiveMouseAction(restoredWheel, L"explorer", down) ||
|
||||||
|
sharedSyncSettings(syncedWheel) != sharedSyncSettings(wheel))
|
||||||
|
throw std::runtime_error("custom wheel event and action must restore from a synchronized backup");
|
||||||
|
if (!restoreSyncSettings(sharedSyncSettings(Settings{}), wheel, {}).customMouseEvents.empty())
|
||||||
|
throw std::runtime_error("an explicit empty backup must clear custom mouse events");
|
||||||
|
wheel.customMouseEvents.clear();
|
||||||
|
rejected = false;
|
||||||
|
try { validateSettings(wheel); }
|
||||||
|
catch (const std::runtime_error &) { rejected = true; }
|
||||||
|
if (!rejected) throw std::runtime_error("unregistered mouse signals cannot have bindings");
|
||||||
std::cout<<"Settings-save merge checks passed\n";
|
std::cout<<"Settings-save merge checks passed\n";
|
||||||
}catch(const std::exception &e){std::cerr<<e.what()<<'\n';return 1;}}
|
}catch(const std::exception &e){std::cerr<<e.what()<<'\n';return 1;}}
|
||||||
+19
-5
@@ -155,11 +155,25 @@ int main()
|
|||||||
rightScope["snippets"].back()["apps"]=Json::array({"NOTEPAD.exe"});
|
rightScope["snippets"].back()["apps"]=Json::array({"NOTEPAD.exe"});
|
||||||
rejects([&]{mergeSyncSettings(base,bytes(leftScope),bytes(rightScope));}, "sync detects overlapping application scopes");
|
rejects([&]{mergeSyncSettings(base,bytes(leftScope),bytes(rightScope));}, "sync detects overlapping application scopes");
|
||||||
}
|
}
|
||||||
require(!j.contains("startWithSystem") && !j.contains("tesseractPath") && !j.contains("historyEnabled"), "local-only fields excluded");
|
require(j.contains("startWithSystem") && j.contains("tesseractPath") && j.contains("historyEnabled"),
|
||||||
auto restored = restoreSyncSettings(base, local, {});
|
"server backup includes startup, OCR and history settings");
|
||||||
require(restored.autostart && restored.elevatedAutostart && !restored.history && restored.tesseractPath == local.tesseractPath, "preserve device settings");
|
auto restored = restoreSyncSettings(base, Settings{}, {});
|
||||||
auto hostile = j; hostile["startWithSystem"] = false;
|
require(sharedSyncSettings(restored) == base, "restore all settings onto a fresh installation");
|
||||||
rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject injected device setting");
|
auto legacySettings = j;
|
||||||
|
for (auto key : {"startWithSystem", "autostartAsAdministrator", "tesseractPath", "useTesseract",
|
||||||
|
"historyEnabled", "maxItems", "maxTotalMegabytes", "maxImageMegabytes", "writeLog", "preventScreenLock",
|
||||||
|
"appLanguageSets", "mouseBindings", "disabledMouseProfiles", "mouseProfileApplications", "customMouseEvents"})
|
||||||
|
legacySettings.erase(key);
|
||||||
|
FakeProvider legacyProvider;
|
||||||
|
legacyProvider.remote = {3, bytes(legacySettings)};
|
||||||
|
auto recovered = synchronize(legacyProvider, {}, base, SyncAction::Download);
|
||||||
|
require(sharedSyncSettings(restoreSyncSettings(recovered.settings, local, {})) == base &&
|
||||||
|
legacyProvider.writes == 0, "older server backup preserves fields it never stored");
|
||||||
|
auto upgraded = synchronize(legacyProvider, bytes(legacySettings), base, SyncAction::Automatic);
|
||||||
|
require(legacyProvider.writes == 1 && parse(upgraded.settings) == j,
|
||||||
|
"automatic sync upgrades an older server backup");
|
||||||
|
auto hostile = j; hostile["unknownSetting"] = false;
|
||||||
|
rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject unsupported settings");
|
||||||
hostile = j; hostile["schemaVersion"] = 2;
|
hostile = j; hostile["schemaVersion"] = 2;
|
||||||
rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject future schema");
|
rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject future schema");
|
||||||
hostile = j; hostile["layouts"] = Json::array({"00000422"});
|
hostile = j; hostile["layouts"] = Json::array({"00000422"});
|
||||||
|
|||||||
Reference in new issue
Block a user