receiver: implementation of GESTURE 2 params; improved UI for multiple toggle
This commit is contained in:
parent
aa067b2774
commit
c8fe87ee2d
|
@ -739,6 +739,40 @@ PARAM = _NamedInts(
|
|||
)
|
||||
PARAM._fallback = lambda x: 'unknown:%04X' % x
|
||||
|
||||
|
||||
class SubParam:
|
||||
__slots__ = ('id', 'length', 'minimum', 'maximum', 'widget')
|
||||
|
||||
def __init__(self, id, length, minimum=None, maximum=None, widget=None):
|
||||
self.id = id
|
||||
self.length = length
|
||||
self.minimum = minimum if minimum is not None else 0
|
||||
self.maximum = maximum if maximum is not None else ((1 << 8 * length) - 1)
|
||||
self.widget = widget if widget is not None else 'Scale'
|
||||
|
||||
def __str__(self):
|
||||
return self.id
|
||||
|
||||
def __repr__(self):
|
||||
return self.id
|
||||
|
||||
|
||||
SUB_PARAM = { # (byte count, minimum, maximum)
|
||||
PARAM['ExtraCapabilities']: None, # ignore
|
||||
PARAM['PixelZone']: ( # TODO: replace min and max with the correct values
|
||||
SubParam('left', 2, 0x0000, 0xFFFF, 'SpinButton'),
|
||||
SubParam('bottom', 2, 0x0000, 0xFFFF, 'SpinButton'),
|
||||
SubParam('width', 2, 0x0000, 0xFFFF, 'SpinButton'),
|
||||
SubParam('height', 2, 0x0000, 0xFFFF, 'SpinButton')),
|
||||
PARAM['RatioZone']: ( # TODO: replace min and max with the correct values
|
||||
SubParam('left', 1, 0x00, 0xFF, 'SpinButton'),
|
||||
SubParam('bottom', 1, 0x00, 0xFF, 'SpinButton'),
|
||||
SubParam('width', 1, 0x00, 0xFF, 'SpinButton'),
|
||||
SubParam('height', 1, 0x00, 0xFF, 'SpinButton')),
|
||||
PARAM['ScaleFactor']: (
|
||||
SubParam('scale', 2, 0x002E, 0x01FF, 'Scale'), )
|
||||
}
|
||||
|
||||
# Spec Ids for feature GESTURE_2
|
||||
SPEC = _NamedInts(
|
||||
DVI_field_width=1,
|
||||
|
@ -774,7 +808,7 @@ ACTION_ID._fallback = lambda x: 'unknown:%04X' % x
|
|||
|
||||
class Gesture(object):
|
||||
|
||||
index = {}
|
||||
gesture_index = {}
|
||||
|
||||
def __init__(self, device, low, high):
|
||||
self._device = device
|
||||
|
@ -788,8 +822,8 @@ class Gesture(object):
|
|||
self.default_enabled = high & 0x20
|
||||
self.index = None
|
||||
if self.can_be_enabled or self.default_enabled:
|
||||
self.index = Gesture.index.get(device, 0)
|
||||
Gesture.index[device] = self.index + 1
|
||||
self.index = Gesture.gesture_index.get(device, 0)
|
||||
Gesture.gesture_index[device] = self.index + 1
|
||||
self.offset, self.mask = self._offset_mask()
|
||||
|
||||
def _offset_mask(self): # offset and mask
|
||||
|
@ -829,7 +863,7 @@ class Gesture(object):
|
|||
|
||||
|
||||
class Param(object):
|
||||
param_index = 0
|
||||
param_index = {}
|
||||
|
||||
def __init__(self, device, low, high):
|
||||
self._device = device
|
||||
|
@ -839,8 +873,12 @@ class Param(object):
|
|||
self.show_in_ui = bool(high & 0x1F)
|
||||
self._value = None
|
||||
self._default_value = None
|
||||
self.index = Param.param_index
|
||||
Param.param_index += 1
|
||||
self.index = Param.param_index.get(device, 0)
|
||||
Param.param_index[device] = self.index + 1
|
||||
|
||||
@property
|
||||
def sub_params(self):
|
||||
return SUB_PARAM.get(self.id, None)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
|
@ -868,6 +906,12 @@ class Param(object):
|
|||
self._value = bytes
|
||||
return feature_request(self._device, FEATURE.GESTURE_2, 0x80, self.index, bytes, 0xFF)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.param)
|
||||
|
||||
def __int__(self):
|
||||
return self.id
|
||||
|
||||
|
||||
class Spec:
|
||||
def __init__(self, device, low, high):
|
||||
|
@ -1108,6 +1152,8 @@ def get_keys(device):
|
|||
|
||||
|
||||
def get_gestures(device):
|
||||
if getattr(device, '_gestures', None) is not None:
|
||||
return device._gestures
|
||||
if FEATURE.GESTURE_2 in device.features:
|
||||
return Gestures(device)
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ del getLogger
|
|||
#
|
||||
#
|
||||
|
||||
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A, multiple_toggle=0x10)
|
||||
KIND = _NamedInts(toggle=0x01, choice=0x02, range=0x04, map_choice=0x0A, multiple_toggle=0x10, multiple_range=0x40)
|
||||
|
||||
|
||||
class Setting(object):
|
||||
|
@ -286,6 +286,103 @@ class Settings(Setting):
|
|||
return value
|
||||
|
||||
|
||||
class LongSettings(Setting):
|
||||
"""A setting descriptor for multiple choices, being a map from keys to values.
|
||||
Allows multiple write requests, if the options don't fit in 16 bytes.
|
||||
The validator must return a list.
|
||||
Needs to be instantiated for each specific device."""
|
||||
def read(self, cached=True):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings read %r from %s', self.name, self._value, self._device)
|
||||
|
||||
self._pre_read(cached)
|
||||
|
||||
if cached and self._value is not None:
|
||||
return self._value
|
||||
|
||||
if self._device.online:
|
||||
reply_map = {}
|
||||
# Reading one item at a time. This can probably be optimised
|
||||
for item in self._validator.items:
|
||||
r = self._validator.prepare_read_item(item)
|
||||
reply = self._rw.read(self._device, r)
|
||||
if reply:
|
||||
# keys are ints, because that is what the device uses,
|
||||
# encoded into strings because JSON requires strings as keys
|
||||
reply_map[str(int(item))] = self._validator.validate_read_item(reply, item)
|
||||
self._value = reply_map
|
||||
if self.persist and getattr(self._device, 'persister', None) and self.name not in self._device.persister:
|
||||
# Don't update the persister if it already has a value,
|
||||
# otherwise the first read might overwrite the value we wanted.
|
||||
self._device.persister[self.name] = self._value
|
||||
return self._value
|
||||
|
||||
def read_item(self, item, cached=True):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
assert item is not None
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings read %r item %r from %s', self.name, self._value, item, self._device)
|
||||
|
||||
self._pre_read(cached)
|
||||
if cached and self._value is not None:
|
||||
return self._value[str(int(item))]
|
||||
|
||||
if self._device.online:
|
||||
r = self._validator.prepare_read_item(item)
|
||||
reply = self._rw.read(self._device, r)
|
||||
if reply:
|
||||
self._value[str(int(item))] = self._validator.validate_read_item(reply, item)
|
||||
if self.persist and getattr(self._device, 'persister', None) and self.name not in self._device.persister:
|
||||
self._device.persister[self.name] = self._value
|
||||
return self._value[str(int(item))]
|
||||
|
||||
def write(self, map):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
assert map is not None
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings write %r to %s', self.name, map, self._device)
|
||||
if self._device.online:
|
||||
self._value = map
|
||||
self._pre_write()
|
||||
for item, value in map.items():
|
||||
data_bytes_list = self._validator.prepare_write(self._value)
|
||||
if data_bytes_list is not None:
|
||||
for data_bytes in data_bytes_list:
|
||||
if data_bytes is not None:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings prepare map write(%s,%s) => %r', self.name, item, value, data_bytes)
|
||||
reply = self._rw.write(self._device, data_bytes)
|
||||
if not reply:
|
||||
return None
|
||||
return map
|
||||
|
||||
def write_item_value(self, item, value):
|
||||
assert hasattr(self, '_value')
|
||||
assert hasattr(self, '_device')
|
||||
assert item is not None
|
||||
assert value is not None
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings write item %r value %r to %s', self.name, item, value, self._device)
|
||||
|
||||
if self._device.online:
|
||||
data_bytes = self._validator.prepare_write_item(item, value)
|
||||
self._value[str(int(item))] = value
|
||||
self._pre_write()
|
||||
if data_bytes is not None:
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings prepare item value write(%s,%s) => %r', self.name, item, value, data_bytes)
|
||||
reply = self._rw.write(self._device, data_bytes)
|
||||
if not reply:
|
||||
return None
|
||||
return value
|
||||
|
||||
|
||||
class BitFieldSetting(Setting):
|
||||
"""A setting descriptor for a set of choices represented by one bit each, being a map from options to booleans.
|
||||
Needs to be instantiated for each specific device."""
|
||||
|
@ -347,7 +444,6 @@ class BitFieldSetting(Setting):
|
|||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('%s: settings write %r to %s', self.name, map, self._device)
|
||||
|
||||
if self._device.online:
|
||||
self._value = map
|
||||
self._pre_write()
|
||||
|
@ -639,13 +735,15 @@ class BitFieldWithOffsetAndMaskValidator(object):
|
|||
|
||||
def __init__(self, options, byte_count=None):
|
||||
assert (isinstance(options, list))
|
||||
# each element of options must have .offset and .mask,
|
||||
# and its int representation must be its id (not its index)
|
||||
self.options = options
|
||||
# to retrieve the options efficiently:
|
||||
self._option_from_key = {}
|
||||
self._mask_from_offset = {}
|
||||
self._option_from_offset_mask = {}
|
||||
for opt in options:
|
||||
self._option_from_key[opt.gesture] = opt
|
||||
self._option_from_key[int(opt)] = opt
|
||||
try:
|
||||
self._mask_from_offset[opt.offset] |= opt.mask
|
||||
except KeyError:
|
||||
|
@ -848,3 +946,73 @@ class RangeValidator(object):
|
|||
if new_value < self.min_value or new_value > self.max_value:
|
||||
raise ValueError('invalid choice %r' % new_value)
|
||||
return _int2bytes(new_value, self._byte_count)
|
||||
|
||||
|
||||
class MultipleRangeValidator:
|
||||
|
||||
kind = KIND.multiple_range
|
||||
|
||||
def __init__(self, items, sub_items):
|
||||
assert isinstance(items, list) # each element must have .index and its __int__ must return its id (not its index)
|
||||
assert isinstance(sub_items, dict)
|
||||
# sub_items: items -> class with .minimum, .maximum, .length (in bytes), .id (a string) and .widget (e.g. 'Scale')
|
||||
self.items = items
|
||||
self._item_from_id = {int(k): k for k in items}
|
||||
self.sub_items = sub_items
|
||||
|
||||
def prepare_read_item(self, item):
|
||||
return _int2bytes((self._item_from_id[int(item)].index << 1) | 0xFF, 2)
|
||||
|
||||
def validate_read_item(self, reply_bytes, item):
|
||||
item = self._item_from_id[int(item)]
|
||||
start = 0
|
||||
value = {}
|
||||
for sub_item in self.sub_items[item]:
|
||||
r = reply_bytes[start:start + sub_item.length]
|
||||
if len(r) < sub_item.length:
|
||||
r += b'\x00' * (sub_item.length - len(value))
|
||||
v = _bytes2int(r)
|
||||
if not (sub_item.minimum < v < sub_item.maximum):
|
||||
_log.warn(
|
||||
f'{self.__class__.__name__}: failed to validate read value for {item}.{sub_item}: ' +
|
||||
f'{v} not in [{sub_item.minimum}..{sub_item.maximum}]'
|
||||
)
|
||||
value[str(sub_item)] = v
|
||||
start += sub_item.length
|
||||
return value
|
||||
|
||||
def prepare_write(self, value):
|
||||
seq = []
|
||||
w = b''
|
||||
for item in value.keys():
|
||||
_item = self._item_from_id[int(item)]
|
||||
b = _int2bytes(_item.index, 1)
|
||||
for sub_item in self.sub_items[_item]:
|
||||
try:
|
||||
v = value[str(int(item))][str(sub_item)]
|
||||
except KeyError:
|
||||
return None
|
||||
if not (sub_item.minimum <= v <= sub_item.maximum):
|
||||
raise ValueError(
|
||||
f'invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]'
|
||||
)
|
||||
b += _int2bytes(v, sub_item.length)
|
||||
if len(w) + len(b) > 15:
|
||||
seq.append(b + b'\xFF')
|
||||
w = b''
|
||||
w += b
|
||||
seq.append(w + b'\xFF')
|
||||
return seq
|
||||
|
||||
def prepare_write_item(self, item, value):
|
||||
_item = self._item_from_id[int(item)]
|
||||
w = _int2bytes(_item.index, 1)
|
||||
for sub_item in self.sub_items[_item]:
|
||||
try:
|
||||
v = value[str(sub_item)]
|
||||
except KeyError:
|
||||
return None
|
||||
if not (sub_item.minimum <= v <= sub_item.maximum):
|
||||
raise ValueError(f'invalid choice for {item}.{sub_item}: {v} not in [{sub_item.minimum}..{sub_item.maximum}]')
|
||||
w += _int2bytes(v, sub_item.length)
|
||||
return w + b'\xFF'
|
||||
|
|
|
@ -40,6 +40,8 @@ from .settings import ChoicesMapValidator as _ChoicesMapV
|
|||
from .settings import ChoicesValidator as _ChoicesV
|
||||
from .settings import FeatureRW as _FeatureRW
|
||||
from .settings import FeatureRWMap as _FeatureRWMap
|
||||
from .settings import LongSettings as _LongSettings
|
||||
from .settings import MultipleRangeValidator as _MultipleRangeV
|
||||
from .settings import RangeValidator as _RangeV
|
||||
from .settings import RegisterRW as _RegisterRW
|
||||
from .settings import Setting as _Setting
|
||||
|
@ -94,6 +96,7 @@ _THUMB_SCROLL_MODE = ('thumb-scroll-mode', _('HID++ Thumb Scrolling'),
|
|||
_('Effectively turns off thumb scrolling in Linux.'))
|
||||
_THUMB_SCROLL_INVERT = ('thumb-scroll-invert', _('Thumb Scroll Invert'), _('Invert thumb scroll direction.'))
|
||||
_GESTURE2_GESTURES = ('gesture2-gestures', _('Gestures'), _('Tweaks the mouse/touchpad behaviour.'))
|
||||
_GESTURE2_PARAMS = ('gesture2-params', _('Gesture params'), _('Changes numerical parameters of a mouse/touchpad.'))
|
||||
# yapf: enable
|
||||
|
||||
# Setting template functions need to set up the setting itself, the validator, and the reader/writer.
|
||||
|
@ -404,15 +407,31 @@ def _feature_thumb_invert():
|
|||
return _Setting(_THUMB_SCROLL_INVERT, rw, validator, device_kind=(_DK.mouse, _DK.trackball))
|
||||
|
||||
|
||||
def _feature_gesture2_gesture_callback(device):
|
||||
def _feature_gesture2_gestures_callback(device):
|
||||
options = [g for g in _hidpp20.get_gestures(device).gestures.values() if g.can_be_enabled or g.default_enabled]
|
||||
return _BitFieldOMV(options) if options else None
|
||||
|
||||
|
||||
def _feature_gesture2_gesture():
|
||||
def _feature_gesture2_gestures():
|
||||
rw = _FeatureRW(_F.GESTURE_2, read_fnid=0x10, write_fnid=0x20)
|
||||
return _BitFieldOMSetting(
|
||||
_GESTURE2_GESTURES, rw, callback=_feature_gesture2_gesture_callback, device_kind=(_DK.touchpad, _DK.mouse)
|
||||
_GESTURE2_GESTURES, rw, callback=_feature_gesture2_gestures_callback, device_kind=(_DK.touchpad, _DK.mouse)
|
||||
)
|
||||
|
||||
|
||||
def _feature_gesture2_params_callback(device):
|
||||
params = _hidpp20.get_gestures(device).params.values()
|
||||
items = [i for i in params if i.sub_params]
|
||||
if not items:
|
||||
return None
|
||||
sub_items = {i: i.sub_params for i in items}
|
||||
return _MultipleRangeV(items, sub_items)
|
||||
|
||||
|
||||
def _feature_gesture2_params():
|
||||
rw = _FeatureRW(_F.GESTURE_2, read_fnid=0x70, write_fnid=0x80)
|
||||
return _LongSettings(
|
||||
_GESTURE2_PARAMS, rw, callback=_feature_gesture2_params_callback, device_kind=(_DK.touchpad, _DK.mouse)
|
||||
)
|
||||
|
||||
|
||||
|
@ -447,7 +466,8 @@ _SETTINGS_TABLE = [
|
|||
_S(_CHANGE_HOST, _F.CHANGE_HOST, _feature_change_host),
|
||||
_S(_THUMB_SCROLL_MODE, _F.THUMB_WHEEL, _feature_thumb_mode),
|
||||
_S(_THUMB_SCROLL_INVERT, _F.THUMB_WHEEL, _feature_thumb_invert),
|
||||
_S(_GESTURE2_GESTURES, _F.GESTURE_2, _feature_gesture2_gesture),
|
||||
_S(_GESTURE2_GESTURES, _F.GESTURE_2, _feature_gesture2_gestures),
|
||||
_S(_GESTURE2_PARAMS, _F.GESTURE_2, _feature_gesture2_params),
|
||||
]
|
||||
|
||||
_SETTINGS_LIST = namedtuple('_SETTINGS_LIST', [s[4] for s in _SETTINGS_TABLE])
|
||||
|
|
|
@ -40,7 +40,7 @@ def _read_async(setting, force_read, sbox, device_is_online):
|
|||
|
||||
|
||||
def _write_async(setting, value, sbox):
|
||||
_ignore, failed, spinner, control = sbox.get_children()
|
||||
failed, spinner, control = _get_failed_spinner_control(sbox)
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
|
@ -54,7 +54,7 @@ def _write_async(setting, value, sbox):
|
|||
|
||||
|
||||
def _write_async_key_value(setting, key, value, sbox):
|
||||
_ignore, failed, spinner, control = sbox.get_children()
|
||||
failed, spinner, control = _get_failed_spinner_control(sbox)
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
|
@ -67,6 +67,20 @@ def _write_async_key_value(setting, key, value, sbox):
|
|||
_ui_async(_do_write_key_value, setting, key, value, sbox)
|
||||
|
||||
|
||||
def _write_async_item_value(setting, item, value, sbox):
|
||||
failed, spinner, control = _get_failed_spinner_control(sbox)
|
||||
control.set_sensitive(False)
|
||||
failed.set_visible(False)
|
||||
spinner.set_visible(True)
|
||||
spinner.start()
|
||||
|
||||
def _do_write_item_value(s, k, v, sb):
|
||||
v = setting.write_item_value(k, v)
|
||||
GLib.idle_add(_update_setting_item, sb, {k: v}, True, priority=99)
|
||||
|
||||
_ui_async(_do_write_item_value, setting, item, value, sbox)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
@ -174,12 +188,15 @@ def _create_multiple_toggle_control(setting):
|
|||
new_state = control.get_active()
|
||||
if setting._value[key] != new_state:
|
||||
setting._value[key] = new_state
|
||||
_write_async_key_value(setting, key, new_state, control.get_parent().get_parent().get_parent().get_parent())
|
||||
p = control
|
||||
for _ in range(5):
|
||||
p = p.get_parent()
|
||||
_write_async_key_value(setting, key, new_state, p)
|
||||
|
||||
def _toggle_display(lb):
|
||||
lb._showing = not lb._showing
|
||||
if not lb._showing:
|
||||
for c in lb.get_children()[1:]:
|
||||
for c in lb.get_children():
|
||||
lb._hidden_rows.append(c)
|
||||
lb.remove(c)
|
||||
else:
|
||||
|
@ -192,19 +209,94 @@ def _create_multiple_toggle_control(setting):
|
|||
lb._toggle_display = (lambda l: (lambda: _toggle_display(l)))(lb)
|
||||
lb.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
btn = Gtk.Button('? / ?')
|
||||
lb.add(btn)
|
||||
lb._showing = True
|
||||
for k in setting._validator.all_options():
|
||||
h = Gtk.HBox(homogeneous=False, spacing=0)
|
||||
h = Gtk.HBox(homogeneous=True, spacing=0)
|
||||
lbl = Gtk.Label(k)
|
||||
control = Gtk.Switch()
|
||||
control._setting_key = str(int(k))
|
||||
control.connect('notify::active', _toggle_notify, setting)
|
||||
h.pack_start(lbl, False, False, 0)
|
||||
h.pack_end(control, False, False, 0)
|
||||
h.pack_start(lbl, True, True, 0)
|
||||
h.pack_end(control, True, True, 0)
|
||||
lb.add(h)
|
||||
btn.connect('clicked', lambda _: lb._toggle_display())
|
||||
return lb
|
||||
|
||||
hbox = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
hbox.pack_end(btn, True, True, 0)
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=6)
|
||||
vbox.pack_start(hbox, True, True, 0)
|
||||
vbox.pack_end(lb, True, True, 0)
|
||||
return vbox
|
||||
|
||||
|
||||
def _create_multiple_range_control(setting):
|
||||
def _write(control, setting, item, sub_item):
|
||||
control._timer.cancel()
|
||||
delattr(control, '_timer')
|
||||
new_state = int(control.get_value())
|
||||
if setting._value[str(int(item))][str(sub_item)] != new_state:
|
||||
setting._value[str(int(item))][str(sub_item)] = new_state
|
||||
p = control
|
||||
for _i in range(7):
|
||||
p = p.get_parent()
|
||||
_write_async_item_value(setting, str(int(item)), setting._value[str(int(item))], p)
|
||||
|
||||
def _changed(control, setting, item, sub_item):
|
||||
if control.get_sensitive():
|
||||
if hasattr(control, '_timer'):
|
||||
control._timer.cancel()
|
||||
control._timer = _Timer(0.5, lambda: GLib.idle_add(_write, control, setting, item, sub_item))
|
||||
control._timer.start()
|
||||
|
||||
def _toggle_display(lb):
|
||||
lb._showing = not lb._showing
|
||||
if not lb._showing:
|
||||
for c in lb.get_children():
|
||||
lb._hidden_rows.append(c)
|
||||
lb.remove(c)
|
||||
else:
|
||||
for c in lb._hidden_rows:
|
||||
lb.add(c)
|
||||
lb._hidden_rows = []
|
||||
|
||||
lb = Gtk.ListBox()
|
||||
lb._hidden_rows = []
|
||||
lb._toggle_display = (lambda l: (lambda: _toggle_display(l)))(lb)
|
||||
lb.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
btn = Gtk.Button('???')
|
||||
lb._showing = True
|
||||
for item in setting._validator.items:
|
||||
item_lbl = Gtk.Label(item)
|
||||
lb.add(item_lbl)
|
||||
item_lb = Gtk.ListBox()
|
||||
item_lb.set_selection_mode(Gtk.SelectionMode.NONE)
|
||||
for sub_item in setting._validator.sub_items[item]:
|
||||
h = Gtk.HBox(homogeneous=True, spacing=0)
|
||||
sub_item_lbl = Gtk.Label(sub_item)
|
||||
h.pack_start(sub_item_lbl, True, True, 0)
|
||||
if sub_item.widget == 'Scale':
|
||||
control = Gtk.Scale.new_with_range(Gtk.Orientation.HORIZONTAL, sub_item.minimum, sub_item.maximum, 1)
|
||||
control.set_round_digits(0)
|
||||
control.set_digits(0)
|
||||
elif sub_item.widget == 'SpinButton':
|
||||
control = Gtk.SpinButton.new_with_range(sub_item.minimum, sub_item.maximum, 1)
|
||||
control.set_digits(0)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
h.pack_end(control, True, True, 0)
|
||||
control.connect('value-changed', _changed, setting, item, sub_item)
|
||||
item_lb.add(h)
|
||||
h._setting_sub_item = sub_item
|
||||
item_lb._setting_item = item
|
||||
lb.add(item_lb)
|
||||
btn.connect('clicked', lambda _: lb._toggle_display())
|
||||
|
||||
hbox = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
hbox.pack_end(btn, True, True, 0)
|
||||
vbox = Gtk.VBox(homogeneous=False, spacing=6)
|
||||
vbox.pack_start(hbox, True, True, 0)
|
||||
vbox.pack_end(lb, True, True, 0)
|
||||
return vbox
|
||||
|
||||
|
||||
#
|
||||
|
@ -214,7 +306,8 @@ def _create_multiple_toggle_control(setting):
|
|||
|
||||
def _create_sbox(s):
|
||||
sbox = Gtk.HBox(homogeneous=False, spacing=6)
|
||||
sbox.pack_start(Gtk.Label(s.label), False, False, 0)
|
||||
label = Gtk.Label(s.label)
|
||||
sbox.pack_start(label, False, False, 0)
|
||||
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.set_tooltip_text(_('Working') + '...')
|
||||
|
@ -235,15 +328,28 @@ def _create_sbox(s):
|
|||
control = _create_map_choice_control(s)
|
||||
sbox.pack_end(control, True, True, 0)
|
||||
elif s.kind == _SETTING_KIND.multiple_toggle:
|
||||
control = _create_multiple_toggle_control(s)
|
||||
sbox.get_children()[0].set_valign(Gtk.Align.START)
|
||||
sbox.pack_end(control, False, False, 0)
|
||||
vbox = _create_multiple_toggle_control(s)
|
||||
control = vbox.get_children()[1]
|
||||
sbox.remove(label)
|
||||
vbox.get_children()[0].pack_start(label, True, True, 0)
|
||||
sbox.pack_start(vbox, True, True, 0)
|
||||
elif s.kind == _SETTING_KIND.multiple_range:
|
||||
vbox = _create_multiple_range_control(s)
|
||||
control = vbox.get_children()[1]
|
||||
sbox.remove(label)
|
||||
vbox.get_children()[0].pack_start(label, True, True, 0)
|
||||
sbox.pack_start(vbox, True, True, 0)
|
||||
else:
|
||||
raise Exception('NotImplemented')
|
||||
|
||||
control.set_sensitive(False) # the first read will enable it
|
||||
sbox.pack_end(spinner, False, False, 0)
|
||||
sbox.pack_end(failed, False, False, 0)
|
||||
control.kind = s.kind
|
||||
|
||||
if s.kind in [_SETTING_KIND.multiple_toggle, _SETTING_KIND.multiple_range]:
|
||||
vbox.get_children()[0].pack_end(spinner, False, False, 0)
|
||||
vbox.get_children()[0].pack_end(failed, False, False, 0)
|
||||
else:
|
||||
sbox.pack_end(spinner, False, False, 0)
|
||||
sbox.pack_end(failed, False, False, 0)
|
||||
|
||||
if s.description:
|
||||
sbox.set_tooltip_text(s.description)
|
||||
|
@ -257,7 +363,7 @@ def _create_sbox(s):
|
|||
|
||||
|
||||
def _update_setting_item(sbox, value, is_online=True):
|
||||
_ignore, failed, spinner, control = sbox.get_children() # depends on box layout
|
||||
failed, spinner, control = _get_failed_spinner_control(sbox)
|
||||
spinner.set_visible(False)
|
||||
spinner.stop()
|
||||
|
||||
|
@ -279,23 +385,65 @@ def _update_setting_item(sbox, value, is_online=True):
|
|||
if value.get(kbox.get_active_id()):
|
||||
vbox.set_active_id(str(value.get(kbox.get_active_id())))
|
||||
elif isinstance(control, Gtk.ListBox):
|
||||
hidden = getattr(control, '_hidden_rows', [])
|
||||
total = len(control.get_children()) + len(hidden) - 1
|
||||
active = 0
|
||||
for ch in control.get_children()[1:] + hidden:
|
||||
elem = ch.get_children()[0].get_children()[-1]
|
||||
v = value.get(elem._setting_key, None)
|
||||
if v is not None:
|
||||
elem.set_active(v)
|
||||
if elem.get_active():
|
||||
active += 1
|
||||
control.get_children()[0].get_children()[0].set_label(f'{active} / {total}')
|
||||
|
||||
if control.kind == _SETTING_KIND.multiple_toggle:
|
||||
hidden = getattr(control, '_hidden_rows', [])
|
||||
total = len(control.get_children()) + len(hidden)
|
||||
active = 0
|
||||
to_join = []
|
||||
for ch in control.get_children() + hidden:
|
||||
elem = ch.get_children()[0].get_children()[-1]
|
||||
v = value.get(elem._setting_key, None)
|
||||
if v is not None:
|
||||
elem.set_active(v)
|
||||
if elem.get_active():
|
||||
active += 1
|
||||
to_join.append(elem.get_parent().get_children()[0].get_text() + ': ' + str(elem.get_active()))
|
||||
b = ', '.join(to_join)
|
||||
btn = control.get_parent().get_children()[0].get_children()[-1]
|
||||
btn.set_label(f'{active} / {total}')
|
||||
btn.set_tooltip_text(b)
|
||||
elif control.kind == _SETTING_KIND.multiple_range:
|
||||
hidden = getattr(control, '_hidden_rows', [])
|
||||
b = ''
|
||||
n = 0
|
||||
for ch in control.get_children()[1:] + hidden:
|
||||
# item
|
||||
item = ch.get_children()[0]._setting_item
|
||||
v = value.get(str(int(item)), None)
|
||||
if v is not None:
|
||||
b += str(item) + ': ('
|
||||
to_join = []
|
||||
for c in ch.get_children()[0].get_children():
|
||||
# sub-item
|
||||
row = c.get_children()[0]
|
||||
sub_item = row._setting_sub_item
|
||||
elem = row.get_children()[-1]
|
||||
elem.set_value(v[str(sub_item)])
|
||||
n += 1
|
||||
to_join.append(str(sub_item) + f'={v[str(sub_item)]}')
|
||||
b += ', '.join(to_join) + ') '
|
||||
btn = control.get_parent().get_children()[0].get_children()[-1]
|
||||
btn.set_label(f'{n} value' + ('s' if n != 1 else '')) # TODO: i18n, singular/plural
|
||||
btn.set_tooltip_text(b)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
else:
|
||||
raise Exception('NotImplemented')
|
||||
control.set_sensitive(True)
|
||||
|
||||
|
||||
def _get_failed_spinner_control(sbox):
|
||||
children = sbox.get_children()
|
||||
if len(children) == 4:
|
||||
_ignore, failed, spinner, control = sbox.get_children() # depends on box layout
|
||||
else:
|
||||
assert len(children) == 1
|
||||
control = children[0].get_children()[-1]
|
||||
failed = children[0].get_children()[0].get_children()[1]
|
||||
spinner = children[0].get_children()[0].get_children()[2]
|
||||
return failed, spinner, control
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
|
Loading…
Reference in New Issue