319 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			319 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
| ## 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(
 | |
|             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(label=_("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(label=_("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(
 | |
|             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(
 | |
|             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(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(label=_("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(label=_("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])
 |