ui: hide unsupported settings/keys/values

This commit is contained in:
Vinícius 2022-01-19 23:39:25 -03:00 committed by Peter F. Patel-Schneider
parent a4e30a7190
commit b9a23f133e
3 changed files with 408 additions and 160 deletions

View File

@ -763,7 +763,7 @@ class BitFieldValidator(Validator):
w |= int(k) w |= int(k)
return _int2bytes(w, self.byte_count) return _int2bytes(w, self.byte_count)
def all_options(self): def get_options(self):
return self.options return self.options
def acceptable(self, args, current): def acceptable(self, args, current):
@ -854,7 +854,7 @@ class BitFieldWithOffsetAndMaskValidator(Validator):
| value, 2 * self.byte_count + 2) for offset, value in w.items() | value, 2 * self.byte_count + 2) for offset, value in w.items()
] ]
def all_options(self): def get_options(self):
return [int(opt) if isinstance(opt, int) else opt.as_int() for opt in self.options] return [int(opt) if isinstance(opt, int) else opt.as_int() for opt in self.options]
def acceptable(self, args, current): def acceptable(self, args, current):

View File

@ -286,7 +286,7 @@ class MultipleToggleControl(Gtk.ListBox, MultipleControl):
self.set_no_show_all(True) self.set_no_show_all(True)
self._showing = True self._showing = True
self._label_control_pairs = [] self._label_control_pairs = []
for k in sbox.setting._validator.all_options(): for k in sbox.setting._validator.get_options():
h = Gtk.HBox(homogeneous=False, spacing=0) h = Gtk.HBox(homogeneous=False, spacing=0)
lbl_text = str(k) lbl_text = str(k)
lbl_tooltip = None lbl_tooltip = None

View File

@ -16,11 +16,15 @@
## with this program; if not, write to the Free Software Foundation, Inc., ## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import string import string
import threading
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager as contextlib_contextmanager from contextlib import contextmanager as contextlib_contextmanager
from copy import copy
from dataclasses import dataclass, field
from logging import getLogger from logging import getLogger
from shlex import quote as shlex_quote from shlex import quote as shlex_quote
from typing import Dict
from gi.repository import Gdk, GObject, Gtk from gi.repository import Gdk, GObject, Gtk
from logitech_receiver import diversion as _DIV from logitech_receiver import diversion as _DIV
@ -675,6 +679,7 @@ class DiversionDialog:
def update_devices(self): def update_devices(self):
for rc in self.ui.values(): for rc in self.ui.values():
rc.update_devices() rc.update_devices()
self.view.queue_draw()
## Not currently used ## Not currently used
@ -698,15 +703,265 @@ class CompletionEntry(Gtk.Entry):
@classmethod @classmethod
def add_completion_to_entry(cls, entry, values): def add_completion_to_entry(cls, entry, values):
entry.liststore = Gtk.ListStore(str) completion = entry.get_completion()
for v in sorted(values, key=str.casefold): if not completion:
entry.liststore.append((v, )) liststore = Gtk.ListStore(str)
entry.completion = Gtk.EntryCompletion() completion = Gtk.EntryCompletion()
entry.completion.set_model(entry.liststore) completion.set_model(liststore)
norm = lambda s: s.replace('_', '').replace(' ', '').lower() norm = lambda s: s.replace('_', '').replace(' ', '').lower()
entry.completion.set_match_func(lambda completion, key, it: norm(key) in norm(completion.get_model()[it][0])) completion.set_match_func(lambda completion, key, it: norm(key) in norm(completion.get_model()[it][0]))
entry.completion.set_text_column(0) completion.set_text_column(0)
entry.set_completion(entry.completion) entry.set_completion(completion)
else:
liststore = completion.get_model()
liststore.clear()
for v in sorted(set(values), key=str.casefold):
liststore.append((v, ))
class SmartComboBox(Gtk.ComboBox):
"""A custom ComboBox with some extra features.
The constructor requires a collection of allowed values.
Each element must be a single value or a non-empty tuple containing:
- a value (any hashable object)
- a name (optional; str(value) is used if not provided)
- alternative names.
Example: (some_object, 'object name', 'other name', 'also accept this').
It is assumed that the same string cannot be the name or an
alternative name of more than one value.
The widget displays the names, but the alternative names are also suggested and accepted as input.
For values that are `int` instances (including `NamedInt`s), their numerical values are also accepted if typed by the user.
If `has_entry` is `True`, then the user can insert arbitrary text (possibly with auto-complete if `completion` is True).
Otherwise, only a drop-down list is shown, with an extra blank item in the beginning (correspondent to `None`).
The display text of the blank item is defined by the parameter `blank`.
"""
def __init__(self, all_values, blank='', completion=False, **kwargs):
super().__init__(**kwargs)
self._name_to_idx = {}
self._value_to_idx = {}
self._hidden_idx = set()
self._all_values = []
self._blank = blank
self._model = None
self._commpletion = completion
self.set_id_column(0)
if self.get_has_entry():
self.set_entry_text_column(1)
else:
renderer = Gtk.CellRendererText()
self.pack_start(renderer, True)
self.add_attribute(renderer, 'text', 1)
self.set_all_values(all_values)
self.set_active_id('')
@classmethod
def new_model(cls):
model = Gtk.ListStore(str, str, bool)
# (index: int converted to str, name: str, visible: bool)
filtered_model = model.filter_new()
filtered_model.set_visible_column(2)
return model, filtered_model
def set_all_values(self, all_values, visible_fn=(lambda value: True)):
old_value = self.get_value()
self._name_to_idx = {}
self._value_to_idx = {}
self._hidden_idx = set()
self._all_values = [v if isinstance(v, tuple) else (v, ) for v in all_values]
model, filtered_model = SmartComboBox.new_model()
# creating a new model seems to be necessary to avoid firing 'changed' event once per inserted item
model.append(('', self._blank, True))
self._model = model
to_complete = [self._blank]
for idx, item in enumerate(self._all_values):
value, *names = item if isinstance(item, tuple) else (item, )
visible = visible_fn(value)
self._include(model, idx, value, visible, *names)
if visible:
to_complete += names if names else [str(value).strip()]
self.set_model(filtered_model)
if self.get_has_entry() and self._commpletion:
CompletionEntry.add_completion_to_entry(self.get_child(), to_complete)
if self._find_idx(old_value) is not None:
self.set_value(old_value)
else:
self.set_value(self._blank)
self.queue_draw()
def _include(self, model, idx, value, visible, *names):
name = str(names[0]) if names else str(value).strip()
self._name_to_idx[name] = idx
if isinstance(value, NamedInt):
self._name_to_idx[str(name)] = idx
model.append((str(idx), name, visible))
for alt in names[1:]:
self._name_to_idx[str(alt).strip()] = idx
self._value_to_idx[value] = idx
def get_value(self, invalid_as_str=True, accept_hidden=True):
"""Return the selected value or the typed text.
If the typed or selected text corresponds to one of the allowed values (or their names and
alternative names), then the value is returned.
Otherwise, the raw text is returned as string if the widget has an entry and `invalid_as_str`
is `True`; if the widget has no entry or `invalid_as_str` is `False`, then `None` is returned.
"""
tree_iter = self.get_active_iter()
if tree_iter is not None:
t = self.get_model()[tree_iter]
number = t[0]
return self._all_values[int(number)][0] if number != '' and (accept_hidden or t[2]) else None
elif self.get_has_entry() and invalid_as_str:
text = self.get_child().get_text().strip()
if text == self._blank:
return None
idx = self._find_idx(text)
if idx is None:
return text
item = self._all_values[idx]
return item[0] if len(item) > 1 else str(item[0])
return None
def _find_idx(self, search):
if search == self._blank:
return None
try:
return self._value_to_idx[search]
except KeyError:
pass
try:
return self._name_to_idx[search]
except KeyError:
pass
if isinstance(search, str) and search.isdigit():
try:
return self._value_to_idx[int(search)]
except KeyError:
pass
return None
def set_value(self, value, accept_invalid=True):
"""Set a specific value.
Raw values, their names and alternative names are accepted.
Base-10 representations of int values as strings are also accepted.
The actual value is used in all cases.
If `value` is invalid, then the entry text is set to the provided value
if the widget has an entry and `accept_invalid` is True, or else the blank value is set.
"""
idx = self._find_idx(value) if value != self._blank else ''
if idx is not None:
self.set_active_id(str(idx))
else:
if self.get_has_entry() and accept_invalid:
self.get_child().set_text(str(value or '') if value != '' else self._blank)
else:
self.set_active_id('')
def show_only(self, only, include_new=False):
"""Hide items not present in `only`.
Only values are accepted (not their names and alternative names).
If `include_new` is True, then the values in `only` not currently present
are included with their string representation as names; otherwise,
they are ignored.
If `only` is new, then the visibility status is reset and all values are shown.
"""
values = self._all_values[:]
if include_new and only is not None:
values += [v for v in only if v not in self._value_to_idx]
self.set_all_values(values, (lambda v: only is None or (v in only)))
@dataclass
class DeviceInfo:
serial: str = ''
unitId: str = ''
codename: str = ''
settings: Dict[str, _Setting] = field(default_factory=dict)
@property
def id(self):
return self.serial or self.unitId or ''
@property
def identifiers(self):
return [id for id in (self.serial, self.unitId) if id]
@property
def display_name(self):
return f'{self.codename} ({self.id})'
def __post_init__(self):
if self.serial is None or self.serial == '?':
self.serial = ''
if self.unitId is None or self.unitId == '?':
self.unitId = ''
def matches(self, search):
return search and search in (self.serial, self.unitId, self.display_name)
def update(self, device):
for k in ('serial', 'unitId', 'codename', 'settings'):
if not getattr(self, k, None):
v = getattr(device, k, None)
if v and v != '?':
setattr(self, k, copy(v) if k != 'settings' else {s.name: s for s in v})
@classmethod
def from_device(cls, device):
d = DeviceInfo()
d.update(device)
return d
class AllDevicesInfo:
def __init__(self):
self._devices = []
self._lock = threading.Lock()
def __iter__(self):
return iter(self._devices)
def __getitem__(self, search):
if not search:
return search
assert isinstance(search, str)
# linear search - ok because it is always a small list
return next((d for d in self._devices if d.matches(search)), None)
def refresh(self):
updated = False
def dev_in_row(_store, _treepath, row):
nonlocal updated
device = _dev_model.get_value(row, 7)
if device and device.kind and (device.serial and device.serial != '?' or device.unitId and device.unitId != '?'):
existing = self[device.serial] or self[device.unitId]
if not existing:
updated = True
self._devices.append(DeviceInfo.from_device(device))
elif not existing.settings and device.settings:
updated = True
existing.update(device)
with self._lock:
_dev_model.foreach(dev_in_row)
return updated
class RuleComponentUI: class RuleComponentUI:
@ -717,7 +972,7 @@ class RuleComponentUI:
self.panel = panel self.panel = panel
self.widgets = {} # widget -> coord. in grid self.widgets = {} # widget -> coord. in grid
self.component = None self.component = None
self._ignore_changes = False self._ignore_changes = 0
self._on_update_callback = (lambda: None) if on_update is None else on_update self._on_update_callback = (lambda: None) if on_update is None else on_update
self.create_widgets() self.create_widgets()
@ -733,9 +988,9 @@ class RuleComponentUI:
@contextlib_contextmanager @contextlib_contextmanager
def ignore_changes(self): def ignore_changes(self):
self._ignore_changes = True self._ignore_changes += 1
yield None yield None
self._ignore_changes = False self._ignore_changes -= 1
def _on_update(self, *_args): def _on_update(self, *_args):
if not self._ignore_changes and self.component is not None: if not self._ignore_changes and self.component is not None:
@ -1311,10 +1566,10 @@ class MouseScrollUI(ActionUI):
self.label_y = Gtk.Label(label='y', halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True) self.label_y = Gtk.Label(label='y', halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True)
self.field_x = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1) self.field_x = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
self.field_y = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1) self.field_y = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
for field in [self.field_x, self.field_y]: for f in [self.field_x, self.field_y]:
field.set_halign(Gtk.Align.CENTER) f.set_halign(Gtk.Align.CENTER)
field.set_valign(Gtk.Align.START) f.set_valign(Gtk.Align.START)
field.set_vexpand(True) f.set_vexpand(True)
self.field_x.connect('changed', self._on_update) self.field_x.connect('changed', self._on_update)
self.field_y.connect('changed', self._on_update) self.field_y.connect('changed', self._on_update)
self.widgets[self.label_x] = (0, 0, 1, 1) self.widgets[self.label_x] = (0, 0, 1, 1)
@ -1364,10 +1619,10 @@ class MouseClickUI(ActionUI):
self.label_c = Gtk.Label(label=_('Count'), halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True) self.label_c = Gtk.Label(label=_('Count'), halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True)
self.field_b = CompletionEntry(self.BUTTONS) self.field_b = CompletionEntry(self.BUTTONS)
self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1) self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
for field in [self.field_b, self.field_c]: for f in [self.field_b, self.field_c]:
field.set_halign(Gtk.Align.CENTER) f.set_halign(Gtk.Align.CENTER)
field.set_valign(Gtk.Align.START) f.set_valign(Gtk.Align.START)
field.set_vexpand(True) f.set_vexpand(True)
self.field_b.connect('changed', self._on_update) self.field_b.connect('changed', self._on_update)
self.field_c.connect('changed', self._on_update) self.field_c.connect('changed', self._on_update)
self.widgets[self.label_b] = (0, 0, 1, 1) self.widgets[self.label_b] = (0, 0, 1, 1)
@ -1489,7 +1744,7 @@ class SetValueControl(Gtk.HBox):
self.toggle_widget.connect('changed', self._changed) self.toggle_widget.connect('changed', self._changed)
self.range_widget = Gtk.SpinButton.new_with_range(0, 0xFFFF, 1) self.range_widget = Gtk.SpinButton.new_with_range(0, 0xFFFF, 1)
self.range_widget.connect('changed', self._changed) self.range_widget.connect('changed', self._changed)
self.choice_widget = Gtk.ComboBoxText.new_with_entry() self.choice_widget = SmartComboBox([], completion=True, has_entry=True)
self.choice_widget.connect('changed', self._changed) self.choice_widget.connect('changed', self._changed)
self.unsupported_label = Gtk.Label(_('Unsupported setting')) self.unsupported_label = Gtk.Label(_('Unsupported setting'))
for w in [self.toggle_widget, self.range_widget, self.choice_widget, self.unsupported_label]: for w in [self.toggle_widget, self.range_widget, self.choice_widget, self.unsupported_label]:
@ -1503,7 +1758,7 @@ class SetValueControl(Gtk.HBox):
if widget.get_visible(): if widget.get_visible():
value = self.get_value() value = self.get_value()
if widget == self.choice_widget: if widget == self.choice_widget:
value = _from_named_ints(value, widget._allowed_values) value = widget.get_value()
icon = 'dialog-warning' if widget._allowed_values and (value not in widget._allowed_values) else '' icon = 'dialog-warning' if widget._allowed_values and (value not in widget._allowed_values) else ''
widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
self.on_change(value) self.on_change(value)
@ -1551,26 +1806,11 @@ class SetValueControl(Gtk.HBox):
def make_choice(self, values): def make_choice(self, values):
self._hide_all() self._hide_all()
self.choice_widget.remove_all()
sort_key = int if all(str(v).isdigit() for v in values) else str sort_key = int if all(str(v).isdigit() for v in values) else str
for v in sorted(values, key=sort_key): self.choice_widget.set_all_values(sorted(values, key=sort_key))
self.choice_widget.append(str(int(v)), str(v))
CompletionEntry.add_completion_to_entry(self.choice_widget.get_child(), map(str, values))
self.choice_widget._allowed_values = values self.choice_widget._allowed_values = values
self.get_value = self.choice_widget.get_value
def g(): self.set_value = self.choice_widget.set_value
value = self.choice_widget.get_active_id() or self.choice_widget.get_active_text().strip() or ''
return _from_named_ints(value, self.choice_widget._allowed_values)
def s(value):
value = _from_named_ints(value, self.choice_widget._allowed_values)
if value in self.choice_widget._allowed_values:
self.choice_widget.set_active_id(str(int(value)))
else:
self.choice_widget.get_child().set_text(str(value))
self.get_value = g
self.set_value = s
self.choice_widget.show() self.choice_widget.show()
def make_unsupported(self): def make_unsupported(self):
@ -1605,39 +1845,6 @@ def _all_settings():
return settings return settings
def _all_devices():
devices = []
def dev_in_row(_store, _treepath, row):
device = _dev_model.get_value(row, 7)
if device and device.kind and (device.serial and device.serial != '?' or device.unitId and device.unitId != '?'):
devices.append(device)
_dev_model.foreach(dev_in_row)
return devices
def _device_identifiers(device):
ids = []
if device.serial and device.serial != '?':
ids.append(device.serial)
if device.unitId and device.unitId != '?':
ids.append(device.unitId)
return ids
def _device_display_name(device):
name = device.codename
id = _device_identifiers(device)[0]
return name if not id else name + ' (' + id + ')'
def _find_device(devices, search):
if not search:
return None
return next((d for d in devices if search in [*_device_identifiers(d), _device_display_name(d)]), None)
class SetUI(ActionUI): class SetUI(ActionUI):
CLASS = _DIV.Set CLASS = _DIV.Set
@ -1647,7 +1854,6 @@ class SetUI(ActionUI):
MULTIPLE = [_SKIND.multiple_toggle, _SKIND.map_choice, _SKIND.multiple_range] MULTIPLE = [_SKIND.multiple_toggle, _SKIND.map_choice, _SKIND.multiple_range]
def create_widgets(self): def create_widgets(self):
self.devices = []
self.widgets = {} self.widgets = {}
@ -1656,23 +1862,21 @@ class SetUI(ActionUI):
_('Device'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False, margin_top=m _('Device'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False, margin_top=m
) )
self.widgets[lbl] = (0, 0, 1, 1) self.widgets[lbl] = (0, 0, 1, 1)
self.device_field = Gtk.ComboBoxText.new_with_entry() self.device_field = SmartComboBox([], completion=True, has_entry=True, blank=_('Originating device'))
self.device_field.get_child().set_text('') self.device_field.set_value('')
self.device_field.set_valign(Gtk.Align.CENTER) self.device_field.set_valign(Gtk.Align.CENTER)
self.device_field.set_size_request(400, 0) self.device_field.set_size_request(400, 0)
self.device_field.set_margin_top(m) self.device_field.set_margin_top(m)
self.device_field.connect('changed', self._changed_device)
self.device_field.connect('changed', self._on_update) self.device_field.connect('changed', self._on_update)
self.widgets[self.device_field] = (1, 0, 1, 1) self.widgets[self.device_field] = (1, 0, 1, 1)
lbl = Gtk.Label(_('Setting'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False) lbl = Gtk.Label(_('Setting'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False)
self.widgets[lbl] = (0, 1, 1, 1) self.widgets[lbl] = (0, 1, 1, 1)
self.setting_field = Gtk.ComboBoxText() self.setting_field = SmartComboBox([(s[0].name, s[0].label) for s in self.ALL_SETTINGS.values()])
self.setting_field.append('', '')
for setting in self.ALL_SETTINGS.values():
self.setting_field.append(setting[0].name, setting[0].label)
self.setting_field.set_valign(Gtk.Align.CENTER) self.setting_field.set_valign(Gtk.Align.CENTER)
self.setting_field.connect('changed', self._on_update)
self.setting_field.connect('changed', self._changed_setting) self.setting_field.connect('changed', self._changed_setting)
self.setting_field.connect('changed', self._on_update)
self.widgets[self.setting_field] = (1, 1, 1, 1) self.widgets[self.setting_field] = (1, 1, 1, 1)
self.value_lbl = Gtk.Label(_('Value'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False) self.value_lbl = Gtk.Label(_('Value'), halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=False)
@ -1687,10 +1891,11 @@ class SetUI(ActionUI):
) )
self.key_lbl.hide() self.key_lbl.hide()
self.widgets[self.key_lbl] = (2, 0, 1, 1) self.widgets[self.key_lbl] = (2, 0, 1, 1)
self.key_field = Gtk.ComboBoxText.new_with_entry() self.key_field = SmartComboBox([], has_entry=True, completion=True)
self.key_field.set_margin_top(m) self.key_field.set_margin_top(m)
self.key_field.hide() self.key_field.hide()
self.key_field.set_valign(Gtk.Align.CENTER) self.key_field.set_valign(Gtk.Align.CENTER)
self.key_field.connect('changed', self._changed_key)
self.key_field.connect('changed', self._on_update) self.key_field.connect('changed', self._on_update)
self.widgets[self.key_field] = (3, 0, 1, 1) self.widgets[self.key_field] = (3, 0, 1, 1)
@ -1734,116 +1939,154 @@ class SetUI(ActionUI):
keys = None keys = None
return setting, val_class, kind, keys return setting, val_class, kind, keys
def _changed_device(self, *args):
device = _all_devices[self.device_field.get_value()]
setting_name = self.setting_field.get_value()
if not device or not device.settings or setting_name in device.settings:
kind = self._setting_attributes(setting_name)[2]
key = self.key_field.get_value() if kind in self.MULTIPLE else None
else:
setting_name = kind = key = None
with self.ignore_changes():
self._update_setting_list(device)
self._update_key_list(setting_name, device)
self._update_value_list(setting_name, device, key)
def _changed_setting(self, *args): def _changed_setting(self, *args):
setting_name = self.setting_field.get_active_id() or None with self.ignore_changes():
device = _all_devices[self.device_field.get_value()]
setting_name = self.setting_field.get_value()
self._update_key_list(setting_name, device)
key = self.key_field.get_value()
self._update_value_list(setting_name, device, key)
def _changed_key(self, *args):
with self.ignore_changes():
setting_name = self.setting_field.get_value()
device = _all_devices[self.device_field.get_value()]
key = self.key_field.get_value()
self._update_value_list(setting_name, device, key)
def update_devices(self):
self._update_device_list()
def _update_device_list(self):
with self.ignore_changes():
self.device_field.set_all_values([(d.id, d.display_name, *d.identifiers[1:]) for d in _all_devices])
def _update_setting_list(self, device=None):
supported_settings = device.settings.keys() if device else {}
with self.ignore_changes():
self.setting_field.show_only(supported_settings or None)
def _update_key_list(self, setting_name, device=None):
setting, val_class, kind, keys = self._setting_attributes(setting_name) setting, val_class, kind, keys = self._setting_attributes(setting_name)
multiple = kind in self.MULTIPLE
self.key_field.set_visible(multiple)
self.key_lbl.set_visible(multiple)
if not multiple:
return
labels = getattr(setting, '_labels', {})
def item(k):
lbl = labels.get(k, None)
return (k, lbl[0] if lbl and isinstance(lbl, tuple) and lbl[0] else str(k))
with self.ignore_changes():
self.key_field.set_all_values(sorted(map(item, keys), key=lambda k: k[1]))
ds = device.settings if device else {}
device_setting = ds.get(setting_name, None)
supported_keys = None
if device_setting:
val = device_setting._validator
if device_setting.kind == _SKIND.multiple_toggle:
supported_keys = val.get_options() or None
elif device_setting.kind == _SKIND.map_choice:
choices = val.choices or None
supported_keys = choices.keys() if choices else None
self.key_field.show_only(supported_keys)
self._update_validation()
def _update_value_list(self, setting_name, device=None, key=None):
setting, val_class, kind, keys = self._setting_attributes(setting_name)
ds = device.settings if device else {}
device_setting = ds.get(setting_name, None)
if kind in (_SKIND.toggle, _SKIND.multiple_toggle): if kind in (_SKIND.toggle, _SKIND.multiple_toggle):
self.value_field.make_toggle() self.value_field.make_toggle()
elif kind in (_SKIND.choice, _SKIND.map_choice): elif kind in (_SKIND.choice, _SKIND.map_choice):
all_values = self._all_choices(setting_name) all_values = self._all_choices(setting_name)
self.value_field.make_choice(all_values) self.value_field.make_choice(all_values)
supported_values = None
if device_setting:
val = device_setting._validator
choices = getattr(val, 'choices', None) or None
if kind == _SKIND.choice:
supported_values = choices
elif kind == _SKIND.map_choice and isinstance(choices, dict):
supported_values = choices.get(key, None) or None
self.value_field.choice_widget.show_only(supported_values)
self._update_validation()
elif kind in (_SKIND.range, ): # _SKIND.multiple_range not supported elif kind in (_SKIND.range, ): # _SKIND.multiple_range not supported
self.value_field.make_range(val_class.min_value, val_class.max_value) self.value_field.make_range(val_class.min_value, val_class.max_value)
else: else:
self.value_field.make_unsupported() self.value_field.make_unsupported()
self.value_field.set_value('')
multiple = kind in self.MULTIPLE
if multiple:
self.key_field.remove_all()
self.key_field.append('', '')
self.key_field.set_active_id('')
CompletionEntry.add_completion_to_entry(self.key_field.get_child(), map(str, keys))
for k in sorted(keys, key=str):
self.key_field.append(str(int(k)), str(k))
def update_devices(self): def _on_update(self, *_args):
if not self.component: if not self._ignore_changes and self.component:
return super()._on_update(*_args)
with self.ignore_changes(): self._update_validation()
device_value = self.collect_value()[0]
self.devices = _all_devices()
self.device_field.remove_all()
self.device_field.append('', _('Originating device'))
acceptable_values = []
for device in self.devices:
display_name = _device_display_name(device)
ids = _device_identifiers(device)
acceptable_values += [display_name, *ids]
self.device_field.append(ids[0], display_name)
CompletionEntry.add_completion_to_entry(self.device_field.get_child(), filter(lambda v: v, acceptable_values))
device = _find_device(self.devices, device_value)
if device or not device_value:
self.device_field.set_active_id(_device_identifiers(device)[0] if device else '')
else:
self.device_field.get_child().set_text(device_value or '')
def _update_visibility(self): def _update_validation(self):
if not self.component: device_str = self.device_field.get_value()
return device = _all_devices[device_str]
a = iter(self.component.args)
device_str = next(a, None)
device = _find_device(self.devices, device_str)
if device_str and not device: if device_str and not device:
if len(device_str) == 8 and all(c in string.hexdigits for c in device_str): icon = 'dialog-question' if len(device_str) == 8 and all(
icon = 'dialog-question' c in string.hexdigits for c in device_str
else: ) else 'dialog-warning'
icon = 'dialog-warning'
else: else:
icon = '' icon = ''
self.device_field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) self.device_field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
setting_name = next(a, '') setting_name = self.setting_field.get_value()
setting, val_class, kind, keys = self._setting_attributes(setting_name) setting, val_class, kind, keys = self._setting_attributes(setting_name)
multiple = kind in self.MULTIPLE multiple = kind in self.MULTIPLE
self.key_field.set_visible(multiple)
self.key_lbl.set_visible(multiple)
if multiple: if multiple:
key = _from_named_ints(next(a, ''), keys) key = self.key_field.get_value(invalid_as_str=False, accept_hidden=False)
icon = 'dialog-warning' if keys and (key not in keys) else '' icon = 'dialog-warning' if key is None else ''
self.key_field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon) self.key_field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
if kind in (_SKIND.choice, _SKIND.map_choice):
def _on_update(self, *_args): value = self.value_field.choice_widget.get_value(invalid_as_str=False, accept_hidden=False)
if self._ignore_changes: icon = 'dialog-warning' if value is None else ''
return self.value_field.choice_widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
super()._on_update(*_args)
self._update_visibility()
def show(self, component, editable): def show(self, component, editable):
super().show(component, editable) super().show(component, editable)
self.update_devices()
a = iter(component.args) a = iter(component.args)
with self.ignore_changes(): with self.ignore_changes():
device_str = next(a, None) device_str = next(a, None)
same = not device_str same = not device_str
device = _find_device(self.devices, device_str) device = _all_devices[device_str]
if device or same: self.device_field.set_value(device.id if device else '' if same else device_str or '')
self.device_field.set_active_id(_device_identifiers(device)[0] if device else '')
else:
self.device_field.get_child().set_text(device_str or '')
setting_name = next(a, '') setting_name = next(a, '')
setting, _v, kind, keys = self._setting_attributes(setting_name) setting, _v, kind, keys = self._setting_attributes(setting_name)
self.setting_field.set_active_id(setting.name if setting else '') self.setting_field.set_value(setting.name if setting else '')
self._changed_setting() self._changed_setting()
key = None
if kind in self.MULTIPLE or kind is None and len(self.component.args) > 3: if kind in self.MULTIPLE or kind is None and len(self.component.args) > 3:
key = _from_named_ints(next(a, ''), keys) key = _from_named_ints(next(a, ''), keys)
if isinstance(key, NamedInt): self.key_field.set_value(key)
self.key_field.set_active_id(str(int(key)))
else:
self.key_field.get_child().set_text(key or '')
self.value_field.set_value(next(a, '')) self.value_field.set_value(next(a, ''))
self._update_visibility() self._update_validation()
def collect_value(self): def collect_value(self):
device_str = self.device_field.get_active_id() device_str = self.device_field.get_value()
if device_str is None:
device_str = self.device_field.get_active_text().strip()
same = device_str in ['', _('Originating device')] same = device_str in ['', _('Originating device')]
device = None if same else _find_device(self.devices, device_str) device = None if same else _all_devices[device_str]
device_value = _device_identifiers(device)[0] if device else None if same else device_str device_value = device.id if device else None if same else device_str
setting_name = self.setting_field.get_active_id() or None setting_name = self.setting_field.get_value()
setting, val_class, kind, keys = self._setting_attributes(setting_name) setting, val_class, kind, keys = self._setting_attributes(setting_name)
key_value = [] key_value = []
if kind in self.MULTIPLE or kind is None and len(self.component.args) > 3: if kind in self.MULTIPLE or kind is None and len(self.component.args) > 3:
key = self.key_field.get_active_id() or self.key_field.get_active_text().strip() or '' key = self.key_field.get_value()
key = _from_named_ints(key, keys) key = _from_named_ints(key, keys)
key_value.append(keys[key] if keys else key) key_value.append(keys[key] if keys else key)
key_value.append(self.value_field.get_value()) key_value.append(self.value_field.get_value())
@ -1853,12 +2096,11 @@ class SetUI(ActionUI):
def right_label(cls, component): def right_label(cls, component):
a = iter(component.args) a = iter(component.args)
device_str = next(a, None) device_str = next(a, None)
device = None if not device_str else _find_device(_all_devices(), device_str) device = None if not device_str else _all_devices[device_str]
device_disp = _('Originating device' device_disp = _('Originating device') if not device_str else device.display_name if device else shlex_quote(device_str)
) if not device_str else _device_display_name(device) if device else shlex_quote(device_str)
setting_name = next(a, None) setting_name = next(a, None)
setting, val_class, kind, keys = cls._setting_attributes(setting_name) setting, val_class, kind, keys = cls._setting_attributes(setting_name)
disp = [setting.label if setting else setting_name] disp = [setting_name]
if kind in cls.MULTIPLE: if kind in cls.MULTIPLE:
key = next(a, None) key = next(a, None)
disp.append(_from_named_ints(key, keys) if keys else key) disp.append(_from_named_ints(key, keys) if keys else key)
@ -1892,10 +2134,15 @@ COMPONENT_UI = {
type(None): RuleComponentUI, # placeholders for empty rule/And/Or type(None): RuleComponentUI, # placeholders for empty rule/And/Or
} }
_all_devices = AllDevicesInfo()
_dev_model = None
def update_devices(): def update_devices():
global _dev_model
global _all_devices
global _diversion_dialog global _diversion_dialog
if _diversion_dialog: if _dev_model and _all_devices.refresh() and _diversion_dialog:
_diversion_dialog.update_devices() _diversion_dialog.update_devices()
@ -1906,4 +2153,5 @@ def show_window(model):
_dev_model = model _dev_model = model
if _diversion_dialog is None: if _diversion_dialog is None:
_diversion_dialog = DiversionDialog() _diversion_dialog = DiversionDialog()
update_devices()
_diversion_dialog.window.present() _diversion_dialog.window.present()