Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ def _validate_types(cfg, defaults, path=""):
print(f"[Config] Type mismatch at {path}.{key}: "
f"expected dict, got {type(cfg[key]).__name__}")
cfg[key] = json.loads(json.dumps(default_val))
elif not isinstance(cfg[key], type(default_val)):
elif not (isinstance(cfg[key], type(default_val)) or (isinstance(default_val, (int, float)) and not isinstance(default_val, bool) and isinstance(cfg[key], (int, float)) and not isinstance(cfg[key], bool))):
print(f"[Config] Type mismatch at {path}.{key}: "
f"expected {type(default_val).__name__}, "
f"got {type(cfg[key]).__name__}")
Expand Down
1 change: 1 addition & 0 deletions core/device_layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
"mx_master_3": "mx_master",
"mx_master_2s": "mx_master",
"mx_anywhere_2s": "mx_anywhere",
"mx_anywhere_2": "mx_anywhere",
"mx_anywhere_3s": "mx_anywhere",
"mx_anywhere_3": "mx_anywhere",
}
Expand Down
2 changes: 1 addition & 1 deletion core/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def _hscroll_step(self, raw_value):

def _hscroll_threshold(self):
return max(
0.1,
0.01,
float(self.cfg.get("settings", {}).get("hscroll_threshold", 1)),
)

Expand Down
36 changes: 30 additions & 6 deletions core/logi_device_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,24 @@ def _layout(
"supported_buttons": MX_ANYWHERE_BUTTONS,
"dpi_max": 4000,
},
{
"key": "mx_anywhere_2",
"display_name": "MX Anywhere 2",
"product_ids": (0xB013, 0xB018, 0xB01F),
"aliases": (
"Wireless Mobile Mouse MX Anywhere 2",
"MX Anywhere 2",
"MX Anywhere 2 for Mac",
),
"ui_layout": "mx_anywhere_2s",
"image_asset": "logitech-mice/mx_anywhere_2s/mouse.png",
"supported_buttons": MX_ANYWHERE_BUTTONS,
"dpi_max": 1600,
},
)



LOGI_DEVICE_LAYOUTS = {
"mx_master_4": _layout(
"mx_master_4",
Expand Down Expand Up @@ -570,14 +585,23 @@ def _layout(
),
_hotspot(
"hscroll_left",
"Horizontal scroll",
"hscroll",
0.38,
0.195,
"Horizontal scroll left",
"mapping",
0.45,
0.20,
label_side="left",
label_off_x=-240,
label_off_y=-70,
is_hscroll=True,
label_off_y=-95,
),
_hotspot(
"hscroll_right",
"Horizontal scroll right",
"mapping",
0.55,
0.20,
label_side="right",
label_off_x=240,
label_off_y=-95,
),
],
),
Expand Down
13 changes: 12 additions & 1 deletion core/mouse_hook_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,18 @@ def _event_tap_callback(self, proxy, event_type, cg_event, refcon):
if self.ignore_trackpad:
is_continuous_field = 88
if Quartz.CGEventGetIntegerValueField(cg_event, is_continuous_field):
return cg_event
scroll_phase = 0
momentum_phase = 0
phase_field = getattr(Quartz, "kCGScrollWheelEventScrollPhase", None)
if phase_field is not None:
scroll_phase = Quartz.CGEventGetIntegerValueField(cg_event, phase_field)
mom_field = getattr(Quartz, "kCGScrollWheelEventMomentumPhase", None)
if mom_field is not None:
momentum_phase = Quartz.CGEventGetIntegerValueField(cg_event, mom_field)
# Only ignore true continuous scrolls from trackpads/Magic Mouse (which have non-zero phases).
# Physical wheels/tilts do not have scroll phases (phase = 0).
if scroll_phase != 0 or momentum_phase != 0:
return cg_event
h_delta = Quartz.CGEventGetIntegerValueField(
cg_event, Quartz.kCGScrollWheelEventFixedPtDeltaAxis2
)
Expand Down
14 changes: 9 additions & 5 deletions tests/test_device_layouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def test_mx_anywhere_2s_layout_identity_and_wheel_tilt_hotspots(self):
)
self.assertEqual(
[hotspot["buttonKey"] for hotspot in layout["hotspots"]],
["middle", "xbutton1", "xbutton2", "hscroll_left"],
["middle", "xbutton1", "xbutton2", "hscroll_left", "hscroll_right"],
)
hotspots = {hotspot["buttonKey"]: hotspot for hotspot in layout["hotspots"]}
self.assertNotIn("gesture_up", hotspots)
Expand All @@ -129,10 +129,14 @@ def test_mx_anywhere_2s_layout_identity_and_wheel_tilt_hotspots(self):
self.assertEqual(hotspots["xbutton2"]["label"], "Forward button")

left = hotspots["hscroll_left"]
self.assertEqual(left["label"], "Horizontal scroll")
self.assertEqual(left["summaryType"], "hscroll")
self.assertTrue(left["isHScroll"])
self.assertNotIn("hscroll_right", hotspots)
self.assertEqual(left["label"], "Horizontal scroll left")
self.assertEqual(left["summaryType"], "mapping")
self.assertFalse(left.get("isHScroll", False))

right = hotspots["hscroll_right"]
self.assertEqual(right["label"], "Horizontal scroll right")
self.assertEqual(right["summaryType"], "mapping")
self.assertFalse(right.get("isHScroll", False))

def test_mx_anywhere_2s_has_no_self_fallback(self):
self.assertEqual(_FAMILY_FALLBACKS.get("mx_anywhere_2s"), "mx_anywhere")
Expand Down
12 changes: 10 additions & 2 deletions tests/test_mouse_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,8 @@ class MacOSTrackpadScrollFilterTests(unittest.TestCase):
def setUp(self):
self.mock_quartz = MagicMock(name="Quartz")
self.mock_quartz.kCGEventScrollWheel = self._kCGEventScrollWheel
self.mock_quartz.kCGScrollWheelEventScrollPhase = 99
self.mock_quartz.kCGScrollWheelEventMomentumPhase = 123
mouse_hook.Quartz = self.mock_quartz

def tearDown(self):
Expand All @@ -901,14 +903,18 @@ def _make_hook(self):
hook.block(mouse_hook.MouseEvent.HSCROLL_RIGHT)
return hook

def _mock_get_field(self, is_continuous, source_user_data=0):
def _mock_get_field(self, is_continuous, source_user_data=0, scroll_phase=0, momentum_phase=0):
"""side_effect: returns is_continuous for field 88, source_user_data
for kCGEventSourceUserData, and 0 for everything else."""
def _get(event, field):
if field == self._kCGScrollWheelEventIsContinuous:
return is_continuous
if field == self.mock_quartz.kCGEventSourceUserData:
return source_user_data
if field == 99: # kCGScrollWheelEventScrollPhase
return scroll_phase
if field == 123: # kCGScrollWheelEventMomentumPhase
return momentum_phase
return 0
return _get

Expand All @@ -917,7 +923,7 @@ def test_trackpad_scroll_passes_through_callback(self):
hook = self._make_hook()
cg_event = MagicMock(name="cg_event")
self.mock_quartz.CGEventGetIntegerValueField.side_effect = \
self._mock_get_field(is_continuous=1)
self._mock_get_field(is_continuous=1, scroll_phase=2) # 2 = Changed phase

result = hook._event_tap_callback(
None, self._kCGEventScrollWheel, cg_event, None)
Expand All @@ -934,6 +940,8 @@ def test_trackpad_hscroll_not_blocked(self):
def _get(event, field):
if field == self._kCGScrollWheelEventIsContinuous:
return 1 # trackpad
if field == 99: # kCGScrollWheelEventScrollPhase
return 2 # Changed phase
if field == self.mock_quartz.kCGScrollWheelEventFixedPtDeltaAxis2:
return 5 * 65536 # non-zero horizontal delta
if field == self.mock_quartz.kCGEventSourceUserData:
Expand Down
16 changes: 13 additions & 3 deletions ui/qml/MousePage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,16 @@ Item {
selectedActionId = mapping ? mapping.actionId : "none"
}

function selectedButtonIsHScroll() {
var hotspots = backend.deviceHotspots
for (var i = 0; i < hotspots.length; i++) {
if (hotspots[i].buttonKey === selectedButton) {
return hotspots[i].isHScroll === true
}
}
return false
}

Connections {
id: mappingsConn
target: backend
Expand Down Expand Up @@ -1256,7 +1266,7 @@ Item {
color: theme.textPrimary
}
Text {
text: selectedButton === "hscroll_left"
text: selectedButton !== "" && selectedButtonIsHScroll()
? s["mouse.configure_scroll_actions"]
: selectedButton === "gesture"
&& backend.supportsGestureDirections
Expand All @@ -1273,7 +1283,7 @@ Item {
Column {
width: parent.width
spacing: 14
visible: selectedButton === "hscroll_left"
visible: selectedButton !== "" && selectedButtonIsHScroll()

Text {
text: s["mouse.scroll_left"]
Expand Down Expand Up @@ -1579,7 +1589,7 @@ Item {
width: parent.width
spacing: 14
visible: selectedButton !== ""
&& selectedButton !== "hscroll_left"
&& !selectedButtonIsHScroll()
&& !(selectedButton === "gesture"
&& backend.supportsGestureDirections)

Expand Down