diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index ebc3209b..8526a731 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -816,6 +816,8 @@ _icons_allowables = {v: k for k, v in _allowables_icons.items()} # the zone index (rgb_zone_1, rgb_zone_2, ...). _SW_CONTROL_DEPENDENT_NAMES = ("rgb_idle_timeout", "rgb_idle_effect", "rgb_sleep_timeout") _SW_CONTROL_DEPENDENT_PREFIXES = ("rgb_zone_",) +# 0x8070 analog of rgb_control/rgb_zone_: led_zone_* rows need led_control on. +_LED_CONTROL_DEPENDENT_PREFIXES = ("led_zone_",) # headset_led_control = whether Solaar holds the live-coloring claim (off lets # another app drive the LEDs). The 0x0620 per-zone painting and the 0x0621 # onboard effect are both live LED control, so both need the claim; per-zone @@ -846,6 +848,25 @@ def _sw_control_blocked(device): return value not in (True, 3) +def _led_control_blocked(device): + """True when the 0x8070 LED Control is off (Device/firmware mode) — its + led_zone_* rows have no effect. Reads setting._value first, then the + persister; accepts the current bool or a legacy int 0/1.""" + persister = getattr(device, "persister", None) + if persister is None: + return False + value = None + for s in getattr(device, "settings", []) or []: + if s.name == "led_control": + value = s._value + break + if value is None: + value = persister.get("led_control") + if value is None: + return False + return value not in (True, 1) + + def _headset_led_blocked(device): """True when the headset's LED Control is off (Device/firmware mode). Reads setting._value first, then the persister; accepts the current bool @@ -919,6 +940,8 @@ def _gate_blocks(device, name): completions can't undo the grey-out when their callbacks land later.""" if name in _SW_CONTROL_DEPENDENT_NAMES or any(name.startswith(p) for p in _SW_CONTROL_DEPENDENT_PREFIXES): return _sw_control_blocked(device) + if any(name.startswith(p) for p in _LED_CONTROL_DEPENDENT_PREFIXES): + return _led_control_blocked(device) if name == "per-key-lighting": return _sw_control_blocked(device) or _zone_effect_blocks_perkey(device) if name in _HEADSET_LED_DEPENDENT_NAMES: @@ -943,6 +966,7 @@ def _apply_rgb_gates(device): if ( name in _SW_CONTROL_DEPENDENT_NAMES or any(name.startswith(p) for p in _SW_CONTROL_DEPENDENT_PREFIXES) + or any(name.startswith(p) for p in _LED_CONTROL_DEPENDENT_PREFIXES) or name == "per-key-lighting" or name in _HEADSET_LED_DEPENDENT_NAMES ): @@ -993,9 +1017,13 @@ def _change_click(button, sbox): # The lock icon on rgb_control, any zone, per-key, or headset_led_control # can change whether a dependent row is functional — re-evaluate the gate. name = sbox.setting.name - if name in ("rgb_control", "per-key-lighting", "headset_led_control", "headset-onboard-effect") or name.startswith( - "rgb_zone_" - ): + if name in ( + "rgb_control", + "led_control", + "per-key-lighting", + "headset_led_control", + "headset-onboard-effect", + ) or name.startswith(("rgb_zone_", "led_zone_")): _apply_rgb_gates(sbox.setting._device) return True @@ -1099,9 +1127,13 @@ def _update_setting_item(sbox, value, is_online=True, sensitive=True, null_okay= logger.warning("%s: error setting control value (%s): %s", sbox.setting.name, sbox.setting._device, repr(e)) sbox._control.set_sensitive(sensitive is True and can_function) _change_icon(sensitive, sbox._change_icon) - # rgb_control / rgb_zone_* gate per-key; headset_led_control and the - # headset-onboard-effect gate the per-zone row — re-evaluate on a change. - if name in ("rgb_control", "headset_led_control", "headset-onboard-effect") or name.startswith("rgb_zone_"): + # A control/zone/onboard-effect change can flip a dependent row's gate. + if name in ( + "rgb_control", + "led_control", + "headset_led_control", + "headset-onboard-effect", + ) or name.startswith(("rgb_zone_", "led_zone_")): _apply_rgb_gates(sbox.setting._device) diff --git a/tests/solaar/ui/test_config_panel_gates.py b/tests/solaar/ui/test_config_panel_gates.py new file mode 100644 index 00000000..2efba909 --- /dev/null +++ b/tests/solaar/ui/test_config_panel_gates.py @@ -0,0 +1,43 @@ +from types import SimpleNamespace + +from solaar.ui import config_panel + + +def _device(settings, persister): + return SimpleNamespace(settings=settings, persister=persister) + + +def _setting(name, value): + return SimpleNamespace(name=name, _value=value) + + +def test_led_control_blocked_reads_setting_value_first(): + device = _device([_setting("led_control", False)], {"led_control": True}) + assert config_panel._led_control_blocked(device) is True + + device = _device([_setting("led_control", True)], {"led_control": False}) + assert config_panel._led_control_blocked(device) is False + + +def test_led_control_blocked_falls_back_to_persister(): + device = _device([_setting("led_control", None)], {"led_control": False}) + assert config_panel._led_control_blocked(device) is True + + +def test_led_control_blocked_unknown_does_not_block(): + device = _device([], {}) + assert config_panel._led_control_blocked(device) is False + + +def test_gate_blocks_led_zone_follows_led_control(): + off = _device([_setting("led_control", False), _setting("led_zone_1", None)], {}) + assert config_panel._gate_blocks(off, "led_zone_1") is True + + on = _device([_setting("led_control", True), _setting("led_zone_1", None)], {}) + assert config_panel._gate_blocks(on, "led_zone_1") is False + + +def test_gate_blocks_rgb_zone_unaffected_by_led_control(): + # rgb_zone_ still keys off rgb_control, not led_control + device = _device([_setting("led_control", False), _setting("rgb_control", True), _setting("rgb_zone_1", None)], {}) + assert config_panel._gate_blocks(device, "rgb_zone_1") is False