Repository: pwr-Solaar/Solaar

Issue:
Add rule component to sleep for a while
It is possible to add a sleep action in the rule editor so we can perform keystrokes after a program has loaded?
(e.g. I load google docs from terminal opening chrome on a specific document, I wait 5 seconds so google docs loads correctly and then I invoke a key press for activating speech recognition).



Discussion:
It is possible.  The issue is just what the sleep action does.
The simplest design is to have sleep take two arguments - a delay and a rule to run after the delay.  This decouples processing of subsequent rule pieces and other rules from the processing of the slept part of the rule.
So you would have a rule something like:
```
---
- MouseGesture: [Mouse Gesture Button, Mouse Right]
- Execute: [docs]
- Sleep:
  - 5.5
  - Rule:
    - KeyPress:
      - [whatever key is needed to start speech recognition]
      - click
- Execute: [notify-send, Starting speech recognition in 5.5 seconds]
...
```
This design makes it easy to set up the sleep and the processing after the sleep.

Resolving patch:
diff --git a/lib/logitech_receiver/diversion.py b/lib/logitech_receiver/diversion.py
--- a/lib/logitech_receiver/diversion.py
+++ b/lib/logitech_receiver/diversion.py
@@ -398,6 +398,10 @@ def evaluate(self, feature, notification, device, status, last_result):
                 return result
         return result
 
+    def once(self, feature, notification, device, status, last_result):
+        self.evaluate(feature, notification, device, status, last_result)
+        return False
+
     def data(self):
         return {'Rule': [c.data() for c in self.components]}
 
@@ -1092,6 +1096,38 @@ def data(self):
         return {'Execute': self.args[:]}
 
 
+class Later(Action):
+    def __init__(self, args, warn=True):
+        self.delay = 0
+        self.rule = Rule([])
+        self.components = []
+        if not (isinstance(args, list)):
+            args = [args]
+        if not (isinstance(args, list) and len(args) >= 1):
+            if warn:
+                _log.warn('rule Later argument not list with minimum length 1: %s', args)
+        elif not (isinstance(args[0], int)) or not 0 < args[0] < 101:
+            if warn:
+                _log.warn('rule Later argument delay not integer between 1 and 100: %s', args)
+        else:
+            self.delay = args[0]
+            self.rule = Rule(args[1:], warn=warn)
+            self.components = self.rule.components
+
+    def __str__(self):
+        return 'Later: [' + str(self.delay) + ', ' + ', '.join(str(c) for c in self.components) + ']'
+
+    def evaluate(self, feature, notification, device, status, last_result):
+        if self.delay and self.rule:
+            GLib.timeout_add_seconds(self.delay, Rule.once, self.rule, feature, notification, device, status, last_result)
+        return None
+
+    def data(self):
+        data = [c.data() for c in self.components]
+        data.insert(0, self.delay)
+        return {'Later': data}
+
+
 COMPONENTS = {
     'Rule': Rule,
     'Not': Not,
@@ -1113,6 +1149,7 @@ def data(self):
     'MouseClick': MouseClick,
     'Set': Set,
     'Execute': Execute,
+    'Later': Later,
 }
 
 built_in_rules = Rule([])
diff --git a/lib/solaar/ui/diversion_rules.py b/lib/solaar/ui/diversion_rules.py
--- a/lib/solaar/ui/diversion_rules.py
+++ b/lib/solaar/ui/diversion_rules.py
@@ -257,7 +257,7 @@ def _populate_model(self, model, it, rule_component, level=0, pos=-1, editable=N
                 editable = editable or (rule_component.source is not None)
         wrapped = RuleComponentWrapper(rule_component, level, editable=editable)
         piter = model.insert(it, pos, (wrapped, ))
-        if isinstance(rule_component, (_DIV.Rule, _DIV.And, _DIV.Or)):
+        if isinstance(rule_component, (_DIV.Rule, _DIV.And, _DIV.Or, _DIV.Later)):
             for c in rule_component.components:
                 ed = editable or (isinstance(c, _DIV.Rule) and c.source is not None)
                 self._populate_model(model, piter, c, level + 1, editable=ed)
@@ -531,6 +531,7 @@ def _menu_insert(self, m, it, below=False):
                         (_('Mouse click'), _DIV.MouseClick, ['left', 1]),
                         (_('Set'), _DIV.Set, [None, '', None]),
                         (_('Execute'), _DIV.Execute, ['']),
+                        (_('Later'), _DIV.Later, [1]),
                     ]
                 ],
             ]
@@ -1117,6 +1118,42 @@ def left_label(cls, component):
         return _('Or')
 
 
+class LaterUI(RuleComponentUI):
+
+    CLASS = _DIV.Later
+    MIN_VALUE = 1
+    MAX_VALUE = 100
+
+    def create_widgets(self):
+        self.widgets = {}
+        self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True)
+        self.label.set_text(_('Number of seconds to delay.'))
+        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.delay)
+
+    def collect_value(self):
+        return [int(self.field.get_value())] + self.component.components
+
+    @classmethod
+    def left_label(cls, component):
+        return _('Later')
+
+    @classmethod
+    def right_label(cls, component):
+        return str(component.delay)
+
+
 class NotUI(RuleComponentUI):
 
     CLASS = _DIV.Not
@@ -2505,6 +2542,7 @@ def _on_update(self, *_args):
     _DIV.Not: NotUI,
     _DIV.Or: OrUI,
     _DIV.And: AndUI,
+    _DIV.Later: LaterUI,
     _DIV.Process: ProcessUI,
     _DIV.MouseProcess: MouseProcessUI,
     _DIV.Active: ActiveUI,


Test patch: