Compare commits

...

6 Commits

Author SHA1 Message Date
MattHag 6b175c74c5
Merge c3f3c91e59 into 783bd5e4da 2025-10-08 18:11:48 +02:00
Peter F. Patel-Schneider 783bd5e4da device: fix bug with unknown tasks 2025-10-05 08:05:15 -04:00
MattHag c3f3c91e59
Merge branch 'master' into diversion_remove_inheritance 2025-03-01 12:07:04 +01:00
MattHag 515f97269d listener: Ensure device is there for log msg 2025-01-02 14:41:47 +01:00
MattHag a70366a786 diversion: Introduce protocols to unite Action and Condition classes
Enforce a common interface for all Action and Condition related classes
and connect them to a common protocol class to support isinstance
checks.

Related #2659
2025-01-02 14:37:29 +01:00
MattHag 9af34b33e8 diversion: Remove RuleComponent class to avoid coupling of classes
Replace tight coupling of Action and Condition classes by removing their
common base class and converting it into a function.

The RuleComponent was a base class solely holding a compile function and
passing it to its children. There is no need for tight coupling with
inheritance for that purpose.

Related #2659
2025-01-02 14:36:55 +01:00
7 changed files with 82 additions and 63 deletions

View File

@ -511,35 +511,33 @@ MOUSE_GESTURE_TESTS = {
"mouse-noop": [],
}
# COMPONENTS = {}
class RuleComponent:
def compile(self, c):
if isinstance(c, RuleComponent):
return c
elif isinstance(c, dict) and len(c) == 1:
k, v = next(iter(c.items()))
if k in COMPONENTS:
return COMPONENTS[k](v)
logger.warning("illegal component in rule: %s", c)
return Condition()
def compile_component(c) -> Rule | type[ConditionProtocol] | type[ActionProtocol]:
if isinstance(c, Rule) or isinstance(c, ConditionProtocol) or isinstance(c, ActionProtocol):
return c
elif isinstance(c, dict) and len(c) == 1:
k, v = next(iter(c.items()))
if k in COMPONENTS:
cls: Rule | type[ConditionProtocol] | type[ActionProtocol] = COMPONENTS[k]
return cls(v)
logger.warning("illegal component in rule: %s", c)
return FallbackCondition()
def _evaluate(components, feature, notification: HIDPPNotification, device, result) -> Any:
res = True
for component in components:
res = component.evaluate(feature, notification, device, result)
if not isinstance(component, Action) and res is None:
if not isinstance(component, ActionProtocol) and res is None:
return None
if isinstance(component, Condition) and not res:
if isinstance(component, ConditionProtocol) and not res:
return res
return res
class Rule(RuleComponent):
class Rule:
def __init__(self, args, source=None, warn=True):
self.components = [self.compile(a) for a in args]
self.components = [compile_component(a) for a in args]
self.source = source
def __str__(self):
@ -559,7 +557,22 @@ class Rule(RuleComponent):
return {"Rule": [c.data() for c in self.components]}
class Condition(RuleComponent):
@typing.runtime_checkable
class ConditionProtocol(typing.Protocol):
def __init__(self, args: Any, warn: bool) -> None:
...
def __str__(self) -> str:
...
def evaluate(self, feature, notification: HIDPPNotification, device, last_result) -> bool:
...
def data(self) -> dict[str, Any]:
...
class FallbackCondition(ConditionProtocol):
def __init__(self, *args):
pass
@ -572,12 +585,12 @@ class Condition(RuleComponent):
return False
class Not(Condition):
class Not(ConditionProtocol):
def __init__(self, op, warn=True):
if isinstance(op, list) and len(op) == 1:
op = op[0]
self.op = op
self.component = self.compile(op)
self.component = compile_component(op)
def __str__(self):
return f"Not: {str(self.component)}"
@ -592,9 +605,9 @@ class Not(Condition):
return {"Not": self.component.data()}
class Or(Condition):
class Or(ConditionProtocol):
def __init__(self, args, warn=True):
self.components = [self.compile(a) for a in args]
self.components = [compile_component(a) for a in args]
def __str__(self):
return "Or: [" + ", ".join(str(c) for c in self.components) + "]"
@ -605,9 +618,9 @@ class Or(Condition):
result = False
for component in self.components:
result = component.evaluate(feature, notification, device, last_result)
if not isinstance(component, Action) and result is None:
if not isinstance(component, ActionProtocol) and result is None:
return None
if isinstance(component, Condition) and result:
if isinstance(component, ConditionProtocol) and result:
return result
return result
@ -615,9 +628,9 @@ class Or(Condition):
return {"Or": [c.data() for c in self.components]}
class And(Condition):
class And(ConditionProtocol):
def __init__(self, args, warn=True):
self.components = [self.compile(a) for a in args]
self.components = [compile_component(a) for a in args]
def __str__(self):
return "And: [" + ", ".join(str(c) for c in self.components) + "]"
@ -677,7 +690,7 @@ def gnome_dbus_pointer_prog():
return (wm_class,) if wm_class else None
class Process(Condition):
class Process(ConditionProtocol):
def __init__(self, process, warn=True):
self.process = process
if (not wayland and not x11_setup()) or (wayland and not gnome_dbus_interface_setup()):
@ -708,7 +721,7 @@ class Process(Condition):
return {"Process": str(self.process)}
class MouseProcess(Condition):
class MouseProcess(ConditionProtocol):
def __init__(self, process, warn=True):
self.process = process
if (not wayland and not x11_setup()) or (wayland and not gnome_dbus_interface_setup()):
@ -739,7 +752,7 @@ class MouseProcess(Condition):
return {"MouseProcess": str(self.process)}
class Feature(Condition):
class Feature(ConditionProtocol):
def __init__(self, feature: str, warn: bool = True):
try:
self.feature = SupportedFeature[feature.replace(" ", "_")]
@ -760,7 +773,7 @@ class Feature(Condition):
return {"Feature": str(self.feature)}
class Report(Condition):
class Report(ConditionProtocol):
def __init__(self, report, warn=True):
if not (isinstance(report, int)):
if warn:
@ -782,7 +795,7 @@ class Report(Condition):
# Setting(device, setting, [key], value...)
class Setting(Condition):
class Setting(ConditionProtocol):
def __init__(self, args, warn=True):
if not (isinstance(args, list) and len(args) > 2):
if warn:
@ -829,7 +842,7 @@ MODIFIERS = {
MODIFIER_MASK = MODIFIERS["Shift"] + MODIFIERS["Control"] + MODIFIERS["Alt"] + MODIFIERS["Super"]
class Modifiers(Condition):
class Modifiers(ConditionProtocol):
def __init__(self, modifiers, warn=True):
modifiers = [modifiers] if isinstance(modifiers, str) else modifiers
self.desired = 0
@ -859,7 +872,7 @@ class Modifiers(Condition):
return {"Modifiers": [str(m) for m in self.modifiers]}
class Key(Condition):
class Key(ConditionProtocol):
DOWN = "pressed"
UP = "released"
@ -914,7 +927,7 @@ class Key(Condition):
return {"Key": [str(self.key), self.action]}
class KeyIsDown(Condition):
class KeyIsDown(ConditionProtocol):
def __init__(self, args, warn=True):
default_key = 0
@ -958,7 +971,7 @@ def range_test(start, end, min, max):
return range_test_helper
class Test(Condition):
class Test(ConditionProtocol):
def __init__(self, test, warn=True):
self.test = ""
self.parameter = None
@ -1000,7 +1013,7 @@ class Test(Condition):
return {"Test": ([self.test, self.parameter] if self.parameter is not None else [self.test])}
class TestBytes(Condition):
class TestBytes(ConditionProtocol):
def __init__(self, test, warn=True):
self.test = test
if (
@ -1028,7 +1041,7 @@ class TestBytes(Condition):
return {"TestBytes": self.test[:]}
class MouseGesture(Condition):
class MouseGesture(ConditionProtocol):
MOVEMENTS = [
"Mouse Up",
"Mouse Down",
@ -1083,7 +1096,7 @@ class MouseGesture(Condition):
return {"MouseGesture": [str(m) for m in self.movements]}
class Active(Condition):
class Active(ConditionProtocol):
def __init__(self, devID, warn=True):
if not (isinstance(devID, str)):
if warn:
@ -1104,7 +1117,7 @@ class Active(Condition):
return {"Active": self.devID}
class Device(Condition):
class Device(ConditionProtocol):
def __init__(self, devID, warn=True):
if not (isinstance(devID, str)):
if warn:
@ -1129,7 +1142,7 @@ class Device(Condition):
return {"Device": self.devID}
class Host(Condition):
class Host(ConditionProtocol):
def __init__(self, host, warn=True):
if not (isinstance(host, str)):
if warn:
@ -1150,12 +1163,16 @@ class Host(Condition):
return {"Host": self.host}
class Action(RuleComponent):
def __init__(self, *args):
pass
@typing.runtime_checkable
class ActionProtocol(typing.Protocol):
def __init__(self, args: Any, warn: bool) -> None:
...
def evaluate(self, feature, notification: HIDPPNotification, device, last_result):
return None
def evaluate(self, feature, notification: HIDPPNotification, device, last_result) -> None:
...
def data(self) -> dict[str, Any]:
...
def keysym_to_keycode(keysym, _modifiers) -> Tuple[int, int]: # maybe should take shift into account
@ -1184,7 +1201,7 @@ def keysym_to_keycode(keysym, _modifiers) -> Tuple[int, int]: # maybe should ta
return keycode, level
class KeyPress(Action):
class KeyPress(ActionProtocol):
def __init__(self, args, warn=True):
self.key_names, self.action = self.regularize_args(args)
if not isinstance(self.key_names, list):
@ -1274,7 +1291,7 @@ class KeyPress(Action):
# super().keyUp(self.keys, current_key_modifiers)
class MouseScroll(Action):
class MouseScroll(ActionProtocol):
def __init__(self, amounts, warn=True):
if len(amounts) == 1 and isinstance(amounts[0], list):
amounts = amounts[0]
@ -1302,7 +1319,7 @@ class MouseScroll(Action):
return {"MouseScroll": self.amounts[:]}
class MouseClick(Action):
class MouseClick(ActionProtocol):
def __init__(self, args, warn=True):
if len(args) == 1 and isinstance(args[0], list):
args = args[0]
@ -1341,7 +1358,7 @@ class MouseClick(Action):
return {"MouseClick": [self.button, self.count]}
class Set(Action):
class Set(ActionProtocol):
def __init__(self, args, warn=True):
if not (isinstance(args, list) and len(args) > 2):
if warn:
@ -1387,7 +1404,7 @@ class Set(Action):
return {"Set": self.args[:]}
class Execute(Action):
class Execute(ActionProtocol):
def __init__(self, args, warn=True):
if isinstance(args, str):
args = [args]
@ -1411,7 +1428,7 @@ class Execute(Action):
return {"Execute": self.args[:]}
class Later(Action):
class Later(ActionProtocol):
def __init__(self, args, warn=True):
self.delay = 0
self.rule = Rule([])
@ -1446,7 +1463,7 @@ class Later(Action):
return {"Later": data}
COMPONENTS = {
COMPONENTS: dict[str, Rule | ConditionProtocol | ActionProtocol] = {
"Rule": Rule,
"Not": Not,
"Or": Or,

View File

@ -296,7 +296,10 @@ class ReprogrammableKeyV4(ReprogrammableKey):
if self._mapped_to is None:
self._getCidReporting()
self._device.keys._ensure_all_keys_queried()
task = str(special_keys.Task(self._device.keys.cid_to_tid[self._mapped_to]))
try:
task = str(special_keys.Task(self._device.keys.cid_to_tid[self._mapped_to]))
except ValueError:
task = f"Unknown_{self._mapped_to:x}"
return NamedInt(self._mapped_to, task)
@property

View File

@ -248,7 +248,9 @@ def _process_bluez_dbus(device: Device, path, dictionary: dict, signature):
def _cleanup_bluez_dbus(device: Device):
"""Remove dbus signal receiver for device"""
logger.info("bluez cleanup for %s", device)
diversion_remove_inheritance
if device and logger.isEnabledFor(logging.INFO):
logger.info(f"bluez cleanup for {device}")
dbus.watch_bluez_connect(device.hid_serial, None)

View File

@ -524,7 +524,7 @@ class ActionMenu:
wrapped = m[it][0]
c = wrapped.component
_rule_component_clipboard = diversion.RuleComponent().compile(c.data())
_rule_component_clipboard = diversion.compile_component(c.data())
def menu_do_cut(self, _mitem, m, it):
global _rule_component_clipboard
@ -545,7 +545,7 @@ class ActionMenu:
c = _rule_component_clipboard
_rule_component_clipboard = None
if c:
_rule_component_clipboard = diversion.RuleComponent().compile(c.data())
_rule_component_clipboard = diversion.compile_component(c.data())
self._menu_do_insert(_mitem, m, it, new_c=c, below=below)
self._on_update()
@ -1208,7 +1208,7 @@ class NotUI(RuleComponentUI):
class ActionUI(RuleComponentUI):
CLASS = diversion.Action
CLASS = diversion.ActionProtocol
@classmethod
def icon_name(cls):

View File

@ -36,7 +36,7 @@ class GtkSignal(Enum):
class ActionUI(RuleComponentUI):
CLASS = diversion.Action
CLASS = diversion.ActionProtocol
@classmethod
def icon_name(cls):

View File

@ -20,7 +20,6 @@ from typing import Any
from typing import Callable
from gi.repository import Gtk
from logitech_receiver import diversion
def norm(s):
@ -50,8 +49,6 @@ class CompletionEntry(Gtk.Entry):
class RuleComponentUI(abc.ABC):
CLASS = diversion.RuleComponent
def __init__(self, panel, on_update: Callable = None):
self.panel = panel
self.widgets = {} # widget -> coord. in grid
@ -109,5 +106,5 @@ class RuleComponentUI(abc.ABC):
for c in self.panel.get_children():
self.panel.remove(c)
def update_devices(self): # noqa: B027
pass
def update_devices(self) -> None:
return None

View File

@ -36,7 +36,7 @@ class GtkSignal(Enum):
class ConditionUI(RuleComponentUI):
CLASS = diversion.Condition
CLASS = diversion.ConditionProtocol
@classmethod
def icon_name(cls):