ui: add UI for LED Zone control
This commit is contained in:
parent
3328a6085f
commit
73d091c86f
|
@ -1152,7 +1152,17 @@ class Backlight:
|
||||||
self.device.feature_request(FEATURE.BACKLIGHT2, 0x10, data_bytes)
|
self.device.feature_request(FEATURE.BACKLIGHT2, 0x10, data_bytes)
|
||||||
|
|
||||||
|
|
||||||
LEDParam = _NamedInts(color=0, speed=1, period=2, intensity=3, ramp=4, form=5)
|
class LEDParam:
|
||||||
|
color = 'color'
|
||||||
|
speed = 'speed'
|
||||||
|
period = 'period'
|
||||||
|
intensity = 'intensity'
|
||||||
|
ramp = 'ramp'
|
||||||
|
form = 'form'
|
||||||
|
|
||||||
|
|
||||||
|
LEDRampChoices = _NamedInts(default=0, yes=1, no=2)
|
||||||
|
LEDFormChoices = _NamedInts(default=0, sine=1, square=2, triangle=3, sawtooth=4, sharkfin=5, exponential=6)
|
||||||
LEDParamSize = {
|
LEDParamSize = {
|
||||||
LEDParam.color: 3,
|
LEDParam.color: 3,
|
||||||
LEDParam.speed: 1,
|
LEDParam.speed: 1,
|
||||||
|
@ -1161,44 +1171,34 @@ LEDParamSize = {
|
||||||
LEDParam.ramp: 1,
|
LEDParam.ramp: 1,
|
||||||
LEDParam.form: 1
|
LEDParam.form: 1
|
||||||
}
|
}
|
||||||
LEDEffects = _NamedInts(
|
LEDEffects = {
|
||||||
Disable=0x00,
|
0x0: [_NamedInt(0x0, _('Disable')), {}],
|
||||||
Fixed=0x01,
|
0x1: [_NamedInt(0x1, _('Fixed')), {
|
||||||
Pulse=0x02,
|
|
||||||
Cycle=0x03,
|
|
||||||
# Wave=0x04, Stars=0x05, Press=0x06, Audio=0x07, # not implemented
|
|
||||||
Boot=0x08,
|
|
||||||
Demo=0x09,
|
|
||||||
Breathe=0x0A,
|
|
||||||
Ripple=0x0B
|
|
||||||
)
|
|
||||||
LEDEffectsParams = {
|
|
||||||
LEDEffects.Disable: {},
|
|
||||||
LEDEffects.Fixed: {
|
|
||||||
LEDParam.color: 0,
|
LEDParam.color: 0,
|
||||||
LEDParam.ramp: 3
|
LEDParam.ramp: 3
|
||||||
},
|
}],
|
||||||
LEDEffects.Pulse: {
|
0x2: [_NamedInt(0x2, _('Pulse')), {
|
||||||
LEDParam.color: 0,
|
LEDParam.color: 0,
|
||||||
LEDParam.speed: 3
|
LEDParam.speed: 3
|
||||||
},
|
}],
|
||||||
LEDEffects.Cycle: {
|
0x3: [_NamedInt(0x3, _('Cycle')), {
|
||||||
LEDParam.period: 5,
|
LEDParam.period: 5,
|
||||||
LEDParam.intensity: 7
|
LEDParam.intensity: 7
|
||||||
},
|
}],
|
||||||
LEDEffects.Boot: {},
|
0x8: [_NamedInt(0x8, _('Boot')), {}],
|
||||||
LEDEffects.Demo: {},
|
0x9: [_NamedInt(0x9, _('Demo')), {}],
|
||||||
LEDEffects.Breathe: {
|
0xA: [_NamedInt(0xA, _('Breathe')), {
|
||||||
LEDParam.color: 0,
|
LEDParam.color: 0,
|
||||||
LEDParam.period: 3,
|
LEDParam.period: 3,
|
||||||
LEDParam.form: 5,
|
LEDParam.form: 5,
|
||||||
LEDParam.intensity: 6
|
LEDParam.intensity: 6
|
||||||
},
|
}],
|
||||||
LEDEffects.Ripple: {
|
0xB: [_NamedInt(0xB, _('Ripple')), {
|
||||||
LEDParam.color: 0,
|
LEDParam.color: 0,
|
||||||
LEDParam.period: 4
|
LEDParam.period: 4
|
||||||
}
|
}]
|
||||||
}
|
}
|
||||||
|
# Wave=0x04, Stars=0x05, Press=0x06, Audio=0x07, # not implemented
|
||||||
|
|
||||||
|
|
||||||
class LEDEffectSetting: # an effect plus its parameters
|
class LEDEffectSetting: # an effect plus its parameters
|
||||||
|
@ -1210,9 +1210,10 @@ class LEDEffectSetting: # an effect plus its parameters
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, bytes):
|
def from_bytes(cls, bytes):
|
||||||
args = {'ID': LEDEffects[bytes[0]]}
|
effect = LEDEffects[bytes[0]] if bytes[0] in LEDEffects else None
|
||||||
if args['ID'] in LEDEffectsParams:
|
args = {'ID': effect[0] if effect else None}
|
||||||
for p, b in LEDEffectsParams[args['ID']].items():
|
if effect:
|
||||||
|
for p, b in effect[1].items():
|
||||||
args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]])
|
args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]])
|
||||||
else:
|
else:
|
||||||
args['bytes'] = bytes
|
args['bytes'] = bytes
|
||||||
|
@ -1224,7 +1225,7 @@ class LEDEffectSetting: # an effect plus its parameters
|
||||||
return self.bytes if self.bytes else b'\xff' * 11
|
return self.bytes if self.bytes else b'\xff' * 11
|
||||||
else:
|
else:
|
||||||
bs = [0] * 10
|
bs = [0] * 10
|
||||||
for p, b in LEDEffectsParams[self.ID].items():
|
for p, b in LEDEffects[self.ID][1].items():
|
||||||
bs[b:b + LEDParamSize[p]] = _int2bytes(getattr(self, str(p), 0), LEDParamSize[p])
|
bs[b:b + LEDParamSize[p]] = _int2bytes(getattr(self, str(p), 0), LEDParamSize[p])
|
||||||
return _int2bytes(ID, 1) + bytes(bs)
|
return _int2bytes(ID, 1) + bytes(bs)
|
||||||
|
|
||||||
|
@ -1238,7 +1239,7 @@ class LEDEffectSetting: # an effect plus its parameters
|
||||||
return dumper.represent_mapping('!LEDEffectSetting', data.__dict__, flow_style=True)
|
return dumper.represent_mapping('!LEDEffectSetting', data.__dict__, flow_style=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _yaml.dump(self).rstrip('\n')
|
return _yaml.dump(self, width=float('inf')).rstrip('\n')
|
||||||
|
|
||||||
|
|
||||||
_yaml.SafeLoader.add_constructor('!LEDEffectSetting', LEDEffectSetting.from_yaml)
|
_yaml.SafeLoader.add_constructor('!LEDEffectSetting', LEDEffectSetting.from_yaml)
|
||||||
|
@ -1253,8 +1254,8 @@ class LEDEffectIndexed(LEDEffectSetting): # an effect plus its parameters, usin
|
||||||
args = {'ID': next((ze.ID for ze in options if ze.index == bytes[0]), None)}
|
args = {'ID': next((ze.ID for ze in options if ze.index == bytes[0]), None)}
|
||||||
else:
|
else:
|
||||||
args = {'ID': None}
|
args = {'ID': None}
|
||||||
if args['ID'] in LEDEffectsParams:
|
if args['ID'] in LEDEffects:
|
||||||
for p, b in LEDEffectsParams[args['ID']].items():
|
for p, b in LEDEffects[args['ID']][1].items():
|
||||||
args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]])
|
args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]])
|
||||||
else:
|
else:
|
||||||
args['bytes'] = bytes
|
args['bytes'] = bytes
|
||||||
|
|
|
@ -1231,14 +1231,8 @@ class HeteroValidator(Validator):
|
||||||
to_write = new_value.to_bytes()
|
to_write = new_value.to_bytes()
|
||||||
return to_write
|
return to_write
|
||||||
|
|
||||||
def acceptable(self, args, current): # FIXME
|
def acceptable(self, args, current): # should this actually do some checking?
|
||||||
if len(args) != 2:
|
return True
|
||||||
return None
|
|
||||||
item = self.items[args[0]] if args[0] in self.items else None
|
|
||||||
if item.kind == KIND.range:
|
|
||||||
return None if args[1] < item.min_value or args[1] > item.max_value else args
|
|
||||||
elif item.kind == KIND.choice:
|
|
||||||
return args if args[1] in item.choices else None
|
|
||||||
|
|
||||||
|
|
||||||
class PackedRangeValidator(Validator):
|
class PackedRangeValidator(Validator):
|
||||||
|
|
|
@ -24,6 +24,8 @@ from struct import pack as _pack
|
||||||
from struct import unpack as _unpack
|
from struct import unpack as _unpack
|
||||||
from time import time as _time
|
from time import time as _time
|
||||||
|
|
||||||
|
import webcolors as _webcolors
|
||||||
|
|
||||||
from . import hidpp10 as _hidpp10
|
from . import hidpp10 as _hidpp10
|
||||||
from . import hidpp20 as _hidpp20
|
from . import hidpp20 as _hidpp20
|
||||||
from . import special_keys as _special_keys
|
from . import special_keys as _special_keys
|
||||||
|
@ -32,6 +34,7 @@ from .common import NamedInts as _NamedInts
|
||||||
from .common import bytes2int as _bytes2int
|
from .common import bytes2int as _bytes2int
|
||||||
from .common import int2bytes as _int2bytes
|
from .common import int2bytes as _int2bytes
|
||||||
from .i18n import _
|
from .i18n import _
|
||||||
|
from .settings import KIND as _KIND
|
||||||
from .settings import ActionSettingRW as _ActionSettingRW
|
from .settings import ActionSettingRW as _ActionSettingRW
|
||||||
from .settings import BitFieldSetting as _BitFieldSetting
|
from .settings import BitFieldSetting as _BitFieldSetting
|
||||||
from .settings import BitFieldValidator as _BitFieldV
|
from .settings import BitFieldValidator as _BitFieldV
|
||||||
|
@ -1430,20 +1433,28 @@ class LEDControl(_Setting):
|
||||||
validator_options = {'choices': choices_universe}
|
validator_options = {'choices': choices_universe}
|
||||||
|
|
||||||
|
|
||||||
# an LED Zone has an index, a set of possible LED effects, and an LED effect setting with parameters
|
colors = _NamedInts()
|
||||||
# the parameters are different for each effect
|
for c, v in _webcolors.CSS3_NAMES_TO_HEX.items():
|
||||||
|
v = int(v[1:], 16)
|
||||||
|
if v not in colors:
|
||||||
|
colors[v] = c
|
||||||
|
_LEDP = _hidpp20.LEDParam
|
||||||
|
|
||||||
|
|
||||||
|
# an LED Zone has an index, a set of possible LED effects, and an LED effect setting
|
||||||
# reading the current setting for a zone returns zeros on some devices
|
# reading the current setting for a zone returns zeros on some devices
|
||||||
class LEDZoneSetting(_Setting):
|
class LEDZoneSetting(_Setting):
|
||||||
name = 'led_zone_'
|
name = 'led_zone_'
|
||||||
label = _('LED Zone Effects')
|
label = _('LED Zone Effects')
|
||||||
description = _('Set effect for LED Zone')
|
description = _('Set effect for LED Zone')
|
||||||
feature = _F.COLOR_LED_EFFECTS
|
feature = _F.COLOR_LED_EFFECTS
|
||||||
|
color_field = {'name': _LEDP.color, 'kind': _KIND.choice, 'label': None, 'choices': colors}
|
||||||
class validator_class(_HeteroV):
|
speed_field = {'name': _LEDP.speed, 'kind': _KIND.range, 'label': _('Speed'), 'min': 0, 'max': 255}
|
||||||
|
period_field = {'name': _LEDP.period, 'kind': _KIND.range, 'label': _('Period'), 'min': 0, 'max': 5000}
|
||||||
@classmethod
|
intensity_field = {'name': _LEDP.intensity, 'kind': _KIND.range, 'label': _('Intensity'), 'min': 0, 'max': 100}
|
||||||
def build(cls, setting_class, device, effects):
|
ramp_field = {'name': _LEDP.ramp, 'kind': _KIND.choice, 'label': _('Ramp'), 'choices': _hidpp20.LEDRampChoices}
|
||||||
return cls(data_class=_hidpp20.LEDEffectIndexed, options=effects)
|
# form_field = { 'name': _LEDP.form, 'kind': _KIND.choice, 'label': _('Form'), 'choices': _hidpp20.LEDFormChoices }
|
||||||
|
possible_fields = [color_field, speed_field, period_field, intensity_field, ramp_field]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, device):
|
def build(cls, device):
|
||||||
|
@ -1452,10 +1463,14 @@ class LEDZoneSetting(_Setting):
|
||||||
for zone in zone_infos:
|
for zone in zone_infos:
|
||||||
prefix = zone.index.to_bytes(1)
|
prefix = zone.index.to_bytes(1)
|
||||||
rw = _FeatureRW(_F.COLOR_LED_EFFECTS, read_fnid=0xE0, write_fnid=0x30, prefix=prefix)
|
rw = _FeatureRW(_F.COLOR_LED_EFFECTS, read_fnid=0xE0, write_fnid=0x30, prefix=prefix)
|
||||||
validator = cls.validator_class.build(cls, device, zone.effects)
|
validator = _HeteroV(data_class=_hidpp20.LEDEffectIndexed, options=zone.effects)
|
||||||
setting = cls(device, rw, validator)
|
setting = cls(device, rw, validator)
|
||||||
setting.name = cls.name + str(int(zone.location))
|
setting.name = cls.name + str(int(zone.location))
|
||||||
setting.label = _('LEDs: ') + str(_hidpp20.LEDZoneLocations[zone.location])
|
setting.label = _('LEDs') + ' ' + str(_hidpp20.LEDZoneLocations[zone.location])
|
||||||
|
choices = [_hidpp20.LEDEffects[e.ID][0] for e in zone.effects]
|
||||||
|
ID_field = {'name': 'ID', 'kind': _KIND.choice, 'label': None, 'choices': choices}
|
||||||
|
setting.possible_fields = [ID_field] + cls.possible_fields
|
||||||
|
setting.fields_map = _hidpp20.LEDEffects
|
||||||
settings.append(setting)
|
settings.append(setting)
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ from logging import getLogger
|
||||||
from threading import Timer as _Timer
|
from threading import Timer as _Timer
|
||||||
|
|
||||||
from gi.repository import Gdk, GLib, Gtk
|
from gi.repository import Gdk, GLib, Gtk
|
||||||
|
from logitech_receiver.hidpp20 import LEDEffectIndexed as _LEDEffectIndexed
|
||||||
from logitech_receiver.settings import KIND as _SETTING_KIND
|
from logitech_receiver.settings import KIND as _SETTING_KIND
|
||||||
from logitech_receiver.settings import SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE
|
from logitech_receiver.settings import SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE
|
||||||
from solaar.i18n import _, ngettext
|
from solaar.i18n import _, ngettext
|
||||||
|
@ -74,6 +75,21 @@ def _write_async(setting, value, sbox, sensitive=True, key=None):
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class ComboBoxText(Gtk.ComboBoxText):
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return int(self.get_active_id())
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
return self.set_active_id(str(int(value)))
|
||||||
|
|
||||||
|
|
||||||
|
class Scale(Gtk.Scale):
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return int(super().get_value())
|
||||||
|
|
||||||
|
|
||||||
class Control():
|
class Control():
|
||||||
|
|
||||||
def __init__(**kwargs):
|
def __init__(**kwargs):
|
||||||
|
@ -93,7 +109,8 @@ class Control():
|
||||||
def layout(self, sbox, label, change, spinner, failed):
|
def layout(self, sbox, label, change, spinner, failed):
|
||||||
sbox.pack_start(label, False, False, 0)
|
sbox.pack_start(label, False, False, 0)
|
||||||
sbox.pack_end(change, False, False, 0)
|
sbox.pack_end(change, False, False, 0)
|
||||||
sbox.pack_end(self, sbox.setting.kind == _SETTING_KIND.range, sbox.setting.kind == _SETTING_KIND.range, 0)
|
fill = sbox.setting.kind == _SETTING_KIND.range or sbox.setting.kind == _SETTING_KIND.hetero
|
||||||
|
sbox.pack_end(self, fill, fill, 0)
|
||||||
sbox.pack_end(spinner, False, False, 0)
|
sbox.pack_end(spinner, False, False, 0)
|
||||||
sbox.pack_end(failed, False, False, 0)
|
sbox.pack_end(failed, False, False, 0)
|
||||||
return self
|
return self
|
||||||
|
@ -510,6 +527,76 @@ class PackedRangeControl(MultipleRangeControl):
|
||||||
self._button.set_tooltip_text(b)
|
self._button.set_tooltip_text(b)
|
||||||
|
|
||||||
|
|
||||||
|
# control an ID key that determines what else to show
|
||||||
|
class HeteroKeyControl(Gtk.HBox, Control):
|
||||||
|
|
||||||
|
def __init__(self, sbox, delegate=None):
|
||||||
|
super().__init__(homogeneous=False, spacing=6)
|
||||||
|
self.init(sbox, delegate)
|
||||||
|
self._items = {}
|
||||||
|
for item in sbox.setting.possible_fields:
|
||||||
|
if item['label']:
|
||||||
|
item_lblbox = Gtk.Label(item['label'])
|
||||||
|
self.pack_start(item_lblbox, False, False, 0)
|
||||||
|
else:
|
||||||
|
item_lblbox = None
|
||||||
|
if item['kind'] == _SETTING_KIND.choice:
|
||||||
|
item_box = ComboBoxText()
|
||||||
|
for entry in item['choices']:
|
||||||
|
item_box.append(str(int(entry)), str(entry))
|
||||||
|
item_box.set_active(0)
|
||||||
|
item_box.connect('changed', self.changed)
|
||||||
|
self.pack_start(item_box, False, False, 0)
|
||||||
|
elif item['kind'] == _SETTING_KIND.range:
|
||||||
|
item_box = Scale()
|
||||||
|
item_box.set_range(item['min'], item['max'])
|
||||||
|
item_box.set_round_digits(0)
|
||||||
|
item_box.set_digits(0)
|
||||||
|
item_box.set_increments(1, 5)
|
||||||
|
item_box.connect('value-changed', self.changed)
|
||||||
|
self.pack_start(item_box, True, True, 0)
|
||||||
|
self._items[str(item['name'])] = (item_lblbox, item_box)
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
result = {}
|
||||||
|
for k, (_lblbox, box) in self._items.items():
|
||||||
|
result[str(k)] = box.get_value()
|
||||||
|
result = _LEDEffectIndexed(**result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_value(self, value):
|
||||||
|
self.set_sensitive(False)
|
||||||
|
for k, v in value.__dict__.items():
|
||||||
|
if k in self._items:
|
||||||
|
(lblbox, box) = self._items[k]
|
||||||
|
box.set_value(v)
|
||||||
|
self.setup_visibles(value.ID)
|
||||||
|
|
||||||
|
def setup_visibles(self, ID):
|
||||||
|
fields = self.sbox.setting.fields_map[ID][1] if ID in self.sbox.setting.fields_map else {}
|
||||||
|
for name, (lblbox, box) in self._items.items():
|
||||||
|
visible = name in fields or name == 'ID'
|
||||||
|
if lblbox:
|
||||||
|
lblbox.set_visible(visible)
|
||||||
|
box.set_visible(visible)
|
||||||
|
|
||||||
|
def changed(self, control):
|
||||||
|
if self.get_sensitive() and control.get_sensitive():
|
||||||
|
if 'ID' in self._items and control == self._items['ID'][1]:
|
||||||
|
self.setup_visibles(int(self._items['ID'][1].get_value()))
|
||||||
|
if hasattr(control, '_timer'):
|
||||||
|
control._timer.cancel()
|
||||||
|
control._timer = _Timer(0.3, lambda: GLib.idle_add(self._write, control))
|
||||||
|
control._timer.start()
|
||||||
|
|
||||||
|
def _write(self, control):
|
||||||
|
control._timer.cancel()
|
||||||
|
delattr(control, '_timer')
|
||||||
|
new_state = self.get_value()
|
||||||
|
if self.sbox.setting._value != new_state:
|
||||||
|
_write_async(self.sbox.setting, new_state, self.sbox)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
|
@ -591,6 +678,8 @@ def _create_sbox(s, device):
|
||||||
control = MultipleRangeControl(sbox, change)
|
control = MultipleRangeControl(sbox, change)
|
||||||
elif s.kind == _SETTING_KIND.packed_range:
|
elif s.kind == _SETTING_KIND.packed_range:
|
||||||
control = PackedRangeControl(sbox, change)
|
control = PackedRangeControl(sbox, change)
|
||||||
|
elif s.kind == _SETTING_KIND.hetero:
|
||||||
|
control = HeteroKeyControl(sbox, change)
|
||||||
else:
|
else:
|
||||||
if _log.isEnabledFor(_WARNING):
|
if _log.isEnabledFor(_WARNING):
|
||||||
_log.warn('setting %s display not implemented', s.label)
|
_log.warn('setting %s display not implemented', s.label)
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -77,6 +77,7 @@ For instructions on installing Solaar see https://pwr-solaar.github.io/Solaar/in
|
||||||
python_requires='>=3.7',
|
python_requires='>=3.7',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'evdev (>= 1.1.2) ; platform_system=="Linux"',
|
'evdev (>= 1.1.2) ; platform_system=="Linux"',
|
||||||
|
'webcolors',
|
||||||
'pyudev (>= 0.13)',
|
'pyudev (>= 0.13)',
|
||||||
'PyYAML (>= 3.12)',
|
'PyYAML (>= 3.12)',
|
||||||
'python-xlib (>= 0.27)',
|
'python-xlib (>= 0.27)',
|
||||||
|
|
Loading…
Reference in New Issue