LEDControl / RGBControl: render as Gtk.Switch instead of a 2-option combo (#3215)

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.
This commit is contained in:
Ken Sanislo 2026-05-13 14:30:02 -07:00 committed by GitHub
parent d159081893
commit fc62ef00ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 29 additions and 12 deletions

View File

@ -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

View File

@ -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"),
),