Grey out LED zone rows when LED Control is off (0x8070)

The 0x8071 path already greys rgb_zone_* rows (and per-key) when
rgb_control is off, so the user sees that those settings have no effect
in Device mode. The 0x8070 zone settings had no such gate: led_zone_*
rows stayed active even with led_control off, implying writes that go
nowhere.

Add the 0x8070 analog — led_zone_* rows gate on led_control, mirroring
rgb_zone_* / rgb_control — wired into the same three points: initial
_apply_rgb_gates, the toggle-time re-gate, and the async-read guard in
_update_setting_item so a late read can't un-grey a blocked row.
This commit is contained in:
Ken Sanislo 2026-07-16 22:27:17 -07:00
parent 3b76be9abb
commit 00a6a59f9e
2 changed files with 81 additions and 6 deletions

View File

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

View File

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