1828 lines
		
	
	
		
			69 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			1828 lines
		
	
	
		
			69 KiB
		
	
	
	
		
			Python
		
	
	
	
| # -*- python-mode -*-
 | |
| 
 | |
| ## Copyright (C) 2020 Solaar
 | |
| ##
 | |
| ## 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 defaultdict
 | |
| from contextlib import contextmanager as contextlib_contextmanager
 | |
| from logging import getLogger
 | |
| from shlex import quote as shlex_quote
 | |
| 
 | |
| from gi.repository import Gdk, GObject, Gtk
 | |
| from logitech_receiver import diversion as _DIV
 | |
| from logitech_receiver.common import NamedInt, UnsortedNamedInts
 | |
| from logitech_receiver.diversion import XK_KEYS as _XK_KEYS
 | |
| from logitech_receiver.diversion import Key as _Key
 | |
| from logitech_receiver.diversion import buttons as _buttons
 | |
| from logitech_receiver.hidpp20 import FEATURE as _ALL_FEATURES
 | |
| from logitech_receiver.settings import KIND as _SKIND
 | |
| from logitech_receiver.settings import Setting as _Setting
 | |
| from logitech_receiver.settings_templates import SETTINGS as _SETTINGS
 | |
| from logitech_receiver.special_keys import CONTROL as _CONTROL
 | |
| from solaar.i18n import _
 | |
| 
 | |
| _log = getLogger(__name__)
 | |
| del getLogger
 | |
| 
 | |
| #
 | |
| #
 | |
| #
 | |
| 
 | |
| _diversion_dialog = None
 | |
| _rule_component_clipboard = None
 | |
| 
 | |
| 
 | |
| class RuleComponentWrapper(GObject.GObject):
 | |
|     def __init__(self, component, level=0, editable=False):
 | |
|         self.component = component
 | |
|         self.level = level
 | |
|         self.editable = editable
 | |
|         GObject.GObject.__init__(self)
 | |
| 
 | |
|     def display_left(self):
 | |
|         if isinstance(self.component, _DIV.Rule):
 | |
|             if self.level == 0:
 | |
|                 return _('Built-in rules') if not self.editable else _('User-defined rules')
 | |
|             if self.level == 1:
 | |
|                 return '  ' + _('Rule')
 | |
|             return '  ' + _('Sub-rule')
 | |
|         if self.component is None:
 | |
|             return _('[empty]')
 | |
|         return '  ' + self.__component_ui().left_label(self.component)
 | |
| 
 | |
|     def display_right(self):
 | |
|         if self.component is None:
 | |
|             return ''
 | |
|         return self.__component_ui().right_label(self.component)
 | |
| 
 | |
|     def display_icon(self):
 | |
|         if self.component is None:
 | |
|             return ''
 | |
|         if isinstance(self.component, _DIV.Rule) and self.level == 0:
 | |
|             return 'emblem-system' if not self.editable else 'avatar-default'
 | |
|         return self.__component_ui().icon_name()
 | |
| 
 | |
|     def __component_ui(self):
 | |
|         return COMPONENT_UI.get(type(self.component), UnsupportedRuleComponentUI)
 | |
| 
 | |
| 
 | |
| class DiversionDialog:
 | |
|     def __init__(self):
 | |
| 
 | |
|         window = Gtk.Window()
 | |
|         window.set_title(_('Solaar Rule Editor'))
 | |
|         window.connect('delete-event', self._closing)
 | |
|         vbox = Gtk.VBox()
 | |
| 
 | |
|         self.top_panel, self.view = self._create_top_panel()
 | |
|         for col in self._create_view_columns():
 | |
|             self.view.append_column(col)
 | |
|         vbox.pack_start(self.top_panel, True, True, 0)
 | |
| 
 | |
|         self.dirty = False  # if dirty, there are pending changes to be saved
 | |
| 
 | |
|         self.type_ui = {}
 | |
|         self.update_ui = {}
 | |
|         self.bottom_panel = self._create_bottom_panel()
 | |
|         self.ui = defaultdict(lambda: UnsupportedRuleComponentUI(self.bottom_panel))
 | |
|         self.ui.update({  # one instance per type
 | |
|             rc_class: rc_ui_class(self.bottom_panel, on_update=self.on_update)
 | |
|             for rc_class, rc_ui_class in COMPONENT_UI.items()
 | |
|         })
 | |
|         bottom_box = Gtk.ScrolledWindow()
 | |
|         bottom_box.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER)
 | |
|         bottom_box.add(self.bottom_panel)
 | |
|         vbox.pack_start(bottom_box, True, True, 0)
 | |
| 
 | |
|         self.model = self._create_model()
 | |
|         self.view.set_model(self.model)
 | |
|         self.view.expand_all()
 | |
| 
 | |
|         window.add(vbox)
 | |
| 
 | |
|         geometry = Gdk.Geometry()
 | |
|         geometry.min_width = 800
 | |
|         geometry.min_height = 800
 | |
|         window.set_geometry_hints(None, geometry, Gdk.WindowHints.MIN_SIZE)
 | |
|         window.set_position(Gtk.WindowPosition.CENTER)
 | |
| 
 | |
|         window.show_all()
 | |
| 
 | |
|         window.connect('delete-event', lambda w, e: w.hide_on_delete() or True)
 | |
| 
 | |
|         style = window.get_style_context()
 | |
|         style.add_class('solaar')
 | |
|         self.window = window
 | |
|         self._editing_component = None
 | |
| 
 | |
|     def _closing(self, w, e):
 | |
|         if self.dirty:
 | |
|             dialog = Gtk.MessageDialog(
 | |
|                 self.window,
 | |
|                 type=Gtk.MessageType.QUESTION,
 | |
|                 title=_('Make changes permanent?'),
 | |
|                 flags=Gtk.DialogFlags.MODAL,
 | |
|             )
 | |
|             dialog.set_default_size(400, 100)
 | |
|             dialog.add_buttons(
 | |
|                 _('Yes'),
 | |
|                 Gtk.ResponseType.YES,
 | |
|                 _('No'),
 | |
|                 Gtk.ResponseType.NO,
 | |
|                 _('Cancel'),
 | |
|                 Gtk.ResponseType.CANCEL,
 | |
|             )
 | |
|             dialog.set_markup(_('If you choose No, changes will be lost when Solaar is closed.'))
 | |
|             dialog.show_all()
 | |
|             response = dialog.run()
 | |
|             dialog.destroy()
 | |
|             if response == Gtk.ResponseType.NO:
 | |
|                 w.hide()
 | |
|             elif response == Gtk.ResponseType.YES:
 | |
|                 self._save_yaml_file()
 | |
|                 w.hide()
 | |
|             else:
 | |
|                 # don't close
 | |
|                 return True
 | |
|         else:
 | |
|             w.hide()
 | |
| 
 | |
|     def _reload_yaml_file(self):
 | |
|         self.discard_btn.set_sensitive(False)
 | |
|         self.save_btn.set_sensitive(False)
 | |
|         self.dirty = False
 | |
|         for c in self.bottom_panel.get_children():
 | |
|             self.bottom_panel.remove(c)
 | |
|         _DIV._load_config_rule_file()
 | |
|         self.model = self._create_model()
 | |
|         self.view.set_model(self.model)
 | |
|         self.view.expand_all()
 | |
| 
 | |
|     def _save_yaml_file(self):
 | |
|         if _DIV._save_config_rule_file():
 | |
|             self.dirty = False
 | |
|             self.save_btn.set_sensitive(False)
 | |
|             self.discard_btn.set_sensitive(False)
 | |
| 
 | |
|     def _create_top_panel(self):
 | |
|         sw = Gtk.ScrolledWindow()
 | |
|         sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.ALWAYS)
 | |
|         view = Gtk.TreeView()
 | |
|         view.set_headers_visible(False)
 | |
|         view.set_enable_tree_lines(True)
 | |
|         view.set_reorderable(False)
 | |
| 
 | |
|         view.connect('key-press-event', self._event_key_pressed)
 | |
|         view.connect('button-release-event', self._event_button_released)
 | |
|         view.get_selection().connect('changed', self._selection_changed)
 | |
|         sw.add(view)
 | |
|         sw.set_size_request(0, 600)
 | |
| 
 | |
|         button_box = Gtk.HBox(spacing=20)
 | |
|         self.save_btn = Gtk.Button.new_from_icon_name('document-save', Gtk.IconSize.BUTTON)
 | |
|         self.save_btn.set_label(_('Save changes'))
 | |
|         self.save_btn.set_always_show_image(True)
 | |
|         self.save_btn.set_sensitive(False)
 | |
|         self.save_btn.set_valign(Gtk.Align.CENTER)
 | |
|         self.discard_btn = Gtk.Button.new_from_icon_name('document-revert', Gtk.IconSize.BUTTON)
 | |
|         self.discard_btn.set_label(_('Discard changes'))
 | |
|         self.discard_btn.set_always_show_image(True)
 | |
|         self.discard_btn.set_sensitive(False)
 | |
|         self.discard_btn.set_valign(Gtk.Align.CENTER)
 | |
|         self.save_btn.connect('clicked', lambda *_args: self._save_yaml_file())
 | |
|         self.discard_btn.connect('clicked', lambda *_args: self._reload_yaml_file())
 | |
|         button_box.pack_start(self.save_btn, False, False, 0)
 | |
|         button_box.pack_start(self.discard_btn, False, False, 0)
 | |
|         button_box.set_halign(Gtk.Align.CENTER)
 | |
|         button_box.set_valign(Gtk.Align.CENTER)
 | |
|         button_box.set_size_request(0, 50)
 | |
| 
 | |
|         vbox = Gtk.VBox()
 | |
|         vbox.pack_start(button_box, False, False, 0)
 | |
|         vbox.pack_start(sw, True, True, 0)
 | |
| 
 | |
|         return vbox, view
 | |
| 
 | |
|     def _create_model(self):
 | |
|         model = Gtk.TreeStore(RuleComponentWrapper)
 | |
|         if len(_DIV.rules.components) == 1:
 | |
|             # only built-in rules - add empty user rule list
 | |
|             _DIV.rules.components.insert(0, _DIV.Rule([], source=_DIV._file_path))
 | |
|         self._populate_model(model, None, _DIV.rules.components)
 | |
|         return model
 | |
| 
 | |
|     def _create_view_columns(self):
 | |
|         cell_icon = Gtk.CellRendererPixbuf()
 | |
|         cell1 = Gtk.CellRendererText()
 | |
|         col1 = Gtk.TreeViewColumn('Type')
 | |
|         col1.pack_start(cell_icon, False)
 | |
|         col1.pack_start(cell1, True)
 | |
|         col1.set_cell_data_func(cell1, lambda _c, c, m, it, _d: c.set_property('text', m.get_value(it, 0).display_left()))
 | |
|         cell2 = Gtk.CellRendererText()
 | |
|         col2 = Gtk.TreeViewColumn('Summary')
 | |
|         col2.pack_start(cell2, True)
 | |
|         col2.set_cell_data_func(cell2, lambda _c, c, m, it, _d: c.set_property('text', m.get_value(it, 0).display_right()))
 | |
|         col2.set_cell_data_func(
 | |
|             cell_icon, lambda _c, c, m, it, _d: c.set_property('icon-name',
 | |
|                                                                m.get_value(it, 0).display_icon())
 | |
|         )
 | |
|         return col1, col2
 | |
| 
 | |
|     def _populate_model(self, model, it, rule_component, level=0, pos=-1, editable=None):
 | |
|         if isinstance(rule_component, list):
 | |
|             for c in rule_component:
 | |
|                 self._populate_model(model, it, c, level=level, pos=pos, editable=editable)
 | |
|                 if pos >= 0:
 | |
|                     pos += 1
 | |
|             return
 | |
|         if editable is None:
 | |
|             editable = model[it][0].editable if it is not None else False
 | |
|             if isinstance(rule_component, _DIV.Rule):
 | |
|                 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)):
 | |
|             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)
 | |
|             if len(rule_component.components) == 0:
 | |
|                 self._populate_model(model, piter, None, level + 1, editable=editable)
 | |
|         elif isinstance(rule_component, _DIV.Not):
 | |
|             self._populate_model(model, piter, rule_component.component, level + 1, editable=editable)
 | |
| 
 | |
|     def _create_bottom_panel(self):
 | |
|         grid = Gtk.Grid()
 | |
|         grid.set_row_spacing(10)
 | |
|         grid.set_column_spacing(10)
 | |
|         grid.set_halign(Gtk.Align.CENTER)
 | |
|         grid.set_valign(Gtk.Align.CENTER)
 | |
|         grid.set_size_request(0, 120)
 | |
|         return grid
 | |
| 
 | |
|     def on_update(self):
 | |
|         self.view.queue_draw()
 | |
|         self.dirty = True
 | |
|         self.save_btn.set_sensitive(True)
 | |
|         self.discard_btn.set_sensitive(True)
 | |
| 
 | |
|     def _selection_changed(self, selection):
 | |
|         self.bottom_panel.set_sensitive(False)
 | |
|         (model, it) = selection.get_selected()
 | |
|         if it is None:
 | |
|             return
 | |
|         wrapped = model[it][0]
 | |
|         component = wrapped.component
 | |
|         self._editing_component = component
 | |
|         self.ui[type(component)].show(component, wrapped.editable)
 | |
|         self.bottom_panel.set_sensitive(wrapped.editable)
 | |
| 
 | |
|     def _event_key_pressed(self, v, e):
 | |
|         '''
 | |
|         Shortcuts:
 | |
|             Ctrl + I                insert component
 | |
|             Ctrl + Delete           delete row
 | |
|             &                       wrap with And
 | |
|             |                       wrap with Or
 | |
|             Shift + R               wrap with Rule
 | |
|             !                       negate
 | |
|             Ctrl + X                cut
 | |
|             Ctrl + C                copy
 | |
|             Ctrl + V                paste below (or here if empty)
 | |
|             Ctrl + Shift + V        paste above
 | |
|             *                       flatten
 | |
|             Ctrl + S                save changes
 | |
|         '''
 | |
|         state = e.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)
 | |
|         m, it = v.get_selection().get_selected()
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         parent_it = m.iter_parent(it)
 | |
|         parent_c = m[parent_it][0].component if wrapped.level > 0 else None
 | |
|         can_wrap = wrapped.editable and wrapped.component is not None and wrapped.level >= 2
 | |
|         can_delete = wrapped.editable and not isinstance(parent_c, _DIV.Not) and c is not None and wrapped.level >= 1
 | |
|         can_insert = wrapped.editable and not isinstance(parent_c, _DIV.Not) and wrapped.level >= 2
 | |
|         can_insert_only_rule = wrapped.editable and wrapped.level == 1
 | |
|         can_flatten = wrapped.editable and not isinstance(parent_c, _DIV.Not) and isinstance(
 | |
|             c, (_DIV.Rule, _DIV.And, _DIV.Or)
 | |
|         ) and wrapped.level >= 2 and len(c.components)
 | |
|         can_copy = wrapped.level >= 1
 | |
|         can_insert_root = wrapped.editable and wrapped.level == 0
 | |
|         if state & Gdk.ModifierType.CONTROL_MASK:
 | |
|             if can_delete and e.keyval in [Gdk.KEY_x, Gdk.KEY_X]:
 | |
|                 self._menu_do_cut(None, m, it)
 | |
|             elif can_copy and e.keyval in [Gdk.KEY_c, Gdk.KEY_C] and c is not None:
 | |
|                 self._menu_do_copy(None, m, it)
 | |
|             elif can_insert and _rule_component_clipboard is not None and e.keyval in [Gdk.KEY_v, Gdk.KEY_V]:
 | |
|                 self._menu_do_paste(None, m, it, below=c is not None and not (state & Gdk.ModifierType.SHIFT_MASK))
 | |
|             elif can_insert_only_rule and isinstance(_rule_component_clipboard,
 | |
|                                                      _DIV.Rule) and e.keyval in [Gdk.KEY_v, Gdk.KEY_V]:
 | |
|                 self._menu_do_paste(None, m, it, below=c is not None and not (state & Gdk.ModifierType.SHIFT_MASK))
 | |
|             elif can_insert_root and isinstance(_rule_component_clipboard, _DIV.Rule) and e.keyval in [Gdk.KEY_v, Gdk.KEY_V]:
 | |
|                 self._menu_do_paste(None, m, m.iter_nth_child(it, 0))
 | |
|             elif can_delete and e.keyval in [Gdk.KEY_KP_Delete, Gdk.KEY_Delete]:
 | |
|                 self._menu_do_delete(None, m, it)
 | |
|             elif (can_insert or can_insert_only_rule or can_insert_root) and e.keyval in [Gdk.KEY_i, Gdk.KEY_I]:
 | |
|                 menu = Gtk.Menu()
 | |
|                 for item in self.__get_insert_menus(m, it, c, can_insert, can_insert_only_rule, can_insert_root):
 | |
|                     menu.append(item)
 | |
|                 menu.show_all()
 | |
|                 rect = self.view.get_cell_area(m.get_path(it), self.view.get_column(1))
 | |
|                 menu.popup_at_rect(self.window.get_window(), rect, Gdk.Gravity.WEST, Gdk.Gravity.CENTER, e)
 | |
|             elif self.dirty and e.keyval in [Gdk.KEY_s, Gdk.KEY_S]:
 | |
|                 self._save_yaml_file()
 | |
|         else:
 | |
|             if can_wrap:
 | |
|                 if e.keyval == Gdk.KEY_exclam:
 | |
|                     self._menu_do_negate(None, m, it)
 | |
|                 elif e.keyval == Gdk.KEY_ampersand:
 | |
|                     self._menu_do_wrap(None, m, it, _DIV.And)
 | |
|                 elif e.keyval == Gdk.KEY_bar:
 | |
|                     self._menu_do_wrap(None, m, it, _DIV.Or)
 | |
|                 elif e.keyval in [Gdk.KEY_r, Gdk.KEY_R] and (state & Gdk.ModifierType.SHIFT_MASK):
 | |
|                     self._menu_do_wrap(None, m, it, _DIV.Rule)
 | |
|             if can_flatten and e.keyval in [Gdk.KEY_asterisk, Gdk.KEY_KP_Multiply]:
 | |
|                 self._menu_do_flatten(None, m, it)
 | |
| 
 | |
|     def __get_insert_menus(self, m, it, c, can_insert, can_insert_only_rule, can_insert_root):
 | |
|         items = []
 | |
|         if can_insert:
 | |
|             ins = self._menu_insert(m, it)
 | |
|             items.append(ins)
 | |
|             if c is None:  # just a placeholder
 | |
|                 ins.set_label(_('Insert here'))
 | |
|             else:
 | |
|                 ins.set_label(_('Insert above'))
 | |
|                 ins2 = self._menu_insert(m, it, below=True)
 | |
|                 ins2.set_label(_('Insert below'))
 | |
|                 items.append(ins2)
 | |
|         elif can_insert_only_rule:
 | |
|             ins = self._menu_create_rule(m, it)
 | |
|             items.append(ins)
 | |
|             if c is None:
 | |
|                 ins.set_label(_('Insert new rule here'))
 | |
|             else:
 | |
|                 ins.set_label(_('Insert new rule above'))
 | |
|                 ins2 = self._menu_create_rule(m, it, below=True)
 | |
|                 ins2.set_label(_('Insert new rule below'))
 | |
|                 items.append(ins2)
 | |
|         elif can_insert_root:
 | |
|             ins = self._menu_create_rule(m, m.iter_nth_child(it, 0))
 | |
|             items.append(ins)
 | |
|         return items
 | |
| 
 | |
|     def _event_button_released(self, v, e):
 | |
|         if e.button == 3:  # right click
 | |
|             m, it = v.get_selection().get_selected()
 | |
|             wrapped = m[it][0]
 | |
|             c = wrapped.component
 | |
|             parent_it = m.iter_parent(it)
 | |
|             parent_c = m[parent_it][0].component if wrapped.level > 0 else None
 | |
|             menu = Gtk.Menu()
 | |
|             can_wrap = wrapped.editable and wrapped.component is not None and wrapped.level >= 2
 | |
|             can_delete = wrapped.editable and not isinstance(parent_c, _DIV.Not) and c is not None and wrapped.level >= 1
 | |
|             can_insert = wrapped.editable and not isinstance(parent_c, _DIV.Not) and wrapped.level >= 2
 | |
|             can_insert_only_rule = wrapped.editable and wrapped.level == 1
 | |
|             can_flatten = wrapped.editable and not isinstance(parent_c, _DIV.Not) and isinstance(
 | |
|                 c, (_DIV.Rule, _DIV.And, _DIV.Or)
 | |
|             ) and wrapped.level >= 2 and len(c.components)
 | |
|             can_copy = wrapped.level >= 1
 | |
|             can_insert_root = wrapped.editable and wrapped.level == 0
 | |
|             for item in self.__get_insert_menus(m, it, c, can_insert, can_insert_only_rule, can_insert_root):
 | |
|                 menu.append(item)
 | |
|             if can_flatten:
 | |
|                 menu.append(self._menu_flatten(m, it))
 | |
|             if can_wrap:
 | |
|                 menu.append(self._menu_wrap(m, it))
 | |
|                 menu.append(self._menu_negate(m, it))
 | |
|             if menu.get_children():
 | |
|                 menu.append(Gtk.SeparatorMenuItem(visible=True))
 | |
|             if can_delete:
 | |
|                 menu.append(self._menu_cut(m, it))
 | |
|             if can_copy and c is not None:
 | |
|                 menu.append(self._menu_copy(m, it))
 | |
|             if can_insert and _rule_component_clipboard is not None:
 | |
|                 p = self._menu_paste(m, it)
 | |
|                 menu.append(p)
 | |
|                 if c is None:  # just a placeholder
 | |
|                     p.set_label(_('Paste here'))
 | |
|                 else:
 | |
|                     p.set_label(_('Paste above'))
 | |
|                     p2 = self._menu_paste(m, it, below=True)
 | |
|                     p2.set_label(_('Paste below'))
 | |
|                     menu.append(p2)
 | |
|             elif can_insert_only_rule and isinstance(_rule_component_clipboard, _DIV.Rule):
 | |
|                 p = self._menu_paste(m, it)
 | |
|                 menu.append(p)
 | |
|                 if c is None:
 | |
|                     p.set_label(_('Paste rule here'))
 | |
|                 else:
 | |
|                     p.set_label(_('Paste rule above'))
 | |
|                     p2 = self._menu_paste(m, it, below=True)
 | |
|                     p2.set_label(_('Paste rule below'))
 | |
|                     menu.append(p2)
 | |
|             elif can_insert_root and isinstance(_rule_component_clipboard, _DIV.Rule):
 | |
|                 p = self._menu_paste(m, m.iter_nth_child(it, 0))
 | |
|                 p.set_label(_('Paste rule'))
 | |
|                 menu.append(p)
 | |
|             if menu.get_children() and can_delete:
 | |
|                 menu.append(Gtk.SeparatorMenuItem(visible=True))
 | |
|             if can_delete:
 | |
|                 menu.append(self._menu_delete(m, it))
 | |
|             if menu.get_children():
 | |
|                 menu.popup_at_pointer(e)
 | |
| 
 | |
|     def _menu_do_flatten(self, _mitem, m, it):
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         parent_it = m.iter_parent(it)
 | |
|         parent_c = m[parent_it][0].component
 | |
|         idx = parent_c.components.index(c)
 | |
|         if isinstance(c, _DIV.Not):
 | |
|             parent_c.components = [*parent_c.components[:idx], c.component, *parent_c.components[idx + 1:]]
 | |
|             children = [next(m[it].iterchildren())[0].component]
 | |
|         else:
 | |
|             parent_c.components = [*parent_c.components[:idx], *c.components, *parent_c.components[idx + 1:]]
 | |
|             children = [child[0].component for child in m[it].iterchildren()]
 | |
|         m.remove(it)
 | |
|         self._populate_model(m, parent_it, children, level=wrapped.level, pos=idx)
 | |
|         new_iter = m.iter_nth_child(parent_it, idx)
 | |
|         self.view.expand_row(m.get_path(parent_it), True)
 | |
|         self.view.get_selection().select_iter(new_iter)
 | |
|         self.on_update()
 | |
| 
 | |
|     def _menu_flatten(self, m, it):
 | |
|         menu_flatten = Gtk.MenuItem(_('Flatten'))
 | |
|         menu_flatten.connect('activate', self._menu_do_flatten, m, it)
 | |
|         menu_flatten.show()
 | |
|         return menu_flatten
 | |
| 
 | |
|     def _menu_do_insert(self, _mitem, m, it, new_c, below=False):
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         parent_it = m.iter_parent(it)
 | |
|         parent_c = m[parent_it][0].component
 | |
|         if len(parent_c.components) == 0:  # we had only a placeholder
 | |
|             idx = 0
 | |
|         else:
 | |
|             idx = parent_c.components.index(c)
 | |
|         if isinstance(new_c, _DIV.Rule) and wrapped.level == 1:
 | |
|             new_c.source = _DIV._file_path  # new rules will be saved to the YAML file
 | |
|         idx += int(below)
 | |
|         parent_c.components.insert(idx, new_c)
 | |
|         self._populate_model(m, parent_it, new_c, level=wrapped.level, pos=idx)
 | |
|         self.on_update()
 | |
|         if len(parent_c.components) == 1:
 | |
|             m.remove(it)  # remove placeholder in the end
 | |
|         new_iter = m.iter_nth_child(parent_it, idx)
 | |
|         self.view.get_selection().select_iter(new_iter)
 | |
|         if isinstance(new_c, (_DIV.Rule, _DIV.And, _DIV.Or, _DIV.Not)):
 | |
|             self.view.expand_row(m.get_path(new_iter), True)
 | |
| 
 | |
|     def _menu_do_insert_new(self, _mitem, m, it, cls, initial_value, below=False):
 | |
|         new_c = cls(initial_value)
 | |
|         return self._menu_do_insert(_mitem, m, it, new_c, below=below)
 | |
| 
 | |
|     def _menu_insert(self, m, it, below=False):
 | |
|         elements = [
 | |
|             _('Insert'),
 | |
|             [
 | |
|                 (_('Sub-rule'), _DIV.Rule, []),
 | |
|                 (_('Or'), _DIV.Or, []),
 | |
|                 (_('And'), _DIV.And, []),
 | |
|                 [
 | |
|                     _('Condition'),
 | |
|                     [
 | |
|                         (_('Feature'), _DIV.Feature, FeatureUI.FEATURES_WITH_DIVERSION[0]),
 | |
|                         (_('Process'), _DIV.Process, ''),
 | |
|                         (_('MouseProcess'), _DIV.MouseProcess, ''),
 | |
|                         (_('Report'), _DIV.Report, 0),
 | |
|                         (_('Modifiers'), _DIV.Modifiers, []),
 | |
|                         (_('Key'), _DIV.Key, ''),
 | |
|                         (_('Test'), _DIV.Test, next(iter(_DIV.TESTS))),
 | |
|                         (_('Mouse Gesture'), _DIV.MouseGesture, ''),
 | |
|                     ]
 | |
|                 ],
 | |
|                 [
 | |
|                     _('Action'),
 | |
|                     [
 | |
|                         (_('Key press'), _DIV.KeyPress, 'space'),
 | |
|                         (_('Mouse scroll'), _DIV.MouseScroll, [0, 0]),
 | |
|                         (_('Mouse click'), _DIV.MouseClick, ['left', 1]),
 | |
|                         (_('Execute'), _DIV.Execute, ['']),
 | |
|                         (_('Set'), _DIV.Set, [None, '', None]),
 | |
|                     ]
 | |
|                 ],
 | |
|             ]
 | |
|         ]
 | |
| 
 | |
|         def build(spec):
 | |
|             if isinstance(spec, list):  # has sub-menu
 | |
|                 label, children = spec
 | |
|                 item = Gtk.MenuItem(label)
 | |
|                 submenu = Gtk.Menu()
 | |
|                 item.set_submenu(submenu)
 | |
|                 for child in children:
 | |
|                     submenu.append(build(child))
 | |
|                 return item
 | |
|             elif isinstance(spec, tuple):  # has click action
 | |
|                 label, feature, *args = spec
 | |
|                 item = Gtk.MenuItem(label)
 | |
|                 args = [a.copy() if isinstance(a, list) else a for a in args]
 | |
|                 item.connect('activate', self._menu_do_insert_new, m, it, feature, *args, below)
 | |
|                 return item
 | |
|             else:
 | |
|                 return None
 | |
| 
 | |
|         menu_insert = build(elements)
 | |
|         menu_insert.show_all()
 | |
|         return menu_insert
 | |
| 
 | |
|     def _menu_create_rule(self, m, it, below=False):
 | |
|         menu_create_rule = Gtk.MenuItem(_('Insert new rule'))
 | |
|         menu_create_rule.connect('activate', self._menu_do_insert_new, m, it, _DIV.Rule, [], below)
 | |
|         menu_create_rule.show()
 | |
|         return menu_create_rule
 | |
| 
 | |
|     def _menu_do_delete(self, _mitem, m, it):
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         parent_it = m.iter_parent(it)
 | |
|         parent_c = m[parent_it][0].component
 | |
|         idx = parent_c.components.index(c)
 | |
|         parent_c.components.pop(idx)
 | |
|         if len(parent_c.components) == 0:  # placeholder
 | |
|             self._populate_model(m, parent_it, None, level=wrapped.level)
 | |
|         m.remove(it)
 | |
|         self.view.get_selection().select_iter(m.iter_nth_child(parent_it, max(0, min(idx, len(parent_c.components) - 1))))
 | |
|         self.on_update()
 | |
|         return c
 | |
| 
 | |
|     def _menu_delete(self, m, it):
 | |
|         menu_delete = Gtk.MenuItem(_('Delete'))
 | |
|         menu_delete.connect('activate', self._menu_do_delete, m, it)
 | |
|         menu_delete.show()
 | |
|         return menu_delete
 | |
| 
 | |
|     def _menu_do_negate(self, _mitem, m, it):
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         parent_it = m.iter_parent(it)
 | |
|         parent_c = m[parent_it][0].component
 | |
|         if isinstance(c, _DIV.Not):  # avoid double negation
 | |
|             self._menu_do_flatten(_mitem, m, it)
 | |
|             self.view.expand_row(m.get_path(parent_it), True)
 | |
|         elif isinstance(parent_c, _DIV.Not):  # avoid double negation
 | |
|             self._menu_do_flatten(_mitem, m, parent_it)
 | |
|         else:
 | |
|             idx = parent_c.components.index(c)
 | |
|             self._menu_do_insert_new(_mitem, m, it, _DIV.Not, c, below=True)
 | |
|             self._menu_do_delete(_mitem, m, m.iter_nth_child(parent_it, idx))
 | |
|         self.on_update()
 | |
| 
 | |
|     def _menu_negate(self, m, it):
 | |
|         menu_negate = Gtk.MenuItem(_('Negate'))
 | |
|         menu_negate.connect('activate', self._menu_do_negate, m, it)
 | |
|         menu_negate.show()
 | |
|         return menu_negate
 | |
| 
 | |
|     def _menu_do_wrap(self, _mitem, m, it, cls):
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         parent_it = m.iter_parent(it)
 | |
|         parent_c = m[parent_it][0].component
 | |
|         if isinstance(parent_c, _DIV.Not):
 | |
|             new_c = cls([c])
 | |
|             parent_c.component = new_c
 | |
|             m.remove(it)
 | |
|             self._populate_model(m, parent_it, new_c, level=wrapped.level, pos=0)
 | |
|             self.view.expand_row(m.get_path(parent_it), True)
 | |
|             self.view.get_selection().select_iter(m.iter_nth_child(parent_it, 0))
 | |
|         else:
 | |
|             idx = parent_c.components.index(c)
 | |
|             self._menu_do_insert_new(_mitem, m, it, cls, [c], below=True)
 | |
|             self._menu_do_delete(_mitem, m, m.iter_nth_child(parent_it, idx))
 | |
|         self.on_update()
 | |
| 
 | |
|     def _menu_wrap(self, m, it):
 | |
|         menu_wrap = Gtk.MenuItem(_('Wrap with'))
 | |
|         submenu_wrap = Gtk.Menu()
 | |
|         menu_sub_rule = Gtk.MenuItem(_('Sub-rule'))
 | |
|         menu_and = Gtk.MenuItem(_('And'))
 | |
|         menu_or = Gtk.MenuItem(_('Or'))
 | |
|         menu_sub_rule.connect('activate', self._menu_do_wrap, m, it, _DIV.Rule)
 | |
|         menu_and.connect('activate', self._menu_do_wrap, m, it, _DIV.And)
 | |
|         menu_or.connect('activate', self._menu_do_wrap, m, it, _DIV.Or)
 | |
|         submenu_wrap.append(menu_sub_rule)
 | |
|         submenu_wrap.append(menu_and)
 | |
|         submenu_wrap.append(menu_or)
 | |
|         menu_wrap.set_submenu(submenu_wrap)
 | |
|         menu_wrap.show_all()
 | |
|         return menu_wrap
 | |
| 
 | |
|     def _menu_do_cut(self, _mitem, m, it):
 | |
|         c = self._menu_do_delete(_mitem, m, it)
 | |
|         self.on_update()
 | |
|         global _rule_component_clipboard
 | |
|         _rule_component_clipboard = c
 | |
| 
 | |
|     def _menu_cut(self, m, it):
 | |
|         menu_cut = Gtk.MenuItem(_('Cut'))
 | |
|         menu_cut.connect('activate', self._menu_do_cut, m, it)
 | |
|         menu_cut.show()
 | |
|         return menu_cut
 | |
| 
 | |
|     def _menu_do_paste(self, _mitem, m, it, below=False):
 | |
|         global _rule_component_clipboard
 | |
|         c = _rule_component_clipboard
 | |
|         _rule_component_clipboard = None
 | |
|         if c:
 | |
|             _rule_component_clipboard = _DIV.RuleComponent().compile(c.data())
 | |
|             self._menu_do_insert(_mitem, m, it, new_c=c, below=below)
 | |
|             self.on_update()
 | |
| 
 | |
|     def _menu_paste(self, m, it, below=False):
 | |
|         menu_paste = Gtk.MenuItem(_('Paste'))
 | |
|         menu_paste.connect('activate', self._menu_do_paste, m, it, below)
 | |
|         menu_paste.show()
 | |
|         return menu_paste
 | |
| 
 | |
|     def _menu_do_copy(self, _mitem, m, it):
 | |
|         global _rule_component_clipboard
 | |
|         wrapped = m[it][0]
 | |
|         c = wrapped.component
 | |
|         _rule_component_clipboard = _DIV.RuleComponent().compile(c.data())
 | |
| 
 | |
|     def _menu_copy(self, m, it):
 | |
|         menu_copy = Gtk.MenuItem(_('Copy'))
 | |
|         menu_copy.connect('activate', self._menu_do_copy, m, it)
 | |
|         menu_copy.show()
 | |
|         return menu_copy
 | |
| 
 | |
| 
 | |
| ## Not currently used
 | |
| #
 | |
| # class HexEntry(Gtk.Entry, Gtk.Editable):
 | |
| #
 | |
| #     def do_insert_text(self, new_text, length, pos):
 | |
| #         new_text = new_text.upper()
 | |
| #         from string import hexdigits
 | |
| #         if any(c for c in new_text if c not in hexdigits):
 | |
| #             return pos
 | |
| #         else:
 | |
| #             self.get_buffer().insert_text(pos, new_text, length)
 | |
| #             return pos + length
 | |
| 
 | |
| 
 | |
| 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):
 | |
|         entry.liststore = Gtk.ListStore(str)
 | |
|         for v in sorted(values, key=str.casefold):
 | |
|             entry.liststore.append((v, ))
 | |
|         entry.completion = Gtk.EntryCompletion()
 | |
|         entry.completion.set_model(entry.liststore)
 | |
|         norm = lambda s: s.replace('_', '').replace(' ', '').lower()
 | |
|         entry.completion.set_match_func(lambda completion, key, it: norm(key) in norm(completion.get_model()[it][0]))
 | |
|         entry.completion.set_text_column(0)
 | |
|         entry.set_completion(entry.completion)
 | |
| 
 | |
| 
 | |
| 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 = False
 | |
|         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):
 | |
|         self._show_widgets(editable)
 | |
|         self.component = component
 | |
| 
 | |
|     def collect_value(self):
 | |
|         return None
 | |
| 
 | |
|     @contextlib_contextmanager
 | |
|     def ignore_changes(self):
 | |
|         self._ignore_changes = True
 | |
|         yield None
 | |
|         self._ignore_changes = False
 | |
| 
 | |
|     def _on_update(self, *_args):
 | |
|         if not self._ignore_changes and self.component is not None:
 | |
|             value = self.collect_value()
 | |
|             self.component.__init__(value)
 | |
|             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)
 | |
| 
 | |
| 
 | |
| class UnsupportedRuleComponentUI(RuleComponentUI):
 | |
| 
 | |
|     CLASS = None
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.label = Gtk.Label(valign=Gtk.Align.CENTER, hexpand=True, vexpand=True)
 | |
|         self.label.set_text(_('This editor does not support the selected rule component yet.'))
 | |
|         self.widgets[self.label] = (0, 0, 1, 1)
 | |
| 
 | |
|     @classmethod
 | |
|     def right_label(cls, component):
 | |
|         return str(component)
 | |
| 
 | |
| 
 | |
| class RuleUI(RuleComponentUI):
 | |
| 
 | |
|     CLASS = _DIV.Rule
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
| 
 | |
|     def collect_value(self):
 | |
|         return self.component.components[:]  # not editable on the bottom panel
 | |
| 
 | |
|     @classmethod
 | |
|     def left_label(cls, component):
 | |
|         return _('Rule')
 | |
| 
 | |
|     @classmethod
 | |
|     def icon_name(cls):
 | |
|         return 'format-justify-fill'
 | |
| 
 | |
| 
 | |
| class ConditionUI(RuleComponentUI):
 | |
| 
 | |
|     CLASS = _DIV.Condition
 | |
| 
 | |
|     @classmethod
 | |
|     def icon_name(cls):
 | |
|         return 'dialog-question'
 | |
| 
 | |
| 
 | |
| class AndUI(RuleComponentUI):
 | |
| 
 | |
|     CLASS = _DIV.And
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
| 
 | |
|     def collect_value(self):
 | |
|         return self.component.components[:]  # not editable on the bottom panel
 | |
| 
 | |
|     @classmethod
 | |
|     def left_label(cls, component):
 | |
|         return _('And')
 | |
| 
 | |
| 
 | |
| class OrUI(RuleComponentUI):
 | |
| 
 | |
|     CLASS = _DIV.Or
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
| 
 | |
|     def collect_value(self):
 | |
|         return self.component.components[:]  # not editable on the bottom panel
 | |
| 
 | |
|     @classmethod
 | |
|     def left_label(cls, component):
 | |
|         return _('Or')
 | |
| 
 | |
| 
 | |
| class NotUI(RuleComponentUI):
 | |
| 
 | |
|     CLASS = _DIV.Not
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
| 
 | |
|     def collect_value(self):
 | |
|         return self.component.component  # not editable on the bottom panel
 | |
| 
 | |
|     @classmethod
 | |
|     def left_label(cls, component):
 | |
|         return _('Not')
 | |
| 
 | |
| 
 | |
| class ProcessUI(ConditionUI):
 | |
| 
 | |
|     CLASS = _DIV.Process
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         self.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True)
 | |
|         self.field.set_size_request(600, 0)
 | |
|         self.field.connect('changed', self._on_update)
 | |
|         self.widgets[self.field] = (0, 0, 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.field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True)
 | |
|         self.field.set_size_request(600, 0)
 | |
|         self.field.connect('changed', self._on_update)
 | |
|         self.widgets[self.field] = (0, 0, 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 = [
 | |
|         'CROWN',
 | |
|         'GESTURE 2',
 | |
|         'REPROG CONTROLS V4',
 | |
|         'THUMB WHEEL',
 | |
|     ]
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         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, 0, 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 '%s (%04X)' % (str(component.feature), int(component.feature or 0))
 | |
| 
 | |
| 
 | |
| class ReportUI(ConditionUI):
 | |
| 
 | |
|     CLASS = _DIV.Report
 | |
|     MIN_VALUE = -1  # for invalid values
 | |
|     MAX_VALUE = 15
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         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, 0, 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.labels = {}
 | |
|         self.switches = {}
 | |
|         for i, m in enumerate(_DIV.MODIFIERS):
 | |
|             switch = Gtk.Switch(halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True, vexpand=True)
 | |
|             label = Gtk.Label(m, halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|             self.widgets[label] = (i, 0, 1, 1)
 | |
|             self.widgets[switch] = (i, 1, 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.key_field = CompletionEntry(
 | |
|             self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.CENTER, hexpand=True, vexpand=True
 | |
|         )
 | |
|         self.key_field.set_size_request(600, 0)
 | |
|         self.key_field.connect('changed', self._on_update)
 | |
|         self.widgets[self.key_field] = (0, 0, 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, 0, 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, 0, 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 '%s (%04X) (%s)' % (str(component.key), int(component.key), _(component.action)) if component.key else 'None'
 | |
| 
 | |
| 
 | |
| class TestUI(ConditionUI):
 | |
| 
 | |
|     CLASS = _DIV.Test
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         self.field = Gtk.ComboBoxText.new_with_entry()
 | |
|         self.field.append('', '')
 | |
|         for t in _DIV.TESTS:
 | |
|             self.field.append(t, t)
 | |
|         self.field.set_valign(Gtk.Align.CENTER)
 | |
|         self.field.set_vexpand(True)
 | |
|         self.field.set_size_request(600, 0)
 | |
|         CompletionEntry.add_completion_to_entry(self.field.get_child(), _DIV.TESTS)
 | |
|         self.field.connect('changed', self._on_update)
 | |
|         self.widgets[self.field] = (0, 0, 1, 1)
 | |
| 
 | |
|     def show(self, component, editable):
 | |
|         super().show(component, editable)
 | |
|         with self.ignore_changes():
 | |
|             self.field.set_active_id(component.test or '')
 | |
|             if component.test not in _DIV.TESTS:
 | |
|                 self.field.get_child().set_text(component.test)
 | |
|                 self._change_status_icon()
 | |
| 
 | |
|     def collect_value(self):
 | |
|         return (self.field.get_active_text() or '').strip()
 | |
| 
 | |
|     def _on_update(self, *args):
 | |
|         super()._on_update(*args)
 | |
|         self._change_status_icon()
 | |
| 
 | |
|     def _change_status_icon(self):
 | |
|         icon = 'dialog-warning' if self.component.test not in _DIV.TESTS else ''
 | |
|         self.field.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 str(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.del_btns = []
 | |
|         self.add_btn = Gtk.Button(_('Add action'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.add_btn.connect('clicked', self._clicked_add)
 | |
|         self.widgets[self.add_btn] = (1, 0, 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, 0, 1, 1)
 | |
|         return field
 | |
| 
 | |
|     def _create_del_btn(self):
 | |
|         btn = Gtk.Button(_('Delete'), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True, vexpand=True)
 | |
|         self.del_btns.append(btn)
 | |
|         self.widgets[btn] = (len(self.del_btns) - 1, 1, 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() + [''])
 | |
|         self.show(self.component)
 | |
|         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)
 | |
|         self.show(self.component)
 | |
|         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, 0, 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)
 | |
| 
 | |
| 
 | |
| 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.del_btns = []
 | |
|         self.add_btn = Gtk.Button(_('Add key'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.add_btn.connect('clicked', self._clicked_add)
 | |
|         self.widgets[self.add_btn] = (1, 0, 1, 1)
 | |
| 
 | |
|     def _create_field(self):
 | |
|         field = CompletionEntry(self.KEY_NAMES, halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         field.connect('changed', self._on_update)
 | |
|         self.fields.append(field)
 | |
|         self.widgets[field] = (len(self.fields) - 1, 0, 1, 1)
 | |
|         return field
 | |
| 
 | |
|     def _create_del_btn(self):
 | |
|         btn = Gtk.Button(_('Delete'), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True, vexpand=True)
 | |
|         self.del_btns.append(btn)
 | |
|         self.widgets[btn] = (len(self.del_btns) - 1, 1, 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() + [''])
 | |
|         self.show(self.component)
 | |
|         self.fields[len(self.component.key_symbols) - 1].grab_focus()
 | |
| 
 | |
|     def _clicked_del(self, _btn, pos):
 | |
|         v = self.collect_value()
 | |
|         v.pop(pos)
 | |
|         self.component.__init__(v)
 | |
|         self.show(self.component)
 | |
|         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_symbols
 | |
|                                                    ) and self.component.key_symbols[i] not in self.KEY_NAMES else ''
 | |
|                 f.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
 | |
| 
 | |
|     def show(self, component, editable):
 | |
|         n = len(component.key_symbols)
 | |
|         while len(self.fields) < n:
 | |
|             self._create_field()
 | |
|             self._create_del_btn()
 | |
|         self.widgets[self.add_btn] = (n + 1, 0, 1, 1)
 | |
|         super().show(component, editable)
 | |
|         for i in range(n):
 | |
|             field = self.fields[i]
 | |
|             with self.ignore_changes():
 | |
|                 field.set_text(component.key_symbols[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_text().strip() for f in self.fields if f.get_visible()]
 | |
| 
 | |
|     @classmethod
 | |
|     def left_label(cls, component):
 | |
|         return _('Key press')
 | |
| 
 | |
|     @classmethod
 | |
|     def right_label(cls, component):
 | |
|         return ' + '.join(component.key_symbols)
 | |
| 
 | |
| 
 | |
| class MouseScrollUI(ActionUI):
 | |
| 
 | |
|     CLASS = _DIV.MouseScroll
 | |
|     MIN_VALUE = -2000
 | |
|     MAX_VALUE = 2000
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         self.label_x = Gtk.Label(label='x', halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.label_y = Gtk.Label(label='y', halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=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 field in [self.field_x, self.field_y]:
 | |
|             field.set_halign(Gtk.Align.CENTER)
 | |
|             field.set_valign(Gtk.Align.START)
 | |
|             field.set_vexpand(True)
 | |
|         self.field_x.connect('changed', self._on_update)
 | |
|         self.field_y.connect('changed', self._on_update)
 | |
|         self.widgets[self.label_x] = (0, 0, 1, 1)
 | |
|         self.widgets[self.label_y] = (1, 0, 1, 1)
 | |
|         self.widgets[self.field_x] = (0, 1, 1, 1)
 | |
|         self.widgets[self.field_y] = (1, 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())
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         self.label_b = Gtk.Label(label=_('Button'), halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.label_c = Gtk.Label(label=_('Count'), halign=Gtk.Align.START, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.field_b = CompletionEntry(self.BUTTONS)
 | |
|         self.field_c = Gtk.SpinButton.new_with_range(self.MIN_VALUE, self.MAX_VALUE, 1)
 | |
|         for field in [self.field_b, self.field_c]:
 | |
|             field.set_halign(Gtk.Align.CENTER)
 | |
|             field.set_valign(Gtk.Align.START)
 | |
|             field.set_vexpand(True)
 | |
|         self.field_b.connect('changed', self._on_update)
 | |
|         self.field_c.connect('changed', self._on_update)
 | |
|         self.widgets[self.label_b] = (0, 0, 1, 1)
 | |
|         self.widgets[self.label_c] = (1, 0, 1, 1)
 | |
|         self.widgets[self.field_b] = (0, 1, 1, 1)
 | |
|         self.widgets[self.field_c] = (1, 1, 1, 1)
 | |
| 
 | |
|     def show(self, component, editable):
 | |
|         super().show(component, editable)
 | |
|         with self.ignore_changes():
 | |
|             self.field_b.set_text(component.button)
 | |
|             self.field_c.set_value(component.count)
 | |
| 
 | |
|     def collect_value(self):
 | |
|         b, c = self.field_b.get_text(), int(self.field_c.get_value())
 | |
|         if b not in self.BUTTONS:
 | |
|             b = 'unknown'
 | |
|         return [b, c]
 | |
| 
 | |
|     @classmethod
 | |
|     def left_label(cls, component):
 | |
|         return _('Mouse click')
 | |
| 
 | |
|     @classmethod
 | |
|     def right_label(cls, component):
 | |
|         return f'{component.button} (x{component.count})'
 | |
| 
 | |
| 
 | |
| class ExecuteUI(ActionUI):
 | |
| 
 | |
|     CLASS = _DIV.Execute
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
|         self.fields = []
 | |
|         self.add_btn = Gtk.Button(_('Add argument'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.del_btns = []
 | |
|         self.add_btn.connect('clicked', self._clicked_add)
 | |
|         self.widgets[self.add_btn] = (1, 0, 1, 1)
 | |
| 
 | |
|     def _create_field(self):
 | |
|         field = Gtk.Entry(halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         field.set_size_request(150, 0)
 | |
|         field.connect('changed', self._on_update)
 | |
|         self.fields.append(field)
 | |
|         self.widgets[field] = (len(self.fields) - 1, 0, 1, 1)
 | |
|         return field
 | |
| 
 | |
|     def _create_del_btn(self):
 | |
|         btn = Gtk.Button(_('Delete'), halign=Gtk.Align.CENTER, valign=Gtk.Align.START, hexpand=True, vexpand=True)
 | |
|         btn.set_size_request(150, 0)
 | |
|         self.del_btns.append(btn)
 | |
|         self.widgets[btn] = (len(self.del_btns) - 1, 1, 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() + [''])
 | |
|         self.show(self.component)
 | |
|         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)
 | |
|         self.show(self.component)
 | |
|         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 = self.fields[i]
 | |
|             with self.ignore_changes():
 | |
|                 field.set_text(component.args[i])
 | |
|             self.del_btns[i].show()
 | |
|         self.widgets[self.add_btn] = (n + 1, 0, 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])
 | |
| 
 | |
| 
 | |
| def _from_named_ints(v, all_values):
 | |
|     """Obtain a NamedInt from NamedInts given its numeric value (as int or str) or name."""
 | |
|     if isinstance(v, str) and v.isdigit():
 | |
|         v = int(v)
 | |
|     if all_values and (v in all_values):
 | |
|         return all_values[v]
 | |
|     return v
 | |
| 
 | |
| 
 | |
| class SetValueControl(Gtk.HBox):
 | |
| 
 | |
|     TOGGLE_VALUES = [('~', _('Toggle')), ('t', _('True')), ('f', _('False'))]
 | |
| 
 | |
|     TYPES = ('toggle', 'choice', 'range')
 | |
| 
 | |
|     def __init__(self, on_change, *args, **kwargs):
 | |
|         super().__init__(*args, **kwargs)
 | |
|         self.on_change = on_change
 | |
|         self.toggle_widget = Gtk.ComboBoxText()
 | |
|         for v in self.TOGGLE_VALUES:
 | |
|             self.toggle_widget.append(*v)
 | |
|         self.toggle_widget.connect('changed', self._changed)
 | |
|         self.range_widget = Gtk.SpinButton.new_with_range(0, 0xFFFF, 1)
 | |
|         self.range_widget.connect('changed', self._changed)
 | |
|         self.choice_widget = Gtk.ComboBoxText.new_with_entry()
 | |
|         self.choice_widget.connect('changed', self._changed)
 | |
|         self.unsupported_label = Gtk.Label(_('Unsupported setting'))
 | |
|         for w in [self.toggle_widget, self.range_widget, self.choice_widget, self.unsupported_label]:
 | |
|             self.pack_end(w, True, True, 0)
 | |
|             w.hide()
 | |
|         self.unsupp_value = None
 | |
|         self.get_value = lambda: None
 | |
|         self.set_value = lambda value: None
 | |
| 
 | |
|     def _changed(self, widget, *args):
 | |
|         if widget.get_visible():
 | |
|             value = self.get_value()
 | |
|             if widget == self.choice_widget:
 | |
|                 value = _from_named_ints(value, widget._allowed_values)
 | |
|                 icon = 'dialog-warning' if widget._allowed_values and (value not in widget._allowed_values) else ''
 | |
|                 widget.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
 | |
|             self.on_change(value)
 | |
| 
 | |
|     def _hide_all(self):
 | |
|         for w in self.get_children():
 | |
|             w.hide()
 | |
| 
 | |
|     def make_toggle(self):
 | |
|         self._hide_all()
 | |
| 
 | |
|         def g():
 | |
|             value = self.toggle_widget.get_active_id()
 | |
|             return True if value == 't' else False if value == 'f' else value
 | |
| 
 | |
|         self.get_value = g
 | |
| 
 | |
|         def s(value):
 | |
|             if value in ('true', 'yes', 'on', 't', 'y'):
 | |
|                 self.toggle_widget.set_active_id('t')
 | |
|             elif value in ('false', 'no', 'off', 'f', 'n'):
 | |
|                 self.toggle_widget.set_active_id('f')
 | |
|             elif value in ('~', 'toggle'):
 | |
|                 self.toggle_widget.set_active_id('~')
 | |
|             else:
 | |
|                 self.toggle_widget.set_active_id(None)
 | |
| 
 | |
|         self.set_value = lambda value: s(str(value).lower())
 | |
|         self.toggle_widget.show()
 | |
| 
 | |
|     def make_range(self, minimum, maximum):
 | |
|         self._hide_all()
 | |
|         self.range_widget.set_range(minimum, maximum)
 | |
|         self.get_value = lambda: int(self.range_widget.get_value())
 | |
| 
 | |
|         def s(value):
 | |
|             try:
 | |
|                 v = round(float(value))
 | |
|             except (ValueError, TypeError):
 | |
|                 v = minimum
 | |
|             self.range_widget.set_value(max(minimum, min(maximum, v)))
 | |
| 
 | |
|         self.set_value = s
 | |
|         self.range_widget.show()
 | |
| 
 | |
|     def make_choice(self, values):
 | |
|         self._hide_all()
 | |
|         self.choice_widget.remove_all()
 | |
|         for v in sorted(values, key=str):
 | |
|             self.choice_widget.append(str(int(v)), str(v))
 | |
|         CompletionEntry.add_completion_to_entry(self.choice_widget.get_child(), map(str, values))
 | |
|         self.choice_widget._allowed_values = values
 | |
| 
 | |
|         def g():
 | |
|             value = self.choice_widget.get_active_id() or self.choice_widget.get_active_text().strip() or ''
 | |
|             return _from_named_ints(value, self.choice_widget._allowed_values)
 | |
| 
 | |
|         def s(value):
 | |
|             value = _from_named_ints(value, self.choice_widget._allowed_values)
 | |
|             if value in self.choice_widget._allowed_values:
 | |
|                 self.choice_widget.set_active_id(str(int(value)))
 | |
|             else:
 | |
|                 self.choice_widget.get_child().set_text(str(value))
 | |
| 
 | |
|         self.get_value = g
 | |
|         self.set_value = s
 | |
|         self.choice_widget.show()
 | |
| 
 | |
|     def make_unsupported(self):
 | |
|         self._hide_all()
 | |
|         self.get_value = lambda: self.unsupp_value
 | |
| 
 | |
|         def s(value):  # preserve unsupported values
 | |
|             self.unsupp_value = value
 | |
| 
 | |
|         self.set_value = s
 | |
|         self.unsupported_label.show()
 | |
| 
 | |
| 
 | |
| def _all_settings():
 | |
|     settings = {}
 | |
|     for s in sorted(_SETTINGS, key=lambda setting: setting.label):
 | |
|         if s.validator_class.kind == _SKIND.multiple_range:  # not supported yet
 | |
|             continue
 | |
|         if s.name not in settings:
 | |
|             settings[s.name] = [s]
 | |
|         else:
 | |
|             prev_setting = settings[s.name][0]
 | |
|             prev_kind = prev_setting.validator_class.kind
 | |
|             if prev_kind != s.validator_class.kind:
 | |
|                 _log.warning(
 | |
|                     'ignoring setting {} - same name of {}, but different kind ({} != {})'.format(
 | |
|                         s.__name__, prev_setting.__name__, prev_kind, s.validator_class.kind
 | |
|                     )
 | |
|                 )
 | |
|                 continue
 | |
|             settings[s.name].append(s)
 | |
|     return settings
 | |
| 
 | |
| 
 | |
| class SetUI(ActionUI):
 | |
| 
 | |
|     CLASS = _DIV.Set
 | |
| 
 | |
|     ALL_SETTINGS = _all_settings()
 | |
| 
 | |
|     MULTIPLE = [_SKIND.multiple_toggle, _SKIND.map_choice, _SKIND.multiple_range]
 | |
| 
 | |
|     def create_widgets(self):
 | |
|         self.widgets = {}
 | |
| 
 | |
|         self._old_device_values = {}
 | |
| 
 | |
|         lbl = Gtk.Label(_('Same device'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.widgets[lbl] = (0, 0, 1, 1)
 | |
|         self.same_device_chk = Gtk.Switch()
 | |
|         self.same_device_chk.connect('state-set', self._changed_same_device)
 | |
|         self.widgets[self.same_device_chk] = (0, 1, 1, 1)
 | |
| 
 | |
|         lbl = Gtk.Label(_('Serial or Unit ID'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.widgets[lbl] = (1, 0, 1, 1)
 | |
|         self.device_field = Gtk.ComboBoxText.new_with_entry()
 | |
|         self.device_field.get_child().set_text('')
 | |
|         self.device_field.set_valign(Gtk.Align.START)
 | |
|         self.device_field.connect('changed', self._on_update)
 | |
|         self.widgets[self.device_field] = (1, 1, 1, 1)
 | |
| 
 | |
|         lbl = Gtk.Label(_('Setting'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.widgets[lbl] = (2, 0, 1, 1)
 | |
|         self.setting_field = Gtk.ComboBoxText()
 | |
|         self.setting_field.append('', '')
 | |
|         for setting in self.ALL_SETTINGS.values():
 | |
|             self.setting_field.append(setting[0].name, setting[0].label)
 | |
|         self.setting_field.set_valign(Gtk.Align.START)
 | |
|         self.setting_field.connect('changed', self._on_update)
 | |
|         self.setting_field.connect('changed', self._changed_setting)
 | |
|         self.widgets[self.setting_field] = (2, 1, 1, 1)
 | |
| 
 | |
|         self.value_lbl = Gtk.Label(_('Value'), halign=Gtk.Align.CENTER, valign=Gtk.Align.END, hexpand=True, vexpand=True)
 | |
|         self.widgets[self.value_lbl] = (4, 0, 1, 1)
 | |
|         self.value_field = SetValueControl(self._on_update)
 | |
|         self.value_field.set_size_request(250, 35)
 | |
|         self.widgets[self.value_field] = (4, 1, 1, 1)
 | |
| 
 | |
|         self.key_field = Gtk.ComboBoxText.new_with_entry()
 | |
|         self.key_field.hide()
 | |
|         self.key_field.set_valign(Gtk.Align.END)
 | |
|         self.key_field.connect('changed', self._on_update)
 | |
|         self.widgets[self.key_field] = (4, 0, 1, 1)
 | |
| 
 | |
|     @classmethod
 | |
|     def _all_choices(cls, setting):  # choice and map-choice
 | |
|         """Return a NamedInts instance with the choices for a setting.
 | |
| 
 | |
|         If the argument `setting` is a Setting instance, then the choices are taken only from it.
 | |
|         If instead it is a name, then the function returns the union of the choices for each setting with that name.
 | |
|         Only one label per number is kept.
 | |
|         """
 | |
|         if isinstance(setting, type) and issubclass(setting, _Setting):
 | |
|             choices = UnsortedNamedInts()
 | |
|             universe = getattr(setting, 'choices_universe', None)
 | |
|             if universe:
 | |
|                 choices |= universe
 | |
|             extra = getattr(setting, 'choices_extra', None)
 | |
|             if extra:
 | |
|                 choices |= extra
 | |
|             return choices
 | |
|         settings = cls.ALL_SETTINGS.get(setting, [])
 | |
|         choices = UnsortedNamedInts()
 | |
|         for s in settings:
 | |
|             choices |= cls._all_choices(s)
 | |
|         return choices
 | |
| 
 | |
|     @classmethod
 | |
|     def _setting_attributes(cls, setting_name):
 | |
|         settings = cls.ALL_SETTINGS.get(setting_name, [None])
 | |
|         setting = settings[0]  # if settings have the same name, use the first one to get the basic data
 | |
|         val_class = setting.validator_class if setting else None
 | |
|         kind = val_class.kind if val_class else None
 | |
|         if kind in cls.MULTIPLE:
 | |
|             keys = UnsortedNamedInts()
 | |
|             for s in settings:
 | |
|                 universe = getattr(s, 'choices_universe' if kind == _SKIND.multiple_toggle else 'keys_universe', None)
 | |
|                 if universe:
 | |
|                     keys |= universe
 | |
|             # only one key per number is used
 | |
|         else:
 | |
|             keys = None
 | |
|         return setting, val_class, kind, keys
 | |
| 
 | |
|     def _changed_same_device(self, *args):
 | |
|         same = self.same_device_chk.get_active()
 | |
|         if same:
 | |
|             self._old_device_values[self.component] = self.device_field.get_child().get_text()
 | |
|             self.device_field.get_child().set_text(_('[originating device]'))
 | |
|             self.device_field.set_sensitive(False)
 | |
|         else:
 | |
|             self.device_field.get_child().set_text(self._old_device_values.get(self.component, ''))
 | |
|             self.device_field.set_sensitive(True)
 | |
|             self.device_field.grab_focus()
 | |
| 
 | |
|     def _changed_setting(self, *args):
 | |
|         setting_name = self.setting_field.get_active_id() or None
 | |
|         setting, val_class, kind, keys = self._setting_attributes(setting_name)
 | |
|         if kind in (_SKIND.toggle, _SKIND.multiple_toggle):
 | |
|             self.value_field.make_toggle()
 | |
|         elif kind in (_SKIND.choice, _SKIND.map_choice):
 | |
|             all_values = self._all_choices(setting_name)
 | |
|             self.value_field.make_choice(all_values)
 | |
|         elif kind in (_SKIND.range, ):  # _SKIND.multiple_range not supported
 | |
|             self.value_field.make_range(val_class.min_value, val_class.max_value)
 | |
|         else:
 | |
|             self.value_field.make_unsupported()
 | |
|         value = self.component.args[-1]
 | |
|         self.value_field.set_value(value if value is not None else '')
 | |
|         multiple = kind in self.MULTIPLE
 | |
|         if multiple:
 | |
|             self.key_field.remove_all()
 | |
|             self.key_field.append('', '')
 | |
|             CompletionEntry.add_completion_to_entry(self.key_field.get_child(), map(str, keys))
 | |
|             for k in sorted(keys, key=str):
 | |
|                 self.key_field.append(str(int(k)), str(k))
 | |
|         self._update_visibility()
 | |
| 
 | |
|     def _update_visibility(self):
 | |
|         a = iter(self.component.args)
 | |
|         next(a, None)  # device - currently not checked
 | |
|         setting_name = next(a, '')
 | |
|         setting, val_class, kind, keys = self._setting_attributes(setting_name)
 | |
|         multiple = kind in self.MULTIPLE
 | |
|         self.value_lbl.set_visible(not multiple)
 | |
|         self.key_field.set_visible(multiple)
 | |
|         if multiple:
 | |
|             key = _from_named_ints(next(a, ''), keys)
 | |
|             icon = 'dialog-warning' if keys and (key not in keys) else ''
 | |
|             self.key_field.get_child().set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
 | |
| 
 | |
|     def _on_update(self, *_args):
 | |
|         if self._ignore_changes:
 | |
|             return
 | |
|         super()._on_update(*_args)
 | |
|         self._update_visibility()
 | |
| 
 | |
|     def show(self, component, editable):
 | |
|         super().show(component, editable)
 | |
|         a = iter(component.args)
 | |
|         with self.ignore_changes():
 | |
|             device = next(a, None)
 | |
|             same = device is None
 | |
|             self._old_device_values[self.component] = device or ''
 | |
|             self.device_field.get_child().set_text(device or '')
 | |
|             if self.same_device_chk.get_active() != same:
 | |
|                 self.same_device_chk.set_active(same)
 | |
|             else:
 | |
|                 self._changed_same_device()
 | |
|             setting_name = next(a, '')
 | |
|             setting, _v, kind, keys = self._setting_attributes(setting_name)
 | |
|             self.setting_field.set_active_id(setting.name if setting else '')
 | |
|             self._changed_setting()
 | |
|             if kind in self.MULTIPLE or kind is None and len(self.component.args) > 3:
 | |
|                 key = _from_named_ints(next(a, ''), keys)
 | |
|                 if isinstance(key, NamedInt):
 | |
|                     self.key_field.set_active_id(str(int(key)))
 | |
|                 else:
 | |
|                     self.key_field.get_child().set_text(key)
 | |
|             self.value_field.set_value(next(a, ''))
 | |
|         self._update_visibility()
 | |
| 
 | |
|     def collect_value(self):
 | |
|         same = self.same_device_chk.get_active()
 | |
|         device = None if same else self.device_field.get_active_id() or self.device_field.get_active_text().strip()
 | |
|         setting_name = self.setting_field.get_active_id() or None
 | |
|         setting, val_class, kind, keys = self._setting_attributes(setting_name)
 | |
|         key_value = []
 | |
|         if kind in self.MULTIPLE or kind is None and len(self.component.args) > 3:
 | |
|             key = self.key_field.get_active_id() or self.key_field.get_active_text().strip() or ''
 | |
|             key = _from_named_ints(key, keys)
 | |
|             key_value.append(keys[key] if keys else key)
 | |
|         key_value.append(self.value_field.get_value())
 | |
|         return [device, setting_name, *key_value]
 | |
| 
 | |
|     @classmethod
 | |
|     def right_label(cls, component):
 | |
|         a = iter(component.args)
 | |
|         device = next(a, None)
 | |
|         disp = [_('[originating device]') if device is None else device]
 | |
|         setting_name = next(a, None)
 | |
|         setting, val_class, kind, keys = cls._setting_attributes(setting_name)
 | |
|         disp.append(setting.label if setting else setting_name)
 | |
|         if kind in cls.MULTIPLE:
 | |
|             key = next(a, None)
 | |
|             disp.append(_from_named_ints(key, keys) if keys else key)
 | |
|         value = next(a, None)
 | |
|         if setting and (kind in (_SKIND.choice, _SKIND.map_choice)):
 | |
|             all_values = cls._all_choices(setting_name)
 | |
|             if all_values:
 | |
|                 value = all_values[value]
 | |
|         disp.append(value)
 | |
|         return '  '.join(map(lambda s: shlex_quote(str(s)), [*disp, *a]))
 | |
| 
 | |
| 
 | |
| COMPONENT_UI = {
 | |
|     _DIV.Rule: RuleUI,
 | |
|     _DIV.Not: NotUI,
 | |
|     _DIV.Or: OrUI,
 | |
|     _DIV.And: AndUI,
 | |
|     _DIV.Process: ProcessUI,
 | |
|     _DIV.MouseProcess: MouseProcessUI,
 | |
|     _DIV.Feature: FeatureUI,
 | |
|     _DIV.Report: ReportUI,
 | |
|     _DIV.Modifiers: ModifiersUI,
 | |
|     _DIV.Key: KeyUI,
 | |
|     _DIV.Test: TestUI,
 | |
|     _DIV.MouseGesture: MouseGestureUI,
 | |
|     _DIV.KeyPress: KeyPressUI,
 | |
|     _DIV.MouseScroll: MouseScrollUI,
 | |
|     _DIV.MouseClick: MouseClickUI,
 | |
|     _DIV.Execute: ExecuteUI,
 | |
|     _DIV.Set: SetUI,
 | |
|     type(None): RuleComponentUI,  # placeholders for empty rule/And/Or
 | |
| }
 | |
| 
 | |
| 
 | |
| def show_window(trigger=None):
 | |
|     GObject.type_register(RuleComponentWrapper)
 | |
|     global _diversion_dialog
 | |
|     if _diversion_dialog is None:
 | |
|         _diversion_dialog = DiversionDialog()
 | |
|     _diversion_dialog.window.present()
 |