324 lines
13 KiB
Python
324 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 enum import Enum
|
|
from shlex import quote as shlex_quote
|
|
|
|
from gi.repository import Gtk
|
|
from logitech_receiver import diversion
|
|
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
|
|
from logitech_receiver.diversion import buttons
|
|
|
|
from solaar.i18n import _
|
|
from solaar.ui.rule_base import CompletionEntry
|
|
from solaar.ui.rule_base import RuleComponentUI
|
|
|
|
|
|
class GtkSignal(Enum):
|
|
CHANGED = "changed"
|
|
CLICKED = "clicked"
|
|
TOGGLED = "toggled"
|
|
|
|
|
|
class ActionUI(RuleComponentUI):
|
|
CLASS = diversion.Action
|
|
|
|
@classmethod
|
|
def icon_name(cls):
|
|
return "go-next"
|
|
|
|
|
|
class KeyPressUI(ActionUI):
|
|
CLASS = diversion.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(GtkSignal.CLICKED.value, 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(GtkSignal.TOGGLED.value, 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(GtkSignal.TOGGLED.value, 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(GtkSignal.TOGGLED.value, 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(GtkSignal.CHANGED.value, 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(GtkSignal.CLICKED.value, 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, 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 = diversion.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(GtkSignal.CHANGED.value, self._on_update)
|
|
self.field_y.connect(GtkSignal.CHANGED.value, 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=True):
|
|
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 = diversion.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(GtkSignal.CHANGED.value, self._on_update)
|
|
self.field_c.connect(GtkSignal.CHANGED.value, self._on_update)
|
|
self.field_d.connect(GtkSignal.CHANGED.value, 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=True):
|
|
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 = diversion.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(GtkSignal.CLICKED.value, 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(GtkSignal.CHANGED.value, 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(GtkSignal.CLICKED.value, 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=True):
|
|
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])
|