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)
|
||||
|
||||
|
||||
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 = {
|
||||
LEDParam.color: 3,
|
||||
LEDParam.speed: 1,
|
||||
|
@ -1161,44 +1171,34 @@ LEDParamSize = {
|
|||
LEDParam.ramp: 1,
|
||||
LEDParam.form: 1
|
||||
}
|
||||
LEDEffects = _NamedInts(
|
||||
Disable=0x00,
|
||||
Fixed=0x01,
|
||||
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: {
|
||||
LEDEffects = {
|
||||
0x0: [_NamedInt(0x0, _('Disable')), {}],
|
||||
0x1: [_NamedInt(0x1, _('Fixed')), {
|
||||
LEDParam.color: 0,
|
||||
LEDParam.ramp: 3
|
||||
},
|
||||
LEDEffects.Pulse: {
|
||||
}],
|
||||
0x2: [_NamedInt(0x2, _('Pulse')), {
|
||||
LEDParam.color: 0,
|
||||
LEDParam.speed: 3
|
||||
},
|
||||
LEDEffects.Cycle: {
|
||||
}],
|
||||
0x3: [_NamedInt(0x3, _('Cycle')), {
|
||||
LEDParam.period: 5,
|
||||
LEDParam.intensity: 7
|
||||
},
|
||||
LEDEffects.Boot: {},
|
||||
LEDEffects.Demo: {},
|
||||
LEDEffects.Breathe: {
|
||||
}],
|
||||
0x8: [_NamedInt(0x8, _('Boot')), {}],
|
||||
0x9: [_NamedInt(0x9, _('Demo')), {}],
|
||||
0xA: [_NamedInt(0xA, _('Breathe')), {
|
||||
LEDParam.color: 0,
|
||||
LEDParam.period: 3,
|
||||
LEDParam.form: 5,
|
||||
LEDParam.intensity: 6
|
||||
},
|
||||
LEDEffects.Ripple: {
|
||||
}],
|
||||
0xB: [_NamedInt(0xB, _('Ripple')), {
|
||||
LEDParam.color: 0,
|
||||
LEDParam.period: 4
|
||||
}
|
||||
}]
|
||||
}
|
||||
# Wave=0x04, Stars=0x05, Press=0x06, Audio=0x07, # not implemented
|
||||
|
||||
|
||||
class LEDEffectSetting: # an effect plus its parameters
|
||||
|
@ -1210,9 +1210,10 @@ class LEDEffectSetting: # an effect plus its parameters
|
|||
|
||||
@classmethod
|
||||
def from_bytes(cls, bytes):
|
||||
args = {'ID': LEDEffects[bytes[0]]}
|
||||
if args['ID'] in LEDEffectsParams:
|
||||
for p, b in LEDEffectsParams[args['ID']].items():
|
||||
effect = LEDEffects[bytes[0]] if bytes[0] in LEDEffects else None
|
||||
args = {'ID': effect[0] if effect else None}
|
||||
if effect:
|
||||
for p, b in effect[1].items():
|
||||
args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]])
|
||||
else:
|
||||
args['bytes'] = bytes
|
||||
|
@ -1224,7 +1225,7 @@ class LEDEffectSetting: # an effect plus its parameters
|
|||
return self.bytes if self.bytes else b'\xff' * 11
|
||||
else:
|
||||
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])
|
||||
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)
|
||||
|
||||
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)
|
||||
|
@ -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)}
|
||||
else:
|
||||
args = {'ID': None}
|
||||
if args['ID'] in LEDEffectsParams:
|
||||
for p, b in LEDEffectsParams[args['ID']].items():
|
||||
if args['ID'] in LEDEffects:
|
||||
for p, b in LEDEffects[args['ID']][1].items():
|
||||
args[str(p)] = _bytes2int(bytes[1 + b:1 + b + LEDParamSize[p]])
|
||||
else:
|
||||
args['bytes'] = bytes
|
||||
|
|
|
@ -1231,14 +1231,8 @@ class HeteroValidator(Validator):
|
|||
to_write = new_value.to_bytes()
|
||||
return to_write
|
||||
|
||||
def acceptable(self, args, current): # FIXME
|
||||
if len(args) != 2:
|
||||
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
|
||||
def acceptable(self, args, current): # should this actually do some checking?
|
||||
return True
|
||||
|
||||
|
||||
class PackedRangeValidator(Validator):
|
||||
|
|
|
@ -24,6 +24,8 @@ from struct import pack as _pack
|
|||
from struct import unpack as _unpack
|
||||
from time import time as _time
|
||||
|
||||
import webcolors as _webcolors
|
||||
|
||||
from . import hidpp10 as _hidpp10
|
||||
from . import hidpp20 as _hidpp20
|
||||
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 int2bytes as _int2bytes
|
||||
from .i18n import _
|
||||
from .settings import KIND as _KIND
|
||||
from .settings import ActionSettingRW as _ActionSettingRW
|
||||
from .settings import BitFieldSetting as _BitFieldSetting
|
||||
from .settings import BitFieldValidator as _BitFieldV
|
||||
|
@ -1430,20 +1433,28 @@ class LEDControl(_Setting):
|
|||
validator_options = {'choices': choices_universe}
|
||||
|
||||
|
||||
# an LED Zone has an index, a set of possible LED effects, and an LED effect setting with parameters
|
||||
# the parameters are different for each effect
|
||||
colors = _NamedInts()
|
||||
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
|
||||
class LEDZoneSetting(_Setting):
|
||||
name = 'led_zone_'
|
||||
label = _('LED Zone Effects')
|
||||
description = _('Set effect for LED Zone')
|
||||
feature = _F.COLOR_LED_EFFECTS
|
||||
|
||||
class validator_class(_HeteroV):
|
||||
|
||||
@classmethod
|
||||
def build(cls, setting_class, device, effects):
|
||||
return cls(data_class=_hidpp20.LEDEffectIndexed, options=effects)
|
||||
color_field = {'name': _LEDP.color, 'kind': _KIND.choice, 'label': None, 'choices': colors}
|
||||
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}
|
||||
intensity_field = {'name': _LEDP.intensity, 'kind': _KIND.range, 'label': _('Intensity'), 'min': 0, 'max': 100}
|
||||
ramp_field = {'name': _LEDP.ramp, 'kind': _KIND.choice, 'label': _('Ramp'), 'choices': _hidpp20.LEDRampChoices}
|
||||
# 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
|
||||
def build(cls, device):
|
||||
|
@ -1452,10 +1463,14 @@ class LEDZoneSetting(_Setting):
|
|||
for zone in zone_infos:
|
||||
prefix = zone.index.to_bytes(1)
|
||||
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.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)
|
||||
return settings
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from logging import getLogger
|
|||
from threading import Timer as _Timer
|
||||
|
||||
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 SENSITIVITY_IGNORE as _SENSITIVITY_IGNORE
|
||||
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():
|
||||
|
||||
def __init__(**kwargs):
|
||||
|
@ -93,7 +109,8 @@ class Control():
|
|||
def layout(self, sbox, label, change, spinner, failed):
|
||||
sbox.pack_start(label, 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(failed, False, False, 0)
|
||||
return self
|
||||
|
@ -510,6 +527,76 @@ class PackedRangeControl(MultipleRangeControl):
|
|||
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)
|
||||
elif s.kind == _SETTING_KIND.packed_range:
|
||||
control = PackedRangeControl(sbox, change)
|
||||
elif s.kind == _SETTING_KIND.hetero:
|
||||
control = HeteroKeyControl(sbox, change)
|
||||
else:
|
||||
if _log.isEnabledFor(_WARNING):
|
||||
_log.warn('setting %s display not implemented', s.label)
|
||||
|
|
Loading…
Reference in New Issue