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
@@ -125,6 +125,27 @@ struct InputRegressionAccess {
|
||||
return a.notifiedUpdate;
|
||||
}
|
||||
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)
|
||||
{
|
||||
constexpr int button = 3;
|
||||
@@ -203,6 +224,8 @@ int main(int argc, char **argv) {
|
||||
require(app.window!=nullptr,"message-only test window");
|
||||
require(ks::InputRegressionAccess::volumeHoldStopsOnRelease(app),
|
||||
"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) &&
|
||||
ks::mouseVolumeRepeatInterval(1000) > ks::mouseVolumeRepeatInterval(2000) &&
|
||||
!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(ListView_GetItemCount(events) == 8 && Header_GetItemCount(ListView_GetHeader(events)) == 2,
|
||||
"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{};
|
||||
click.hdr = {events, 733, NM_DBLCLK};
|
||||
click.iItem = 7;
|
||||
|
||||
@@ -27,9 +27,9 @@ int main(){try{
|
||||
if (merged.mouseBindings.at(L"chrome").size() != 2)
|
||||
throw std::runtime_error("independent mouse bindings lost");
|
||||
auto shared = sharedSyncSettings(merged);
|
||||
if (shared != sharedSyncSettings(base) ||
|
||||
restoreSyncSettings(shared, merged, {}, false).mouseBindings != merged.mouseBindings)
|
||||
throw std::runtime_error("mouse profiles must stay local during sync");
|
||||
if (shared == sharedSyncSettings(base) ||
|
||||
restoreSyncSettings(shared, Settings{}, {}, false).mouseBindings != merged.mouseBindings)
|
||||
throw std::runtime_error("mouse profiles must restore from a synchronized backup");
|
||||
auto withButtons = merged;
|
||||
draft = current = withButtons;
|
||||
draft.mouseBindings.at(L"chrome").erase(L"XButton1");
|
||||
@@ -100,5 +100,38 @@ int main(){try{
|
||||
try { validateSettings(profiles); }
|
||||
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");
|
||||
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";
|
||||
}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"});
|
||||
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");
|
||||
auto restored = restoreSyncSettings(base, local, {});
|
||||
require(restored.autostart && restored.elevatedAutostart && !restored.history && restored.tesseractPath == local.tesseractPath, "preserve device settings");
|
||||
auto hostile = j; hostile["startWithSystem"] = false;
|
||||
rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject injected device setting");
|
||||
require(j.contains("startWithSystem") && j.contains("tesseractPath") && j.contains("historyEnabled"),
|
||||
"server backup includes startup, OCR and history settings");
|
||||
auto restored = restoreSyncSettings(base, Settings{}, {});
|
||||
require(sharedSyncSettings(restored) == base, "restore all settings onto a fresh installation");
|
||||
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;
|
||||
rejects([&] { restoreSyncSettings(bytes(hostile), local, {}); }, "reject future schema");
|
||||
hostile = j; hostile["layouts"] = Json::array({"00000422"});
|
||||
|
||||
Reference in new issue
Block a user