refactor: Split diversion rules into smaller modules
Put rule conditions and actions into their into module Related #2378
This commit is contained in:
parent
4e7356385d
commit
2f6e3e21ec
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,318 @@
|
|||
## Copyright (C) Solaar Contributors
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from shlex import quote as shlex_quote
|
||||
|
||||
from gi.repository import Gtk
|
||||
from logitech_receiver import diversion as _DIV
|
||||
from logitech_receiver.diversion import CLICK
|
||||
from logitech_receiver.diversion import DEPRESS
|
||||
from logitech_receiver.diversion import RELEASE
|
||||
from logitech_receiver.diversion import XK_KEYS as _XK_KEYS
|
||||
from logitech_receiver.diversion import buttons as _buttons
|
||||
|
||||
from solaar.i18n import _
|
||||
from solaar.ui.rule_base import CompletionEntry
|
||||
from solaar.ui.rule_base import RuleComponentUI
|
||||
|
||||
|
||||
class ActionUI(RuleComponentUI):
|
||||
CLASS = _DIV.Action
|
||||
|
||||
@classmethod
|
||||
def icon_name(cls):
|
||||
return "go-next"
|
||||
|
||||
|
||||
class KeyPressUI(ActionUI):
|
||||
CLASS = _DIV.KeyPress
|
||||
KEY_NAMES = [k[3:] if k.startswith("XK_") else k for k, v in _XK_KEYS.items() if isinstance(v, int)]
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.fields = []
|
||||
self.label = Gtk.Label(
|
||||
_("Simulate a chorded key click or depress or release.\nOn Wayland requires write access to /dev/uinput."),
|
||||
halign=Gtk.Align.CENTER,
|
||||
)
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
self.del_btns = []
|
||||
self.add_btn = Gtk.Button(_("Add key"), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
self.add_btn.connect("clicked", self._clicked_add)
|
||||
self.widgets[self.add_btn] = (1, 1, 1, 1)
|
||||
self.action_clicked_radio = Gtk.RadioButton.new_with_label_from_widget(None, _("Click"))
|
||||
self.action_clicked_radio.connect("toggled", self._on_update, CLICK)
|
||||
self.widgets[self.action_clicked_radio] = (0, 3, 1, 1)
|
||||
self.action_pressed_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_clicked_radio, _("Depress"))
|
||||
self.action_pressed_radio.connect("toggled", self._on_update, DEPRESS)
|
||||
self.widgets[self.action_pressed_radio] = (1, 3, 1, 1)
|
||||
self.action_released_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_pressed_radio, _("Release"))
|
||||
self.action_released_radio.connect("toggled", self._on_update, RELEASE)
|
||||
self.widgets[self.action_released_radio] = (2, 3, 1, 1)
|
||||
|
||||
def _create_field(self):
|
||||
field_entry = CompletionEntry(self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
field_entry.connect("changed", self._on_update)
|
||||
self.fields.append(field_entry)
|
||||
self.widgets[field_entry] = (len(self.fields) - 1, 1, 1, 1)
|
||||
return field_entry
|
||||
|
||||
def _create_del_btn(self):
|
||||
btn = Gtk.Button(_("Delete"), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True)
|
||||
self.del_btns.append(btn)
|
||||
self.widgets[btn] = (len(self.del_btns) - 1, 2, 1, 1)
|
||||
btn.connect("clicked", self._clicked_del, len(self.del_btns) - 1)
|
||||
return btn
|
||||
|
||||
def _clicked_add(self, _btn):
|
||||
keys, action = self.component.regularize_args(self.collect_value())
|
||||
self.component.__init__([keys + [""], action], warn=False)
|
||||
self.show(self.component, editable=True)
|
||||
self.fields[len(self.component.key_names) - 1].grab_focus()
|
||||
|
||||
def _clicked_del(self, _btn, pos):
|
||||
keys, action = self.component.regularize_args(self.collect_value())
|
||||
keys.pop(pos)
|
||||
self.component.__init__([keys, action], warn=False)
|
||||
self.show(self.component, editable=True)
|
||||
self._on_update_callback()
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
for i, f in enumerate(self.fields):
|
||||
if f.get_visible():
|
||||
icon = (
|
||||
"dialog-warning"
|
||||
if i < len(self.component.key_names) and self.component.key_names[i] not in self.KEY_NAMES
|
||||
else ""
|
||||
)
|
||||
f.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
def show(self, component, editable=True):
|
||||
n = len(component.key_names)
|
||||
while len(self.fields) < n:
|
||||
self._create_field()
|
||||
self._create_del_btn()
|
||||
|
||||
# self.widgets[self.add_btn] = (n + 1, 0, 1, 1)
|
||||
self.widgets[self.add_btn] = (n, 1, 1, 1)
|
||||
super().show(component, editable)
|
||||
for i in range(n):
|
||||
field_entry = self.fields[i]
|
||||
with self.ignore_changes():
|
||||
field_entry.set_text(component.key_names[i])
|
||||
field_entry.set_size_request(int(0.3 * self.panel.get_toplevel().get_size()[0]), 0)
|
||||
field_entry.show_all()
|
||||
self.del_btns[i].show()
|
||||
for i in range(n, len(self.fields)):
|
||||
self.fields[i].hide()
|
||||
self.del_btns[i].hide()
|
||||
|
||||
def collect_value(self):
|
||||
action = (
|
||||
CLICK if self.action_clicked_radio.get_active() else DEPRESS if self.action_pressed_radio.get_active() else RELEASE
|
||||
)
|
||||
return [[f.get_text().strip() for f in self.fields if f.get_visible()], action]
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Key press")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return " + ".join(component.key_names) + (" (" + component.action + ")" if component.action != CLICK else "")
|
||||
|
||||
|
||||
class MouseScrollUI(ActionUI):
|
||||
CLASS = _DIV.MouseScroll
|
||||
MIN_VALUE = -2000
|
||||
MAX_VALUE = 2000
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(
|
||||
_("Simulate a mouse scroll.\nOn Wayland requires write access to /dev/uinput."), halign=Gtk.Align.CENTER
|
||||
)
|
||||
self.widgets[self.label] = (0, 0, 4, 1)
|
||||
self.label_x = Gtk.Label(label="x", halign=Gtk.Align.END, valign=Gtk.Align.END, hexpand=True)
|
||||
self.label_y = Gtk.Label(label="y", halign=Gtk.Align.END, valign=Gtk.Align.END, hexpand=True)
|
||||
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)
|
||||
for f in [self.field_x, self.field_y]:
|
||||
f.set_halign(Gtk.Align.CENTER)
|
||||
f.set_valign(Gtk.Align.START)
|
||||
self.field_x.connect("changed", self._on_update)
|
||||
self.field_y.connect("changed", self._on_update)
|
||||
self.widgets[self.label_x] = (0, 1, 1, 1)
|
||||
self.widgets[self.field_x] = (1, 1, 1, 1)
|
||||
self.widgets[self.label_y] = (2, 1, 1, 1)
|
||||
self.widgets[self.field_y] = (3, 1, 1, 1)
|
||||
|
||||
@classmethod
|
||||
def __parse(cls, v):
|
||||
try:
|
||||
# allow floats, but round them down
|
||||
return int(float(v))
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.field_x.set_value(self.__parse(component.amounts[0] if len(component.amounts) >= 1 else 0))
|
||||
self.field_y.set_value(self.__parse(component.amounts[1] if len(component.amounts) >= 2 else 0))
|
||||
|
||||
def collect_value(self):
|
||||
return [int(self.field_x.get_value()), int(self.field_y.get_value())]
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Mouse scroll")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
x = y = 0
|
||||
x = cls.__parse(component.amounts[0] if len(component.amounts) >= 1 else 0)
|
||||
y = cls.__parse(component.amounts[1] if len(component.amounts) >= 2 else 0)
|
||||
return f"{x}, {y}"
|
||||
|
||||
|
||||
class MouseClickUI(ActionUI):
|
||||
CLASS = _DIV.MouseClick
|
||||
MIN_VALUE = 1
|
||||
MAX_VALUE = 9
|
||||
BUTTONS = list(_buttons.keys())
|
||||
ACTIONS = [CLICK, DEPRESS, RELEASE]
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(
|
||||
_("Simulate a mouse click.\nOn Wayland requires write access to /dev/uinput."), halign=Gtk.Align.CENTER
|
||||
)
|
||||
self.widgets[self.label] = (0, 0, 4, 1)
|
||||
self.label_b = Gtk.Label(label=_("Button"), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label_c = Gtk.Label(label=_("Count and Action"), halign=Gtk.Align.END, valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.field_b = CompletionEntry(self.BUTTONS)
|
||||
self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
|
||||
self.field_d = CompletionEntry(self.ACTIONS)
|
||||
for f in [self.field_b, self.field_c]:
|
||||
f.set_halign(Gtk.Align.CENTER)
|
||||
f.set_valign(Gtk.Align.START)
|
||||
self.field_b.connect("changed", self._on_update)
|
||||
self.field_c.connect("changed", self._on_update)
|
||||
self.field_d.connect("changed", self._on_update)
|
||||
self.widgets[self.label_b] = (0, 1, 1, 1)
|
||||
self.widgets[self.field_b] = (1, 1, 1, 1)
|
||||
self.widgets[self.label_c] = (2, 1, 1, 1)
|
||||
self.widgets[self.field_c] = (3, 1, 1, 1)
|
||||
self.widgets[self.field_d] = (4, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.field_b.set_text(component.button)
|
||||
if isinstance(component.count, int):
|
||||
self.field_c.set_value(component.count)
|
||||
self.field_d.set_text(CLICK)
|
||||
else:
|
||||
self.field_c.set_value(1)
|
||||
self.field_d.set_text(component.count)
|
||||
|
||||
def collect_value(self):
|
||||
b, c, d = self.field_b.get_text(), int(self.field_c.get_value()), self.field_d.get_text()
|
||||
if b not in self.BUTTONS:
|
||||
b = "unknown"
|
||||
if d != CLICK:
|
||||
c = d
|
||||
return [b, c]
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Mouse click")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return f'{component.button} ({"x" if isinstance(component.count, int) else ""}{component.count})'
|
||||
|
||||
|
||||
class ExecuteUI(ActionUI):
|
||||
CLASS = _DIV.Execute
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(_("Execute a command with arguments."), halign=Gtk.Align.CENTER)
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
self.fields = []
|
||||
self.add_btn = Gtk.Button(_("Add argument"), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
self.del_btns = []
|
||||
self.add_btn.connect("clicked", self._clicked_add)
|
||||
self.widgets[self.add_btn] = (1, 1, 1, 1)
|
||||
|
||||
def _create_field(self):
|
||||
field_entry = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
field_entry.set_size_request(150, 0)
|
||||
field_entry.connect("changed", self._on_update)
|
||||
self.fields.append(field_entry)
|
||||
self.widgets[field_entry] = (len(self.fields) - 1, 1, 1, 1)
|
||||
return field_entry
|
||||
|
||||
def _create_del_btn(self):
|
||||
btn = Gtk.Button(_("Delete"), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True)
|
||||
btn.set_size_request(150, 0)
|
||||
self.del_btns.append(btn)
|
||||
self.widgets[btn] = (len(self.del_btns) - 1, 2, 1, 1)
|
||||
btn.connect("clicked", self._clicked_del, len(self.del_btns) - 1)
|
||||
return btn
|
||||
|
||||
def _clicked_add(self, *_args):
|
||||
self.component.__init__(self.collect_value() + [""], warn=False)
|
||||
self.show(self.component, editable=True)
|
||||
self.fields[len(self.component.args) - 1].grab_focus()
|
||||
|
||||
def _clicked_del(self, _btn, pos):
|
||||
v = self.collect_value()
|
||||
v.pop(pos)
|
||||
self.component.__init__(v, warn=False)
|
||||
self.show(self.component, editable=True)
|
||||
self._on_update_callback()
|
||||
|
||||
def show(self, component, editable):
|
||||
n = len(component.args)
|
||||
while len(self.fields) < n:
|
||||
self._create_field()
|
||||
self._create_del_btn()
|
||||
for i in range(n):
|
||||
field_entry = self.fields[i]
|
||||
with self.ignore_changes():
|
||||
field_entry.set_text(component.args[i])
|
||||
self.del_btns[i].show()
|
||||
self.widgets[self.add_btn] = (n + 1, 1, 1, 1)
|
||||
super().show(component, editable)
|
||||
for i in range(n, len(self.fields)):
|
||||
self.fields[i].hide()
|
||||
self.del_btns[i].hide()
|
||||
self.add_btn.set_valign(Gtk.Align.END if n >= 1 else Gtk.Align.CENTER)
|
||||
|
||||
def collect_value(self):
|
||||
return [f.get_text() for f in self.fields if f.get_visible()]
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Execute")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return " ".join([shlex_quote(a) for a in component.args])
|
|
@ -0,0 +1,108 @@
|
|||
## Copyright (C) Solaar Contributors
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from contextlib import contextmanager as contextlib_contextmanager
|
||||
|
||||
from gi.repository import Gtk
|
||||
from logitech_receiver import diversion as _DIV
|
||||
|
||||
|
||||
def norm(s):
|
||||
return s.replace("_", "").replace(" ", "").lower()
|
||||
|
||||
|
||||
class CompletionEntry(Gtk.Entry):
|
||||
def __init__(self, values, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
CompletionEntry.add_completion_to_entry(self, values)
|
||||
|
||||
@classmethod
|
||||
def add_completion_to_entry(cls, entry, values):
|
||||
completion = entry.get_completion()
|
||||
if not completion:
|
||||
liststore = Gtk.ListStore(str)
|
||||
completion = Gtk.EntryCompletion()
|
||||
completion.set_model(liststore)
|
||||
completion.set_match_func(lambda completion, key, it: norm(key) in norm(completion.get_model()[it][0]))
|
||||
completion.set_text_column(0)
|
||||
entry.set_completion(completion)
|
||||
else:
|
||||
liststore = completion.get_model()
|
||||
liststore.clear()
|
||||
for v in sorted(set(values), key=str.casefold):
|
||||
liststore.append((v,))
|
||||
|
||||
|
||||
class RuleComponentUI:
|
||||
CLASS = _DIV.RuleComponent
|
||||
|
||||
def __init__(self, panel, on_update=None):
|
||||
self.panel = panel
|
||||
self.widgets = {} # widget -> coord. in grid
|
||||
self.component = None
|
||||
self._ignore_changes = 0
|
||||
self._on_update_callback = (lambda: None) if on_update is None else on_update
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
pass
|
||||
|
||||
def show(self, component, editable=True):
|
||||
self._show_widgets(editable)
|
||||
self.component = component
|
||||
|
||||
def collect_value(self):
|
||||
return None
|
||||
|
||||
@contextlib_contextmanager
|
||||
def ignore_changes(self):
|
||||
self._ignore_changes += 1
|
||||
yield None
|
||||
self._ignore_changes -= 1
|
||||
|
||||
def _on_update(self, *_args):
|
||||
if not self._ignore_changes and self.component is not None:
|
||||
value = self.collect_value()
|
||||
self.component.__init__(value, warn=False)
|
||||
self._on_update_callback()
|
||||
return value
|
||||
return None
|
||||
|
||||
def _show_widgets(self, editable):
|
||||
self._remove_panel_items()
|
||||
for widget, coord in self.widgets.items():
|
||||
self.panel.attach(widget, *coord)
|
||||
widget.set_sensitive(editable)
|
||||
widget.show()
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return type(component).__name__
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, _component):
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def icon_name(cls):
|
||||
return ""
|
||||
|
||||
def _remove_panel_items(self):
|
||||
for c in self.panel.get_children():
|
||||
self.panel.remove(c)
|
||||
|
||||
def update_devices(self):
|
||||
pass
|
|
@ -0,0 +1,597 @@
|
|||
## Copyright (C) Solaar Contributors
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
## it under the terms of the GNU General Public License as published by
|
||||
## the Free Software Foundation; either version 2 of the License, or
|
||||
## (at your option) any later version.
|
||||
##
|
||||
## This program is distributed in the hope that it will be useful,
|
||||
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
## GNU General Public License for more details.
|
||||
##
|
||||
## You should have received a copy of the GNU General Public License along
|
||||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
from collections import namedtuple
|
||||
|
||||
from gi.repository import Gtk
|
||||
from logitech_receiver import diversion as _DIV
|
||||
from logitech_receiver.diversion import Key as _Key
|
||||
from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES
|
||||
from logitech_receiver.special_keys import CONTROL as _CONTROL
|
||||
|
||||
from solaar.i18n import _
|
||||
from solaar.ui.rule_base import CompletionEntry
|
||||
from solaar.ui.rule_base import RuleComponentUI
|
||||
|
||||
|
||||
class ConditionUI(RuleComponentUI):
|
||||
CLASS = _DIV.Condition
|
||||
|
||||
@classmethod
|
||||
def icon_name(cls):
|
||||
return "dialog-question"
|
||||
|
||||
|
||||
class ProcessUI(ConditionUI):
|
||||
CLASS = _DIV.Process
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("X11 active process. For use in X11 only."))
|
||||
self.widgets[self.label] = (0, 0, 1, 1)
|
||||
self.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.field.set_size_request(600, 0)
|
||||
self.field.connect("changed", self._on_update)
|
||||
self.widgets[self.field] = (0, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.field.set_text(component.process)
|
||||
|
||||
def collect_value(self):
|
||||
return self.field.get_text()
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Process")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return str(component.process)
|
||||
|
||||
|
||||
class MouseProcessUI(ConditionUI):
|
||||
CLASS = _DIV.MouseProcess
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("X11 mouse process. For use in X11 only."))
|
||||
self.widgets[self.label] = (0, 0, 1, 1)
|
||||
self.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.field.set_size_request(600, 0)
|
||||
self.field.connect("changed", self._on_update)
|
||||
self.widgets[self.field] = (0, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.field.set_text(component.process)
|
||||
|
||||
def collect_value(self):
|
||||
return self.field.get_text()
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("MouseProcess")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return str(component.process)
|
||||
|
||||
|
||||
class FeatureUI(ConditionUI):
|
||||
CLASS = _DIV.Feature
|
||||
FEATURES_WITH_DIVERSION = [
|
||||
str(_ALL_FEATURES.CROWN),
|
||||
str(_ALL_FEATURES.THUMB_WHEEL),
|
||||
str(_ALL_FEATURES.LOWRES_WHEEL),
|
||||
str(_ALL_FEATURES.HIRES_WHEEL),
|
||||
str(_ALL_FEATURES.GESTURE_2),
|
||||
str(_ALL_FEATURES.REPROG_CONTROLS_V4),
|
||||
str(_ALL_FEATURES.GKEY),
|
||||
str(_ALL_FEATURES.MKEYS),
|
||||
str(_ALL_FEATURES.MR),
|
||||
]
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("Feature name of notification triggering rule processing."))
|
||||
self.widgets[self.label] = (0, 0, 1, 1)
|
||||
self.field = Gtk.ComboBoxText.new_with_entry()
|
||||
self.field.append("", "")
|
||||
for feature in self.FEATURES_WITH_DIVERSION:
|
||||
self.field.append(feature, feature)
|
||||
self.field.set_valign(Gtk.Align.CENTER)
|
||||
# self.field.set_vexpand(True)
|
||||
self.field.set_size_request(600, 0)
|
||||
self.field.connect("changed", self._on_update)
|
||||
all_features = [str(f) for f in _ALL_FEATURES]
|
||||
CompletionEntry.add_completion_to_entry(self.field.get_child(), all_features)
|
||||
self.widgets[self.field] = (0, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
f = str(component.feature) if component.feature else ""
|
||||
self.field.set_active_id(f)
|
||||
if f not in self.FEATURES_WITH_DIVERSION:
|
||||
self.field.get_child().set_text(f)
|
||||
|
||||
def collect_value(self):
|
||||
return (self.field.get_active_text() or "").strip()
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
icon = "dialog-warning" if not self.component.feature else ""
|
||||
self.field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Feature")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return f"{str(component.feature)} ({int(component.feature or 0):04X})"
|
||||
|
||||
|
||||
class ReportUI(ConditionUI):
|
||||
CLASS = _DIV.Report
|
||||
MIN_VALUE = -1 # for invalid values
|
||||
MAX_VALUE = 15
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("Report number of notification triggering rule processing."))
|
||||
self.widgets[self.label] = (0, 0, 1, 1)
|
||||
self.field = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
|
||||
self.field.set_halign(Gtk.Align.CENTER)
|
||||
self.field.set_valign(Gtk.Align.CENTER)
|
||||
self.field.set_hexpand(True)
|
||||
# self.field.set_vexpand(True)
|
||||
self.field.connect("changed", self._on_update)
|
||||
self.widgets[self.field] = (0, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.field.set_value(component.report)
|
||||
|
||||
def collect_value(self):
|
||||
return int(self.field.get_value())
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Report")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return str(component.report)
|
||||
|
||||
|
||||
class ModifiersUI(ConditionUI):
|
||||
CLASS = _DIV.Modifiers
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("Active keyboard modifiers. Not always available in Wayland."))
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
self.labels = {}
|
||||
self.switches = {}
|
||||
for i, m in enumerate(_DIV.MODIFIERS):
|
||||
switch = Gtk.Switch(halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True)
|
||||
label = Gtk.Label(m, halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
self.widgets[label] = (i, 1, 1, 1)
|
||||
self.widgets[switch] = (i, 2, 1, 1)
|
||||
self.labels[m] = label
|
||||
self.switches[m] = switch
|
||||
switch.connect("notify::active", self._on_update)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
for m in _DIV.MODIFIERS:
|
||||
self.switches[m].set_active(m in component.modifiers)
|
||||
|
||||
def collect_value(self):
|
||||
return [m for m, s in self.switches.items() if s.get_active()]
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Modifiers")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return "+".join(component.modifiers) or "None"
|
||||
|
||||
|
||||
class KeyUI(ConditionUI):
|
||||
CLASS = _DIV.Key
|
||||
KEY_NAMES = map(str, _CONTROL)
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(
|
||||
_(
|
||||
"Diverted key or button depressed or released.\n"
|
||||
"Use the Key/Button Diversion and Divert G Keys settings to divert keys and buttons."
|
||||
)
|
||||
)
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
self.key_field = CompletionEntry(self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.key_field.set_size_request(600, 0)
|
||||
self.key_field.connect("changed", self._on_update)
|
||||
self.widgets[self.key_field] = (0, 1, 2, 1)
|
||||
self.action_pressed_radio = Gtk.RadioButton.new_with_label_from_widget(None, _("Key down"))
|
||||
self.action_pressed_radio.connect("toggled", self._on_update, _Key.DOWN)
|
||||
self.widgets[self.action_pressed_radio] = (2, 1, 1, 1)
|
||||
self.action_released_radio = Gtk.RadioButton.new_with_label_from_widget(self.action_pressed_radio, _("Key up"))
|
||||
self.action_released_radio.connect("toggled", self._on_update, _Key.UP)
|
||||
self.widgets[self.action_released_radio] = (3, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.key_field.set_text(str(component.key) if self.component.key else "")
|
||||
if not component.action or component.action == _Key.DOWN:
|
||||
self.action_pressed_radio.set_active(True)
|
||||
else:
|
||||
self.action_released_radio.set_active(True)
|
||||
|
||||
def collect_value(self):
|
||||
action = _Key.UP if self.action_released_radio.get_active() else _Key.DOWN
|
||||
return [self.key_field.get_text(), action]
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
icon = "dialog-warning" if not self.component.key or not self.component.action else ""
|
||||
self.key_field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Key")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return f"{str(component.key)} ({int(component.key):04X}) ({_(component.action)})" if component.key else "None"
|
||||
|
||||
|
||||
class KeyIsDownUI(ConditionUI):
|
||||
CLASS = _DIV.KeyIsDown
|
||||
KEY_NAMES = map(str, _CONTROL)
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(
|
||||
_(
|
||||
"Diverted key or button is currently down.\n"
|
||||
"Use the Key/Button Diversion and Divert G Keys settings to divert keys and buttons."
|
||||
)
|
||||
)
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
self.key_field = CompletionEntry(self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.key_field.set_size_request(600, 0)
|
||||
self.key_field.connect("changed", self._on_update)
|
||||
self.widgets[self.key_field] = (0, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.key_field.set_text(str(component.key) if self.component.key else "")
|
||||
|
||||
def collect_value(self):
|
||||
return self.key_field.get_text()
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
icon = "dialog-warning" if not self.component.key else ""
|
||||
self.key_field.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("KeyIsDown")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return f"{str(component.key)} ({int(component.key):04X})" if component.key else "None"
|
||||
|
||||
|
||||
class TestUI(ConditionUI):
|
||||
CLASS = _DIV.Test
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("Test condition on notification triggering rule processing."))
|
||||
self.widgets[self.label] = (0, 0, 4, 1)
|
||||
lbl = Gtk.Label(_("Test"), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=False, vexpand=False)
|
||||
self.widgets[lbl] = (0, 1, 1, 1)
|
||||
lbl = Gtk.Label(_("Parameter"), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=False, vexpand=False)
|
||||
self.widgets[lbl] = (2, 1, 1, 1)
|
||||
|
||||
self.test = Gtk.ComboBoxText.new_with_entry()
|
||||
self.test.append("", "")
|
||||
for t in _DIV.TESTS:
|
||||
self.test.append(t, t)
|
||||
self.test.set_halign(Gtk.Align.END)
|
||||
self.test.set_valign(Gtk.Align.CENTER)
|
||||
self.test.set_hexpand(False)
|
||||
self.test.set_size_request(300, 0)
|
||||
CompletionEntry.add_completion_to_entry(self.test.get_child(), _DIV.TESTS)
|
||||
self.test.connect("changed", self._on_update)
|
||||
self.widgets[self.test] = (1, 1, 1, 1)
|
||||
|
||||
self.parameter = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
self.parameter.set_size_request(150, 0)
|
||||
self.parameter.connect("changed", self._on_update)
|
||||
self.widgets[self.parameter] = (3, 1, 1, 1)
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
with self.ignore_changes():
|
||||
self.test.set_active_id(component.test)
|
||||
self.parameter.set_text(str(component.parameter) if component.parameter is not None else "")
|
||||
if component.test not in _DIV.TESTS:
|
||||
self.test.get_child().set_text(component.test)
|
||||
self._change_status_icon()
|
||||
|
||||
def collect_value(self):
|
||||
try:
|
||||
param = int(self.parameter.get_text()) if self.parameter.get_text() else None
|
||||
except Exception:
|
||||
param = self.parameter.get_text()
|
||||
test = (self.test.get_active_text() or "").strip()
|
||||
return [test, param] if param is not None else [test]
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
self._change_status_icon()
|
||||
|
||||
def _change_status_icon(self):
|
||||
icon = "dialog-warning" if (self.test.get_active_text() or "").strip() not in _DIV.TESTS else ""
|
||||
self.test.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Test")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
return component.test + (" " + repr(component.parameter) if component.parameter is not None else "")
|
||||
|
||||
|
||||
_TestBytesElement = namedtuple("TestBytesElement", ["id", "label", "min", "max"])
|
||||
_TestBytesMode = namedtuple("TestBytesMode", ["label", "elements", "label_fn"])
|
||||
|
||||
|
||||
class TestBytesUI(ConditionUI):
|
||||
CLASS = _DIV.TestBytes
|
||||
|
||||
_common_elements = [
|
||||
_TestBytesElement("begin", _("begin (inclusive)"), 0, 16),
|
||||
_TestBytesElement("end", _("end (exclusive)"), 0, 16),
|
||||
]
|
||||
|
||||
_global_min = -(2**31)
|
||||
_global_max = 2**31 - 1
|
||||
|
||||
_modes = {
|
||||
"range": _TestBytesMode(
|
||||
_("range"),
|
||||
_common_elements
|
||||
+ [
|
||||
_TestBytesElement("minimum", _("minimum"), _global_min, _global_max), # uint32
|
||||
_TestBytesElement("maximum", _("maximum"), _global_min, _global_max),
|
||||
],
|
||||
lambda e: _("bytes %(0)d to %(1)d, ranging from %(2)d to %(3)d" % {str(i): v for i, v in enumerate(e)}),
|
||||
),
|
||||
"mask": _TestBytesMode(
|
||||
_("mask"),
|
||||
_common_elements + [_TestBytesElement("mask", _("mask"), _global_min, _global_max)],
|
||||
lambda e: _("bytes %(0)d to %(1)d, mask %(2)d" % {str(i): v for i, v in enumerate(e)}),
|
||||
),
|
||||
}
|
||||
|
||||
def create_widgets(self):
|
||||
self.fields = {}
|
||||
self.field_labels = {}
|
||||
self.widgets = {}
|
||||
self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
|
||||
self.label.set_text(_("Bit or range test on bytes in notification message triggering rule processing."))
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
col = 0
|
||||
mode_col = 2
|
||||
self.mode_field = Gtk.ComboBox.new_with_model(Gtk.ListStore(str, str))
|
||||
mode_renderer = Gtk.CellRendererText()
|
||||
self.mode_field.set_id_column(0)
|
||||
self.mode_field.pack_start(mode_renderer, True)
|
||||
self.mode_field.add_attribute(mode_renderer, "text", 1)
|
||||
self.widgets[self.mode_field] = (mode_col, 2, 1, 1)
|
||||
mode_label = Gtk.Label(_("type"), margin_top=20)
|
||||
self.widgets[mode_label] = (mode_col, 1, 1, 1)
|
||||
for mode_id, mode in TestBytesUI._modes.items():
|
||||
self.mode_field.get_model().append([mode_id, mode.label])
|
||||
for element in mode.elements:
|
||||
if element.id not in self.fields:
|
||||
field = Gtk.SpinButton.new_with_range(element.min, element.max, 1)
|
||||
field.set_value(0)
|
||||
field.set_size_request(150, 0)
|
||||
field.connect("value-changed", self._on_update)
|
||||
label = Gtk.Label(element.label, margin_top=20)
|
||||
self.fields[element.id] = field
|
||||
self.field_labels[element.id] = label
|
||||
self.widgets[label] = (col, 1, 1, 1)
|
||||
self.widgets[field] = (col, 2, 1, 1)
|
||||
col += 1 if col != mode_col - 1 else 2
|
||||
self.mode_field.connect("changed", lambda cb: (self._on_update(), self._only_mode(cb.get_active_id())))
|
||||
self.mode_field.set_active_id("range")
|
||||
|
||||
def show(self, component, editable):
|
||||
super().show(component, editable)
|
||||
|
||||
with self.ignore_changes():
|
||||
mode_id = {3: "mask", 4: "range"}.get(len(component.test), None)
|
||||
self._only_mode(mode_id)
|
||||
if not mode_id:
|
||||
return
|
||||
self.mode_field.set_active_id(mode_id)
|
||||
if mode_id:
|
||||
mode = TestBytesUI._modes[mode_id]
|
||||
for i, element in enumerate(mode.elements):
|
||||
self.fields[element.id].set_value(component.test[i])
|
||||
|
||||
def collect_value(self):
|
||||
mode_id = self.mode_field.get_active_id()
|
||||
return [self.fields[element.id].get_value_as_int() for element in TestBytesUI._modes[mode_id].elements]
|
||||
|
||||
def _only_mode(self, mode_id):
|
||||
if not mode_id:
|
||||
return
|
||||
keep = {element.id for element in TestBytesUI._modes[mode_id].elements}
|
||||
for element_id, f in self.fields.items():
|
||||
visible = element_id in keep
|
||||
f.set_visible(visible)
|
||||
self.field_labels[element_id].set_visible(visible)
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
if not self.component:
|
||||
return
|
||||
begin, end, *etc = self.component.test
|
||||
icon = "dialog-warning" if end <= begin else ""
|
||||
self.fields["end"].set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
if len(self.component.test) == 4:
|
||||
*etc, minimum, maximum = self.component.test
|
||||
icon = "dialog-warning" if maximum < minimum else ""
|
||||
self.fields["maximum"].set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Test bytes")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
mode_id = {3: "mask", 4: "range"}.get(len(component.test), None)
|
||||
if not mode_id:
|
||||
return str(component.test)
|
||||
return TestBytesUI._modes[mode_id].label_fn(component.test)
|
||||
|
||||
|
||||
class MouseGestureUI(ConditionUI):
|
||||
CLASS = _DIV.MouseGesture
|
||||
MOUSE_GESTURE_NAMES = [
|
||||
"Mouse Up",
|
||||
"Mouse Down",
|
||||
"Mouse Left",
|
||||
"Mouse Right",
|
||||
"Mouse Up-left",
|
||||
"Mouse Up-right",
|
||||
"Mouse Down-left",
|
||||
"Mouse Down-right",
|
||||
]
|
||||
MOVE_NAMES = list(map(str, _CONTROL)) + MOUSE_GESTURE_NAMES
|
||||
|
||||
def create_widgets(self):
|
||||
self.widgets = {}
|
||||
self.fields = []
|
||||
self.label = Gtk.Label(
|
||||
_("Mouse gesture with optional initiating button followed by zero or more mouse movements."),
|
||||
halign=Gtk.Align.CENTER,
|
||||
)
|
||||
self.widgets[self.label] = (0, 0, 5, 1)
|
||||
self.del_btns = []
|
||||
self.add_btn = Gtk.Button(_("Add movement"), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True)
|
||||
self.add_btn.connect("clicked", self._clicked_add)
|
||||
self.widgets[self.add_btn] = (1, 1, 1, 1)
|
||||
|
||||
def _create_field(self):
|
||||
field = Gtk.ComboBoxText.new_with_entry()
|
||||
for g in self.MOUSE_GESTURE_NAMES:
|
||||
field.append(g, g)
|
||||
CompletionEntry.add_completion_to_entry(field.get_child(), self.MOVE_NAMES)
|
||||
field.connect("changed", self._on_update)
|
||||
self.fields.append(field)
|
||||
self.widgets[field] = (len(self.fields) - 1, 1, 1, 1)
|
||||
return field
|
||||
|
||||
def _create_del_btn(self):
|
||||
btn = Gtk.Button(_("Delete"), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True)
|
||||
self.del_btns.append(btn)
|
||||
self.widgets[btn] = (len(self.del_btns) - 1, 2, 1, 1)
|
||||
btn.connect("clicked", self._clicked_del, len(self.del_btns) - 1)
|
||||
return btn
|
||||
|
||||
def _clicked_add(self, _btn):
|
||||
self.component.__init__(self.collect_value() + [""], warn=False)
|
||||
self.show(self.component, editable=True)
|
||||
self.fields[len(self.component.movements) - 1].grab_focus()
|
||||
|
||||
def _clicked_del(self, _btn, pos):
|
||||
v = self.collect_value()
|
||||
v.pop(pos)
|
||||
self.component.__init__(v, warn=False)
|
||||
self.show(self.component, editable=True)
|
||||
self._on_update_callback()
|
||||
|
||||
def _on_update(self, *args):
|
||||
super()._on_update(*args)
|
||||
for i, f in enumerate(self.fields):
|
||||
if f.get_visible():
|
||||
icon = (
|
||||
"dialog-warning"
|
||||
if i < len(self.component.movements) and self.component.movements[i] not in self.MOVE_NAMES
|
||||
else ""
|
||||
)
|
||||
f.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
|
||||
def show(self, component, editable):
|
||||
n = len(component.movements)
|
||||
while len(self.fields) < n:
|
||||
self._create_field()
|
||||
self._create_del_btn()
|
||||
self.widgets[self.add_btn] = (n + 1, 1, 1, 1)
|
||||
super().show(component, editable)
|
||||
for i in range(n):
|
||||
field = self.fields[i]
|
||||
with self.ignore_changes():
|
||||
field.get_child().set_text(component.movements[i])
|
||||
field.set_size_request(int(0.3 * self.panel.get_toplevel().get_size()[0]), 0)
|
||||
field.show_all()
|
||||
self.del_btns[i].show()
|
||||
for i in range(n, len(self.fields)):
|
||||
self.fields[i].hide()
|
||||
self.del_btns[i].hide()
|
||||
self.add_btn.set_valign(Gtk.Align.END if n >= 1 else Gtk.Align.CENTER)
|
||||
|
||||
def collect_value(self):
|
||||
return [f.get_active_text().strip() for f in self.fields if f.get_visible()]
|
||||
|
||||
@classmethod
|
||||
def left_label(cls, component):
|
||||
return _("Mouse Gesture")
|
||||
|
||||
@classmethod
|
||||
def right_label(cls, component):
|
||||
if len(component.movements) == 0:
|
||||
return "No-op"
|
||||
else:
|
||||
return " -> ".join(component.movements)
|
Loading…
Reference in New Issue