From fc62ef00addb000d39a4c5af9ed87944efca3f82 Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Wed, 13 May 2026 14:30:02 -0700 Subject: [PATCH] LEDControl / RGBControl: render as Gtk.Switch instead of a 2-option combo (#3215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LED Control on both 0x8070 (COLOR_LED_EFFECTS) and 0x8071 (RGB_EFFECTS) features is a ChoicesValidator with exactly two values, Device=0 and Solaar=1 — semantically a binary toggle dressed up as a dropdown. Render both as a Gtk.Switch (Solaar=on, Device=off) by swapping ChoicesValidator for BooleanValidator with true_value=1 / false_value=0. BooleanValidator's kind is TOGGLE, which the existing config_panel dispatcher already maps to ToggleControl (Gtk.Switch). No UI plumbing changes required. Add a small _pre_read override to migrate persister entries from the old ChoicesValidator (stored as int 0/1) to bool, so the switch widget gets a value it can pass to Gtk.Switch.set_state(). Tooltip cleanup to match the new styling: - "Switch control of LED zones between device and Solaar" → "Allow Solaar to control LED zones." on both control rows. - "LED Control needs to be set to Solaar to be effective." → "LED Control needs to be enabled." on every dependent setting's tooltip. Setting names (led_control / rgb_control) and persister keys are unchanged; only the YAML value type migrates from int to bool on the next read. --- lib/logitech_receiver/settings_templates.py | 37 ++++++++++++++----- .../test_setting_templates.py | 4 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 6e6e7d22..27f5e8da 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -1815,12 +1815,20 @@ class BrightnessControl(settings.Setting): class LEDControl(settings.Setting): name = "led_control" label = _("LED Control") - description = _("Switch control of LED zones between device and Solaar") + description = _("Allow Solaar to control LED zones.") feature = _F.COLOR_LED_EFFECTS rw_options = {"read_fnid": 0x70, "write_fnid": 0x80} - choices_universe = common.NamedInts(Device=0, Solaar=1) - validator_class = settings_validator.ChoicesValidator - validator_options = {"choices": choices_universe} + # Two-state setting — render as a Gtk.Switch rather than a 2-option combo. + # true_value=1 / false_value=0 are the wire bytes for Solaar / Device mode. + validator_class = settings_validator.BooleanValidator + validator_options = {"true_value": 1, "false_value": 0} + + def _pre_read(self, cached, key=None): + # Migrate legacy int values (0/1) stored under the old ChoicesValidator + # to bool so the switch widget gets a value it can set_state() on. + super()._pre_read(cached, key) + if isinstance(self._value, int) and not isinstance(self._value, bool): + self._value = self._value != 0 colors = special_keys.COLORS @@ -1831,7 +1839,7 @@ _LEDP = hidpp20.LEDParam class LEDZoneSetting(settings.Setting): name = "led_zone_" # the trailing underscore signals that this setting creates other settings label = _("LED Zone Effects") - description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.") + description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be enabled.") feature = _F.COLOR_LED_EFFECTS color_field = {"name": _LEDP.color, "kind": settings.Kind.COLOR, "label": _("Color")} speed_field = {"name": _LEDP.speed, "kind": settings.Kind.RANGE, "label": _("Speed"), "min": 0, "max": 255} @@ -1868,18 +1876,27 @@ class LEDZoneSetting(settings.Setting): class RGBControl(settings.Setting): name = "rgb_control" label = _("LED Control") - description = _("Switch control of LED zones between device and Solaar") + description = _("Allow Solaar to control LED zones.") feature = _F.RGB_EFFECTS rw_options = {"read_fnid": 0x50, "write_fnid": 0x50} - choices_universe = common.NamedInts(Device=0, Solaar=1) - validator_class = settings_validator.ChoicesValidator - validator_options = {"choices": choices_universe, "write_prefix_bytes": b"\x01", "read_skip_byte_count": 1} + # Two-state setting — render as a Gtk.Switch rather than a 2-option combo. + # true_value=1 / false_value=0 are the wire bytes for Solaar / Device mode + # returned by GetSWControl after the 1-byte sub-fn echo. + validator_class = settings_validator.BooleanValidator + validator_options = {"true_value": 1, "false_value": 0, "write_prefix_bytes": b"\x01", "read_skip_byte_count": 1} + + def _pre_read(self, cached, key=None): + # Migrate legacy int values (0/1) stored under the old ChoicesValidator + # to bool so the switch widget gets a value it can set_state() on. + super()._pre_read(cached, key) + if isinstance(self._value, int) and not isinstance(self._value, bool): + self._value = self._value != 0 class RGBEffectSetting(LEDZoneSetting): name = "rgb_zone_" # the trailing underscore signals that this setting creates other settings label = _("LED Zone Effects") - description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be set to Solaar to be effective.") + description = _("Set effect for LED Zone") + "\n" + _("LED Control needs to be enabled.") feature = _F.RGB_EFFECTS @classmethod diff --git a/tests/logitech_receiver/test_setting_templates.py b/tests/logitech_receiver/test_setting_templates.py index 582e601f..44f8d844 100644 --- a/tests/logitech_receiver/test_setting_templates.py +++ b/tests/logitech_receiver/test_setting_templates.py @@ -277,7 +277,7 @@ simple_tests = [ fake_hidpp.Response("0A", 0x0420, "0A"), ), Setup( - FeatureTest(settings_templates.LEDControl, 0, 1), + FeatureTest(settings_templates.LEDControl, False, True), fake_hidpp.Response("00", 0x0470), fake_hidpp.Response("01", 0x0480, "01"), ), @@ -295,7 +295,7 @@ simple_tests = [ fake_hidpp.Response("000000000000000101500000", 0x0430, "000000000000000101500000"), ), Setup( - FeatureTest(settings_templates.RGBControl, 0, 1), + FeatureTest(settings_templates.RGBControl, False, True), fake_hidpp.Response("0000", 0x0450), fake_hidpp.Response("010100", 0x0450, "0101"), ),