ui: hide unsupported settings/keys/values
This commit is contained in:
parent
a4e30a7190
commit
b9a23f133e
|
@ -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):
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue