From fc2e770b002e99050388cca0009f9d419bee9ed9 Mon Sep 17 00:00:00 2001 From: Rick Calixte <10281587+rcalixte@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:28:49 -0500 Subject: [PATCH] Add support for a second keybind per action Closes #371 --- terminatorlib/config.py | 314 +++++++++--------- terminatorlib/keybindings.py | 29 +- terminatorlib/plugin.py | 67 ++-- terminatorlib/preferences.glade | 31 +- terminatorlib/prefseditor.py | 465 ++++++++++++++++----------- terminatorlib/terminal_popup_menu.py | 24 +- 6 files changed, 529 insertions(+), 401 deletions(-) diff --git a/terminatorlib/config.py b/terminatorlib/config.py index 4fbb907d3..87e5169bd 100644 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -66,7 +66,7 @@ >>> config.options_set({}) >>> config.options_get() {} ->>> +>>> """ @@ -123,89 +123,89 @@ 'new_tab_after_current_tab': False, }, 'keybindings': { - 'zoom_in' : 'plus', - 'zoom_out' : 'minus', - 'zoom_normal' : '0', - 'zoom_in_all' : '', - 'zoom_out_all' : '', - 'zoom_normal_all' : '', - 'new_tab' : 't', - 'cycle_next' : 'Tab', - 'cycle_prev' : 'Tab', - 'go_next' : 'n', - 'go_prev' : 'p', - 'go_up' : 'Up', - 'go_down' : 'Down', - 'go_left' : 'Left', - 'go_right' : 'Right', - 'rotate_cw' : 'r', - 'rotate_ccw' : 'r', - 'split_auto' : 'a', - 'split_horiz' : 'o', - 'split_vert' : 'e', - 'close_term' : 'w', - 'copy' : 'c', - 'paste' : 'v', - 'paste_selection' : '', - 'toggle_scrollbar' : 's', - 'search' : 'f', - 'page_up' : '', - 'page_down' : '', - 'page_up_half' : '', - 'page_down_half' : '', - 'line_up' : '', - 'line_down' : '', - 'close_window' : 'q', - 'resize_up' : 'Up', - 'resize_down' : 'Down', - 'resize_left' : 'Left', - 'resize_right' : 'Right', - 'move_tab_right' : 'Page_Down', - 'move_tab_left' : 'Page_Up', - 'toggle_zoom' : 'x', - 'scaled_zoom' : 'z', - 'next_tab' : 'Page_Down', - 'prev_tab' : 'Page_Up', - 'switch_to_tab_1' : '', - 'switch_to_tab_2' : '', - 'switch_to_tab_3' : '', - 'switch_to_tab_4' : '', - 'switch_to_tab_5' : '', - 'switch_to_tab_6' : '', - 'switch_to_tab_7' : '', - 'switch_to_tab_8' : '', - 'switch_to_tab_9' : '', - 'switch_to_tab_10' : '', - 'full_screen' : 'F11', - 'reset' : 'r', - 'reset_clear' : 'g', - 'hide_window' : 'a', - 'create_group' : '', - 'group_all' : 'g', - 'group_all_toggle' : '', - 'ungroup_all' : 'g', - 'group_win' : '', - 'group_win_toggle' : '', - 'ungroup_win' : 'w', - 'group_tab' : 't', - 'group_tab_toggle' : '', - 'ungroup_tab' : 't', - 'new_window' : 'i', - 'new_terminator' : 'i', - 'broadcast_off' : '', - 'broadcast_group' : '', - 'broadcast_all' : '', - 'insert_number' : '1', - 'insert_padded' : '0', - 'edit_window_title': 'w', - 'edit_tab_title' : 'a', - 'edit_terminal_title': 'x', - 'layout_launcher' : 'l', - 'next_profile' : '', - 'previous_profile' : '', - 'preferences' : '', - 'preferences_keybindings' : 'k', - 'help' : 'F1' + 'zoom_in': ['plus', ''], + 'zoom_out': ['minus', ''], + 'zoom_normal': ['0', ''], + 'zoom_in_all': ['', ''], + 'zoom_out_all': ['', ''], + 'zoom_normal_all': ['', ''], + 'new_tab': ['t', ''], + 'cycle_next': ['Tab', ''], + 'cycle_prev': ['Tab', ''], + 'go_next': ['n', ''], + 'go_prev': ['p', ''], + 'go_up': ['Up', ''], + 'go_down': ['Down', ''], + 'go_left': ['Left', ''], + 'go_right': ['Right', ''], + 'rotate_cw': ['r', ''], + 'rotate_ccw': ['r', ''], + 'split_auto': ['a', ''], + 'split_horiz': ['o', ''], + 'split_vert': ['e', ''], + 'close_term': ['w', ''], + 'copy': ['c', ''], + 'paste': ['v', ''], + 'paste_selection': ['', ''], + 'toggle_scrollbar': ['s', ''], + 'search': ['f', ''], + 'page_up': ['', ''], + 'page_down': ['', ''], + 'page_up_half': ['', ''], + 'page_down_half': ['', ''], + 'line_up': ['', ''], + 'line_down': ['', ''], + 'close_window': ['q', ''], + 'resize_up': ['Up', ''], + 'resize_down': ['Down', ''], + 'resize_left': ['Left', ''], + 'resize_right': ['Right', ''], + 'move_tab_right': ['Page_Down', ''], + 'move_tab_left': ['Page_Up', ''], + 'toggle_zoom': ['x', ''], + 'scaled_zoom': ['z', ''], + 'next_tab': ['Page_Down', ''], + 'prev_tab': ['Page_Up', ''], + 'switch_to_tab_1': ['', ''], + 'switch_to_tab_2': ['', ''], + 'switch_to_tab_3': ['', ''], + 'switch_to_tab_4': ['', ''], + 'switch_to_tab_5': ['', ''], + 'switch_to_tab_6': ['', ''], + 'switch_to_tab_7': ['', ''], + 'switch_to_tab_8': ['', ''], + 'switch_to_tab_9': ['', ''], + 'switch_to_tab_10': ['', ''], + 'full_screen': ['F11', ''], + 'reset': ['r', ''], + 'reset_clear': ['g', ''], + 'hide_window': ['a', ''], + 'create_group': ['', ''], + 'group_all': ['g', ''], + 'group_all_toggle': ['', ''], + 'ungroup_all': ['g', ''], + 'group_win': ['', ''], + 'group_win_toggle': ['', ''], + 'ungroup_win': ['w', ''], + 'group_tab': ['t', ''], + 'group_tab_toggle': ['', ''], + 'ungroup_tab': ['t', ''], + 'new_window': ['i', ''], + 'new_terminator': ['i', ''], + 'broadcast_off': ['', ''], + 'broadcast_group': ['', ''], + 'broadcast_all': ['', ''], + 'insert_number': ['1', ''], + 'insert_padded': ['0', ''], + 'edit_window_title': ['w', ''], + 'edit_tab_title': ['a', ''], + 'edit_terminal_title': ['x', ''], + 'layout_launcher': ['l', ''], + 'next_profile': ['', ''], + 'previous_profile': ['', ''], + 'preferences': ['', ''], + 'preferences_keybindings': ['k', ''], + 'help': ['F1', ''] }, 'profiles': { 'default': { @@ -286,6 +286,7 @@ }, } + class Config(object): """Class to provide a slightly richer config API above ConfigBase""" base = None @@ -294,7 +295,7 @@ class Config(object): system_prop_font = None system_focus = None inhibited = None - + def __init__(self, profile='default'): self.base = ConfigBase() self.set_profile(profile) @@ -303,19 +304,19 @@ def __init__(self, profile='default'): def __getitem__(self, key, default=None): """Look up a configuration item""" - return(self.base.get_item(key, self.profile, default=default)) + return self.base.get_item(key, self.profile, default=default) def __setitem__(self, key, value): """Set a particular configuration item""" - return(self.base.set_item(key, value, self.profile)) + return self.base.set_item(key, value, self.profile) def get_profile(self): """Get our profile""" - return(self.profile) + return self.profile def get_profile_by_name(self, profile): """Get the profile with the specified name""" - return(self.base.profiles[profile]) + return self.base.profiles[profile] def set_profile(self, profile, force=False): """Set our profile (which usually means change it)""" @@ -331,7 +332,7 @@ def set_profile(self, profile, force=False): def add_profile(self, profile, toclone): """Add a new profile""" - return(self.base.add_profile(profile, toclone)) + return self.base.add_profile(profile, toclone) def del_profile(self, profile): """Delete a profile""" @@ -341,7 +342,7 @@ def del_profile(self, profile): err('Config::del_profile: Deleting in-use profile %s.' % profile) self.set_profile('default') if profile in self.base.profiles: - del(self.base.profiles[profile]) + del self.base.profiles[profile] options = self.options_get() if options and options.profile == profile: options.profile = None @@ -351,89 +352,89 @@ def rename_profile(self, profile, newname): """Rename a profile""" if profile in self.base.profiles: self.base.profiles[newname] = self.base.profiles[profile] - del(self.base.profiles[profile]) + del self.base.profiles[profile] if profile == self.profile: self.profile = newname def list_profiles(self): """List all configured profiles""" - return(list(self.base.profiles.keys())) + return list(self.base.profiles.keys()) def add_layout(self, name, layout): """Add a new layout""" - return(self.base.add_layout(name, layout)) + return self.base.add_layout(name, layout) def replace_layout(self, name, layout): """Replace an existing layout""" - return(self.base.replace_layout(name, layout)) + return self.base.replace_layout(name, layout) def del_layout(self, layout): """Delete a layout""" if layout in self.base.layouts: - del(self.base.layouts[layout]) + del self.base.layouts[layout] def rename_layout(self, layout, newname): """Rename a layout""" if layout in self.base.layouts: self.base.layouts[newname] = self.base.layouts[layout] - del(self.base.layouts[layout]) + del self.base.layouts[layout] def list_layouts(self): """List all configured layouts""" - return(list(self.base.layouts.keys())) + return list(self.base.layouts.keys()) def connect_gsetting_callbacks(self): """Get system settings and create callbacks for changes""" dbg("GSetting connects for system changes") # Have to preserve these to self, or callbacks don't happen - self.gsettings_interface=Gio.Settings.new('org.gnome.desktop.interface') + self.gsettings_interface = Gio.Settings.new('org.gnome.desktop.interface') self.gsettings_interface.connect("changed::font-name", self.on_gsettings_change_event) self.gsettings_interface.connect("changed::monospace-font-name", self.on_gsettings_change_event) - self.gsettings_wm=Gio.Settings.new('org.gnome.desktop.wm.preferences') + self.gsettings_wm = Gio.Settings.new('org.gnome.desktop.wm.preferences') self.gsettings_wm.connect("changed::focus-mode", self.on_gsettings_change_event) def get_system_prop_font(self): """Look up the system font""" if self.system_prop_font is not None: - return(self.system_prop_font) + return self.system_prop_font elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): return else: - gsettings=Gio.Settings.new('org.gnome.desktop.interface') + gsettings = Gio.Settings.new('org.gnome.desktop.interface') value = gsettings.get_value('font-name') if value: self.system_prop_font = value.get_string() else: self.system_prop_font = "Sans 10" - return(self.system_prop_font) + return self.system_prop_font def get_system_mono_font(self): """Look up the system font""" if self.system_mono_font is not None: - return(self.system_mono_font) + return self.system_mono_font elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): return else: - gsettings=Gio.Settings.new('org.gnome.desktop.interface') + gsettings = Gio.Settings.new('org.gnome.desktop.interface') value = gsettings.get_value('monospace-font-name') if value: self.system_mono_font = value.get_string() else: self.system_mono_font = "Mono 10" - return(self.system_mono_font) + return self.system_mono_font def get_system_focus(self): """Look up the system focus setting""" if self.system_focus is not None: - return(self.system_focus) + return self.system_focus elif 'org.gnome.desktop.interface' not in Gio.Settings.list_schemas(): return else: - gsettings=Gio.Settings.new('org.gnome.desktop.wm.preferences') + gsettings = Gio.Settings.new('org.gnome.desktop.wm.preferences') value = gsettings.get_value('focus-mode') if value: self.system_focus = value.get_string() - return(self.system_focus) + return self.system_focus def on_gsettings_change_event(self, settings, key): """Handle a gsetting change event""" @@ -449,9 +450,9 @@ def on_gsettings_change_event(self, settings, key): def save(self): """Cause ConfigBase to save our config to file""" if self.inhibited is True: - return(True) + return True else: - return(self.base.save()) + return self.base.save() def inhibit_save(self): """Prevent calls to save() being honoured""" @@ -467,37 +468,38 @@ def options_set(self, options): def options_get(self): """Get the command line options""" - return(self.base.command_line_options) + return self.base.command_line_options def plugin_get(self, pluginname, key, default=None): """Get a plugin config value, if doesn't exist return default if specified """ - return(self.base.get_item(key, plugin=pluginname, default=default)) + return self.base.get_item(key, plugin=pluginname, default=default) def plugin_set(self, pluginname, key, value): """Set a plugin config value""" - return(self.base.set_item(key, value, plugin=pluginname)) + return self.base.set_item(key, value, plugin=pluginname) def plugin_get_config(self, plugin): """Return a whole config tree for a given plugin""" - return(self.base.get_plugin(plugin)) + return self.base.get_plugin(plugin) def plugin_set_config(self, plugin, tree): """Set a whole config tree for a given plugin""" - return(self.base.set_plugin(plugin, tree)) + return self.base.set_plugin(plugin, tree) def plugin_del_config(self, plugin): """Delete a whole config tree for a given plugin""" - return(self.base.del_plugin(plugin)) + return self.base.del_plugin(plugin) def layout_get_config(self, layout): """Return a layout""" - return(self.base.get_layout(layout)) + return self.base.get_layout(layout) def layout_set_config(self, layout, tree): """Set a layout""" - return(self.base.set_layout(layout, tree)) + return self.base.set_layout(layout, tree) + class ConfigBase(Borg): """Class to provide access to our user configuration""" @@ -574,11 +576,20 @@ def defaults_to_configspec(self): configspecdata['global_config'] = section section = {} - for key in DEFAULTS['keybindings']: - value = DEFAULTS['keybindings'][key] - if value is None or value == '': - continue - section[key] = 'string(default=%s)' % value + # print(f"{DEFAULTS['keybindings']=}") # DEBUG + for key, val in DEFAULTS['keybindings'].items(): + if isinstance(val, str): + value = list(val, '') + if value is None or value == '': + continue + elif isinstance(val, list): + if len(val) < 2: + val.append('') + value = val + # DEBUG if value is None or value == '': + # DEBUG continue + section[key] = f'list(default=list{tuple(value)})' + # print(f"{section[key]=}") # DEBUG configspecdata['keybindings'] = section section = {} @@ -612,9 +623,9 @@ def defaults_to_configspec(self): configspecdata['plugins'] = {} configspec = ConfigObj(configspecdata) - if DEBUG == True: + if DEBUG: configspec.write(open('/tmp/terminator_configspec_debug.txt', 'wb')) - return(configspec) + return configspec def load(self): """Load configuration data from our various sources""" @@ -649,16 +660,19 @@ def load(self): try: configspec = self.defaults_to_configspec() + # print(f'{configspec =}') # DEBUG parser = ConfigObj(configfile, configspec=configspec) validator = Validator() result = parser.validate(validator, preserve_errors=True) + # print(f'{result=}') # DEBUG except Exception as ex: err('Unable to load configuration: %s' % ex) return - if result != True: + if result is not True: err('ConfigBase::load: config format is not valid') for (section_list, key, _other) in flatten_errors(parser, result): + print(f'{section_list=} {key=}') # DEBUG if key is not None: err('[%s]: %s is invalid' % (','.join(section_list), key)) else: @@ -687,7 +701,7 @@ def load(self): dbg('Processing %s: %s' % (section_name, layout)) if layout == 'default' and \ parser[section_name][layout] == {}: - continue + continue section[layout] = parser[section_name][layout] elif section_name == 'keybindings': if section_name not in parser: @@ -701,7 +715,7 @@ def load(self): else: try: section.update(parser[section_name]) - except KeyError as ex: + except KeyError: dbg('skipping missing section %s' % section_name) self.loaded = True @@ -723,11 +737,11 @@ def save(self): if section_name == 'keybindings': from terminatorlib.plugin import KeyBindUtil # for plugin KeyBindUtil assist in plugin_util - keybindutil = KeyBindUtil(); - keyb_keys = keybindutil.get_all_act_to_keys() + keybindutil = KeyBindUtil() + keyb_keys = keybindutil.get_all_act_to_keys() # we only need keys as a reference so to match them # against new values - keyb_keys = dict.fromkeys(keyb_keys, "") + keyb_keys = dict.fromkeys(keyb_keys, "") default_merged_section = {**keyb_keys, **DEFAULTS[section_name]} merged_section = {**keyb_keys, **section} @@ -764,8 +778,8 @@ def save(self): try: if self.command_line_options.config: filename = self.command_line_options.config - else: - filename = os.path.join(config_dir,'config') + else: + filename = os.path.join(config_dir, 'config') if not os.path.isfile(filename): open(filename, 'a').close() @@ -788,18 +802,18 @@ def get_item(self, key, profile='default', plugin=None, default=None): if key in self.global_config: dbg('%s found in globals: %s' % - (key, self.global_config[key])) - return(self.global_config[key]) + (key, self.global_config[key])) + return self.global_config[key] elif key in self.profiles[profile]: dbg('%s found in profile %s: %s' % ( key, profile, self.profiles[profile][key])) - return(self.profiles[profile][key]) + return self.profiles[profile][key] elif key == 'keybindings': - return(self.keybindings) + return self.keybindings elif plugin and plugin in self.plugins and key in self.plugins[plugin]: dbg('%s found in plugin %s: %s' % ( key, plugin, self.plugins[plugin][key])) - return(self.plugins[plugin][key]) + return self.plugins[plugin][key] elif default: return default else: @@ -808,7 +822,7 @@ def get_item(self, key, profile='default', plugin=None, default=None): def set_item(self, key, value, profile='default', plugin=None): """Set a configuration item""" dbg('Setting %s=%s (profile=%s, plugin=%s)' % - (key, value, profile, plugin)) + (key, value, profile, plugin)) if key in self.global_config: self.global_config[key] = value @@ -823,12 +837,12 @@ def set_item(self, key, value, profile='default', plugin=None): else: raise KeyError('ConfigBase::set_item: unknown key %s' % key) - return(True) + return True def get_plugin(self, plugin): """Return a whole tree for a plugin""" if plugin in self.plugins: - return(self.plugins[plugin]) + return self.plugins[plugin] def set_plugin(self, plugin, tree): """Set a whole tree for a plugin""" @@ -842,32 +856,32 @@ def del_plugin(self, plugin): def add_profile(self, profile, toclone): """Add a new profile""" if profile in self.profiles: - return(False) + return False if toclone is not None: newprofile = copy(toclone) else: newprofile = copy(DEFAULTS['profiles']['default']) self.profiles[profile] = newprofile - return(True) + return True def add_layout(self, name, layout): """Add a new layout""" if name in self.layouts: - return(False) + return False self.layouts[name] = layout - return(True) + return True def replace_layout(self, name, layout): """Replaces a layout with the given name""" - if not name in self.layouts: - return(False) + if name not in self.layouts: + return False self.layouts[name] = layout - return(True) + return True def get_layout(self, layout): """Return a layout""" if layout in self.layouts: - return(self.layouts[layout]) + return self.layouts[layout] else: err('layout does not exist: %s' % layout) diff --git a/terminatorlib/keybindings.py b/terminatorlib/keybindings.py index 61551c8eb..b8741921d 100644 --- a/terminatorlib/keybindings.py +++ b/terminatorlib/keybindings.py @@ -16,19 +16,23 @@ """Terminator by Chris Jones -Validator and functions for dealing with Terminator's customisable +Validator and functions for dealing with Terminator's customisable keyboard shortcuts. """ import re -from gi.repository import Gtk, Gdk +from gi.repository import Gdk from .util import err + class KeymapError(Exception): """Custom exception for errors in keybinding configurations""" + MODIFIER = re.compile('<([^<]+)>') + + class Keybindings: """Class to handle loading and lookup of Terminator keybindings""" @@ -61,20 +65,22 @@ def reload(self): """Parse bindings and mangle into an appropriate form""" self._lookup = {} self._masks = 0 + # print(f'{self.keys=}') # DEBUG for action, bindings in list(self.keys.items()): - if not isinstance(bindings, tuple): - bindings = (bindings,) + if not isinstance(bindings, list): + bindings = [bindings, ''] for binding in bindings: if not binding or binding == "None": continue try: + # print(f'{binding=}') # DEBUG keyval, mask = self._parsebinding(binding) # Does much the same, but with poorer error handling. - #keyval, mask = Gtk.accelerator_parse(binding) + # keyval, mask = Gtk.accelerator_parse(binding) except KeymapError as e: - err ("keybindings.reload failed to parse binding '%s': %s" % (binding, e)) + err("keybindings.reload failed to parse binding '%s': %s" % (binding, e)) else: if mask & Gdk.ModifierType.SHIFT_MASK: if keyval == Gdk.KEY_Tab: @@ -93,6 +99,7 @@ def reload(self): def _parsebinding(self, binding): """Parse an individual binding using gtk's binding function""" + # print(f'{binding=}') # DEBUG mask = 0 modifiers = re.findall(MODIFIER, binding) if modifiers: @@ -116,14 +123,14 @@ def _lookup_modifier(self, modifier): def lookup(self, event): """Translate a keyboard event into a mapped key""" try: - _found, keyval, _egp, _lvl, consumed = self.keymap.translate_keyboard_state( - event.hardware_keycode, + _, keyval, _, _, consumed = self.keymap.translate_keyboard_state( + event.hardware_keycode, Gdk.ModifierType(event.get_state() & ~Gdk.ModifierType.LOCK_MASK), event.group) except TypeError: - err ("keybindings.lookup failed to translate keyboard event: %s" % - dir(event)) + err("keybindings.lookup failed to translate keyboard event: %s" % + dir(event)) return None mask = (event.get_state() & ~consumed) & self._masks + print(f'{mask=} {keyval=} {self.empty=}') return self._lookup.get(mask, self.empty).get(keyval, None) - diff --git a/terminatorlib/plugin.py b/terminatorlib/plugin.py index 49a972e78..7d7710ef2 100644 --- a/terminatorlib/plugin.py +++ b/terminatorlib/plugin.py @@ -30,6 +30,7 @@ from .util import dbg, err, get_config_dir from .terminator import Terminator + class Plugin(object): """Definition of our base plugin class""" capabilities = None @@ -42,6 +43,7 @@ def unload(self): """Prepare to be unloaded""" pass + class PluginRegistry(borg.Borg): """Definition of a class to store plugin instances""" available_plugins = None @@ -105,10 +107,10 @@ def load_plugins(self, force=False): if item not in self.instances: self.instances[item] = func() elif force: - #instead of multiple copies of loaded - #plugin objects, unload where plugins - #can clean up and then re-init so there - #is one plugin object + # instead of multiple copies of loaded + # plugin objects, unload where plugins + # can clean up and then re-init so there + # is one plugin object self.instances[item].unload() self.instances.pop(item, None) self.instances[item] = func() @@ -130,17 +132,17 @@ def get_plugins_by_capability(self, capability): def get_all_plugins(self): """Return all plugins""" - return(self.instances) + return self.instances def get_available_plugins(self): """Return a list of all available plugins whether they are enabled or disabled""" - return(list(self.available_plugins.keys())) + return list(self.available_plugins.keys()) def is_enabled(self, plugin): """Return a boolean value indicating whether a plugin is enabled or not""" - return(plugin in self.instances) + return plugin in self.instances def enable(self, plugin): """Enable a plugin""" @@ -153,11 +155,12 @@ def disable(self, plugin): """Disable a plugin""" dbg("Disabling %s" % plugin) self.instances[plugin].unload() - del(self.instances[plugin]) + del self.instances[plugin] # This is where we should define a base class for each type of plugin we # support + # URLHandler - This adds a regex match to the Terminal widget and provides a # callback to turn that into a URL. class URLHandler(Plugin): @@ -188,6 +191,7 @@ def unload(self): for terminal in terminator.terminals: terminal.match_remove(self.handler_name) + # MenuItem - This is able to execute code during the construction of the # context menu of a Terminal. class MenuItem(Plugin): @@ -206,25 +210,26 @@ def callback(self, menuitems, menu, terminal): Vishweshwar Saran Singh Deo vssdeo@gmail.com """ -from gi.repository import Gtk, Gdk -from terminatorlib.keybindings import Keybindings, KeymapError +from gi.repository import Gdk +from terminatorlib.keybindings import Keybindings PLUGIN_UTIL_DESC = 0 -PLUGIN_UTIL_ACT = 1 +PLUGIN_UTIL_ACT = 1 PLUGIN_UTIL_KEYS = 2 + class KeyBindUtil: keybindings = Keybindings() - map_key_to_act = {} + map_key_to_act = {} map_act_to_keys = {} map_act_to_desc = {} def __init__(self, config=None): self.config = config - #Example + # Example # bind # first param is desc, second is action str # self.keyb.bindkey([PluginUrlFindNext , PluginUrlActFindNext, "j"]) @@ -234,8 +239,7 @@ def __init__(self, config=None): # if act == "url_find_next": - - #check map key_val_mask -> action + # check map key_val_mask -> action def _check_keybind_change(self, key): act = key[PLUGIN_UTIL_ACT] for key_val_mask in self.map_key_to_act: @@ -244,7 +248,7 @@ def _check_keybind_change(self, key): return key_val_mask return None - #check in config before binding + # check in config before binding def bindkey_check_config(self, key): if not self.config: raise Warning("bindkey_check_config called without config init") @@ -256,30 +260,30 @@ def bindkey_check_config(self, key): if len(keystr): key[PLUGIN_UTIL_KEYS] = keystr dbg("found new Action->KeyVal in config: (%s, %s)" - % (actstr, keystr)); + % (actstr, keystr)) self.bindkey(key) def bindkey(self, key): - (keyval, mask) = self.keybindings._parsebinding(key[PLUGIN_UTIL_KEYS]) + (keyval, mask) = self.keybindings._parsebinding(key[PLUGIN_UTIL_KEYS]) keyval = Gdk.keyval_to_lower(keyval) mask = Gdk.ModifierType(mask) ret = (keyval, mask) - dbg("bindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) + dbg("bindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) - #remove if any old key_val_mask + # remove if any old key_val_mask old_key_val_mask = self._check_keybind_change(key) if old_key_val_mask: dbg("found old key binding, removing: (%s)" % str(old_key_val_mask)) del self.map_key_to_act[old_key_val_mask] - #map key-val-mask to action, used to ease key-press management + # map key-val-mask to action, used to ease key-press management self.map_key_to_act[ret] = key[PLUGIN_UTIL_ACT] - - #map action to key-combo-str, used in preferences->keybinding - self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_KEYS] - #map action to key-combo description, in used preferences->keybinding + # map action to key-combo-str, used in preferences->keybinding + self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_KEYS] + print(f'{self.map_act_to_keys=}') # DEBUG + # map action to key-combo description, in used preferences->keybinding self.map_act_to_desc[key[PLUGIN_UTIL_ACT]] = key[PLUGIN_UTIL_DESC] def unbindkey(self, key): @@ -287,15 +291,15 @@ def unbindkey(self, key): # Suppose user changed the key-combo and its diff from # what the plugin had set by default, we need to get # current key-combo - act = key[PLUGIN_UTIL_ACT] + act = key[PLUGIN_UTIL_ACT] act_keys = self.map_act_to_keys[act] - (keyval, mask) = self.keybindings._parsebinding(act_keys) + (keyval, mask) = self.keybindings._parsebinding(act_keys) keyval = Gdk.keyval_to_lower(keyval) mask = Gdk.ModifierType(mask) ret = (keyval, mask) - dbg("unbindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) + dbg("unbindkey: (%s) (%s)" % (key[PLUGIN_UTIL_KEYS], str(ret))) # FIXME keys should always be there, can also use .pop(key, None) # lets do it after testing @@ -303,10 +307,9 @@ def unbindkey(self, key): del self.map_act_to_keys[key[PLUGIN_UTIL_ACT]] del self.map_act_to_desc[key[PLUGIN_UTIL_ACT]] - def keyaction(self, event): - #FIXME MOD2 mask comes in the event, remove - event.state &= ~Gdk.ModifierType.MOD2_MASK + # FIXME MOD2 mask comes in the event, remove + event.state &= ~Gdk.ModifierType.MOD2_MASK keyval = Gdk.keyval_to_lower(event.keyval) ret = (keyval, event.state) @@ -325,7 +328,7 @@ def get_all_act_to_desc(self): def get_act_to_desc(self, act): return self.map_act_to_desc.get(act) - #get action to key binding from config + # get action to key binding from config def get_act_to_keys_config(self, act): if not self.config: raise Warning("get_keyvalmask_for_act called without config init") diff --git a/terminatorlib/preferences.glade b/terminatorlib/preferences.glade index e0e8a2a5b..e27b8805d 100644 --- a/terminatorlib/preferences.glade +++ b/terminatorlib/preferences.glade @@ -214,9 +214,13 @@ - + - + + + + + @@ -4081,13 +4085,13 @@ - Keybinding + Keybinding 1 True other - - + + 2 @@ -4096,6 +4100,23 @@ + + + Keybinding 2 + + + True + other + + + + + 4 + 5 + + + + diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py index d5933c4b0..1ff0ebda4 100755 --- a/terminatorlib/prefseditor.py +++ b/terminatorlib/prefseditor.py @@ -1,5 +1,5 @@ #!/usr/bin/env python -"""Preferences Editor for Terminator. +"""Preferences Editor for Terminator. Load a UIBuilder config file, display it, populate it with our current config, then optionally read that back out and @@ -8,6 +8,7 @@ """ import os +import re from gi.repository import GObject, Gtk, Gdk from .util import dbg, err @@ -20,19 +21,24 @@ from .plugin import KeyBindUtil + def get_color_string(widcol): - return('#%02x%02x%02x' % (widcol.red>>8, widcol.green>>8, widcol.blue>>8)) + return '#%02x%02x%02x' % (widcol.red >> 8, widcol.green >> 8, widcol.blue >> 8) + def color2hex(widget): """Pull the colour values out of a Gtk ColorPicker widget and return them as 8bit hex values, sinces its default behaviour is to give 16bit values""" return get_color_string(widget.get_color()) + def rgba2hex(widget): return get_color_string(widget.get_rgba().to_color()) + NUM_PALETTE_COLORS = 16 + # FIXME: We need to check that we have represented all of Config() below class PrefsEditor: """Class implementing the various parts of the preferences editor""" @@ -103,92 +109,92 @@ class PrefsEditor: 'gruvbox_dark': '#282828:#cc241d:#98971a:#d79921:\ #458588:#b16286:#689d6a:#a89984:#928374:#fb4934:#b8bb26:#fabd2f:\ #83a598:#d3869b:#8ec07c:#ebdbb2'} - keybindingnames = { 'zoom_in' : _('Increase font size'), - 'zoom_out' : _('Decrease font size'), - 'zoom_normal' : _('Restore original font size'), - 'zoom_in_all' : _('Increase font size on all terminals'), - 'zoom_out_all' : _('Decrease font size on all terminals'), - 'zoom_normal_all' : _('Restore original font size on all terminals'), - 'new_tab' : _('Create a new tab'), - 'cycle_next' : _('Focus the next terminal'), - 'cycle_prev' : _('Focus the previous terminal'), - 'go_next' : _('Focus the next terminal'), - 'go_prev' : _('Focus the previous terminal'), - 'go_up' : _('Focus the terminal above'), - 'go_down' : _('Focus the terminal below'), - 'go_left' : _('Focus the terminal left'), - 'go_right' : _('Focus the terminal right'), - 'rotate_cw' : _('Rotate terminals clockwise'), - 'rotate_ccw' : _('Rotate terminals counter-clockwise'), - 'split_auto' : _('Split automatically'), - 'split_horiz' : _('Split horizontally'), - 'split_vert' : _('Split vertically'), - 'close_term' : _('Close terminal'), - 'copy' : _('Copy selected text'), - 'paste' : _('Paste clipboard'), - 'paste_selection' : _('Paste primary selection'), - 'toggle_scrollbar' : _('Show/Hide the scrollbar'), - 'search' : _('Search terminal scrollback'), - 'page_up' : _('Scroll upwards one page'), - 'page_down' : _('Scroll downwards one page'), - 'page_up_half' : _('Scroll upwards half a page'), - 'page_down_half' : _('Scroll downwards half a page'), - 'line_up' : _('Scroll upwards one line'), - 'line_down' : _('Scroll downwards one line'), - 'close_window' : _('Close window'), - 'resize_up' : _('Resize the terminal up'), - 'resize_down' : _('Resize the terminal down'), - 'resize_left' : _('Resize the terminal left'), - 'resize_right' : _('Resize the terminal right'), - 'move_tab_right' : _('Move the tab right'), - 'move_tab_left' : _('Move the tab left'), - 'toggle_zoom' : _('Maximize terminal'), - 'scaled_zoom' : _('Zoom terminal'), - 'next_tab' : _('Switch to the next tab'), - 'prev_tab' : _('Switch to the previous tab'), - 'switch_to_tab_1' : _('Switch to the first tab'), - 'switch_to_tab_2' : _('Switch to the second tab'), - 'switch_to_tab_3' : _('Switch to the third tab'), - 'switch_to_tab_4' : _('Switch to the fourth tab'), - 'switch_to_tab_5' : _('Switch to the fifth tab'), - 'switch_to_tab_6' : _('Switch to the sixth tab'), - 'switch_to_tab_7' : _('Switch to the seventh tab'), - 'switch_to_tab_8' : _('Switch to the eighth tab'), - 'switch_to_tab_9' : _('Switch to the ninth tab'), - 'switch_to_tab_10' : _('Switch to the tenth tab'), - 'full_screen' : _('Toggle fullscreen'), - 'reset' : _('Reset the terminal'), - 'reset_clear' : _('Reset and clear the terminal'), - 'hide_window' : _('Toggle window visibility'), - 'create_group' : _('Create new group'), - 'group_all' : _('Group all terminals'), - 'group_all_toggle' : _('Group/Ungroup all terminals'), - 'ungroup_all' : _('Ungroup all terminals'), - 'group_win' : _('Group terminals in window'), - 'group_win_toggle' : _('Group/Ungroup terminals in window'), - 'ungroup_win' : _('Ungroup terminals in window'), - 'group_tab' : _('Group terminals in tab'), - 'group_tab_toggle' : _('Group/Ungroup terminals in tab'), - 'ungroup_tab' : _('Ungroup terminals in tab'), - 'new_window' : _('Create a new window'), - 'new_terminator' : _('Spawn a new Terminator process'), - 'broadcast_off' : _('Don\'t broadcast key presses'), - 'broadcast_group' : _('Broadcast key presses to group'), - 'broadcast_all' : _('Broadcast key events to all'), - 'insert_number' : _('Insert terminal number'), - 'insert_padded' : _('Insert zero padded terminal number'), - 'edit_window_title': _('Edit window title'), - 'edit_terminal_title': _('Edit terminal title'), - 'edit_tab_title' : _('Edit tab title'), - 'layout_launcher' : _('Open layout launcher window'), - 'next_profile' : _('Switch to next profile'), - 'previous_profile' : _('Switch to previous profile'), - 'preferences' : _('Open the Preferences window'), - 'preferences_keybindings' : _('Open the Preferences-Keybindings window'), - 'help' : _('Open the manual') - } - - def __init__ (self, term, cur_page=0): + keybindingnames = {'zoom_in': _('Increase font size'), + 'zoom_out': _('Decrease font size'), + 'zoom_normal': _('Restore original font size'), + 'zoom_in_all': _('Increase font size on all terminals'), + 'zoom_out_all': _('Decrease font size on all terminals'), + 'zoom_normal_all': _('Restore original font size on all terminals'), + 'new_tab': _('Create a new tab'), + 'cycle_next': _('Focus the next terminal'), + 'cycle_prev': _('Focus the previous terminal'), + 'go_next': _('Focus the next terminal'), + 'go_prev': _('Focus the previous terminal'), + 'go_up': _('Focus the terminal above'), + 'go_down': _('Focus the terminal below'), + 'go_left': _('Focus the terminal left'), + 'go_right': _('Focus the terminal right'), + 'rotate_cw': _('Rotate terminals clockwise'), + 'rotate_ccw': _('Rotate terminals counter-clockwise'), + 'split_auto': _('Split automatically'), + 'split_horiz': _('Split horizontally'), + 'split_vert': _('Split vertically'), + 'close_term': _('Close terminal'), + 'copy': _('Copy selected text'), + 'paste': _('Paste clipboard'), + 'paste_selection': _('Paste primary selection'), + 'toggle_scrollbar': _('Show/Hide the scrollbar'), + 'search': _('Search terminal scrollback'), + 'page_up': _('Scroll upwards one page'), + 'page_down': _('Scroll downwards one page'), + 'page_up_half': _('Scroll upwards half a page'), + 'page_down_half': _('Scroll downwards half a page'), + 'line_up': _('Scroll upwards one line'), + 'line_down': _('Scroll downwards one line'), + 'close_window': _('Close window'), + 'resize_up': _('Resize the terminal up'), + 'resize_down': _('Resize the terminal down'), + 'resize_left': _('Resize the terminal left'), + 'resize_right': _('Resize the terminal right'), + 'move_tab_right': _('Move the tab right'), + 'move_tab_left': _('Move the tab left'), + 'toggle_zoom': _('Maximize terminal'), + 'scaled_zoom': _('Zoom terminal'), + 'next_tab': _('Switch to the next tab'), + 'prev_tab': _('Switch to the previous tab'), + 'switch_to_tab_1': _('Switch to the first tab'), + 'switch_to_tab_2': _('Switch to the second tab'), + 'switch_to_tab_3': _('Switch to the third tab'), + 'switch_to_tab_4': _('Switch to the fourth tab'), + 'switch_to_tab_5': _('Switch to the fifth tab'), + 'switch_to_tab_6': _('Switch to the sixth tab'), + 'switch_to_tab_7': _('Switch to the seventh tab'), + 'switch_to_tab_8': _('Switch to the eighth tab'), + 'switch_to_tab_9': _('Switch to the ninth tab'), + 'switch_to_tab_10': _('Switch to the tenth tab'), + 'full_screen': _('Toggle fullscreen'), + 'reset': _('Reset the terminal'), + 'reset_clear': _('Reset and clear the terminal'), + 'hide_window': _('Toggle window visibility'), + 'create_group': _('Create new group'), + 'group_all': _('Group all terminals'), + 'group_all_toggle': _('Group/Ungroup all terminals'), + 'ungroup_all': _('Ungroup all terminals'), + 'group_win': _('Group terminals in window'), + 'group_win_toggle': _('Group/Ungroup terminals in window'), + 'ungroup_win': _('Ungroup terminals in window'), + 'group_tab': _('Group terminals in tab'), + 'group_tab_toggle': _('Group/Ungroup terminals in tab'), + 'ungroup_tab': _('Ungroup terminals in tab'), + 'new_window': _('Create a new window'), + 'new_terminator': _('Spawn a new Terminator process'), + 'broadcast_off': _('Don\'t broadcast key presses'), + 'broadcast_group': _('Broadcast key presses to group'), + 'broadcast_all': _('Broadcast key events to all'), + 'insert_number': _('Insert terminal number'), + 'insert_padded': _('Insert zero padded terminal number'), + 'edit_window_title': _('Edit window title'), + 'edit_terminal_title': _('Edit terminal title'), + 'edit_tab_title': _('Edit tab title'), + 'layout_launcher': _('Open layout launcher window'), + 'next_profile': _('Switch to next profile'), + 'previous_profile': _('Switch to previous profile'), + 'preferences': _('Open the Preferences window'), + 'preferences_keybindings': _('Open the Preferences-Keybindings window'), + 'help': _('Open the manual') + } + + def __init__(self, term, cur_page=0): self.config = config.Config() self.config.base.reload() self.term = term @@ -226,7 +232,9 @@ def __init__ (self, term, cur_page=0): self.window.show_all() try: self.config.inhibit_save() + # print('got here8') # DEBUG self.set_values() + # print('got here7') # DEBUG except Exception as e: err('Unable to set values: %s' % e) self.config.uninhibit_save() @@ -241,14 +249,14 @@ def on_closebutton_clicked(self, _button): terminator.reconfigure() self.window.destroy() self.calling_window.preventHide = False - del(self) + del self def set_values(self): """Update the preferences window with all the configuration from Config()""" guiget = self.builder.get_object - ## Global tab + # GLOBAL TAB # Mouse focus focus = self.config['focus'] active = 0 @@ -267,7 +275,7 @@ def set_values(self): # Cell Height cellheightsize = self.config['cell_height'] - cellheightsize = round(float(cellheightsize),1) + cellheightsize = round(float(cellheightsize), 1) widget = guiget('cellheight') widget.set_value(cellheightsize) widget = guiget('cellheight_value_label') @@ -275,7 +283,7 @@ def set_values(self): # Cell Width cellwidthsize = self.config['cell_width'] - cellwidthsize = round(float(cellwidthsize),1) + cellwidthsize = round(float(cellwidthsize), 1) widget = guiget('cellwidth') widget.set_value(cellwidthsize) widget = guiget('cellwidth_value_label') @@ -342,16 +350,16 @@ def set_values(self): # Detachable tabs widget = guiget('detachable_tabs') widget.set_active(self.config['detachable_tabs']) - #Hide from taskbar + # Hide from taskbar widget = guiget('hidefromtaskbcheck') widget.set_active(self.config['hide_from_taskbar']) - #Always on top + # Always on top widget = guiget('alwaysontopcheck') widget.set_active(self.config['always_on_top']) - #Hide on lose focus + # Hide on lose focus widget = guiget('hideonlosefocuscheck') widget.set_active(self.config['hide_on_lose_focus']) - #Show on all workspaces + # Show on all workspaces widget = guiget('stickycheck') widget.set_active(self.config['sticky']) @@ -363,7 +371,7 @@ def set_values(self): widget = guiget('new_tab_after_current_checkbutton') widget.set_active(self.config['new_tab_after_current_tab']) - #Always split with profile + # Always split with profile widget = guiget('always_split_with_profile') widget.set_active(self.config['always_split_with_profile']) # Putty paste style @@ -385,7 +393,7 @@ def set_values(self): widget = guiget('disable_mouse_paste') widget.set_active(self.config['disable_mouse_paste']) - ## Profile tab + # PROFILE TAB # Populate the profile list widget = guiget('profilelist') liststore = widget.get_model() @@ -403,7 +411,7 @@ def set_values(self): selection.connect('changed', self.on_profile_selection_changed) selection.select_iter(self.profileiters['default']) - ## Layouts tab + # LAYOUTS TAB widget = guiget('layoutlist') liststore = widget.get_model() layouts = self.config.list_layouts() @@ -429,28 +437,39 @@ def set_values(self): selection = widget.get_selection() selection.connect('changed', self.on_layout_item_selection_changed) - ## Keybindings tab - widget = guiget('keybindingtreeview') + # KEYBINDINGS TAB + widget = guiget('keybindingtreeview') kbsearch = guiget('keybindingsearchentry') self.keybind_filter_str = "" - #lets hide whatever we can in nested scope + # let's hide whatever we can in nested scope def filter_visible(model, treeiter, data): - act = model[treeiter][0] + # print(f'{treeiter=} {data=}') # DEBUG + act = model[treeiter][0] keys = data[act] if act in data else "" desc = model[treeiter][1] - kval = model[treeiter][2] - mask = model[treeiter][3] - #so user can search for disabled keys also - if not (len(keys) and kval and mask): + kval0 = model[treeiter][2] + mask0 = model[treeiter][3] + kval1 = model[treeiter][4] + mask1 = model[treeiter][5] + # print(f'{keys=}') # DEBUG + # print(f'{model[treeiter][0]=} {model[treeiter][1]=}') # DEBUG + # print(f'{model[treeiter][2]=} {model[treeiter][3]=}') # DEBUG + # print(f'{model[treeiter][4]=} {model[treeiter][5]=}') # DEBUG + # so user can search for disabled keys also + if isinstance(keys, str): + keys = [keys, ''] + if not (any(keys) and any([kval0, mask0, kval1, mask1])): act = "Disabled" self.keybind_filter_str = self.keybind_filter_str.lower() - searchtxt = (act + " " + keys + " " + desc).lower() + keys_str = re.sub(r'[\[,\]]', '', str(keys)) + searchtxt = f'{act} {keys_str} {desc}'.lower() + # print(f'{searchtxt=}') # DEBUG pos = searchtxt.find(self.keybind_filter_str) if (pos >= 0): dbg("filter find:%s in search text: %s" % - (self.keybind_filter_str, searchtxt)) + (self.keybind_filter_str, searchtxt)) return True return False @@ -459,8 +478,8 @@ def on_search(widget, text): MAX_SEARCH_LEN = 10 self.keybind_filter_str = widget.get_text() ln = len(self.keybind_filter_str) - #its a small list & we are eager for quick search, but limit - if (ln >=2 and ln < MAX_SEARCH_LEN): + # it's a small list & we are eager for quick search, but limit + if (ln >= 2 and ln < MAX_SEARCH_LEN): dbg("filter search str: %s" % self.keybind_filter_str) self.treemodelfilter.refilter() @@ -475,31 +494,54 @@ def on_search_refilter(widget): liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) keybindings = self.config['keybindings'] - keybindutil = KeyBindUtil() - plugin_keyb_act = keybindutil.get_all_act_to_keys() - plugin_keyb_desc = keybindutil.get_all_act_to_desc() - #merge give preference to main bindings over plugin - keybindings = {**plugin_keyb_act, **keybindings} + keybindutil = KeyBindUtil() + plugin_keyb_act = keybindutil.get_all_act_to_keys() + plugin_keyb_desc = keybindutil.get_all_act_to_desc() + # merge give preference to main bindings over plugin + keybindings = {**plugin_keyb_act, **keybindings} self.keybindingnames = {**plugin_keyb_desc, **self.keybindingnames} - #dbg("appended actions %s names %s" % (keybindings, self.keybindingnames)) + # dbg("appended actions %s names %s" % (keybindings, self.keybindingnames)) - for keybinding in keybindings: + # print('got here2') # DEBUG + for keybinding, value in keybindings.items(): keyval = 0 mask = 0 - value = keybindings[keybinding] - if value is not None and value != '': - try: - (keyval, mask) = self.keybindings._parsebinding(value) - except KeymapError: - pass - liststore.append([keybinding, self.keybindingnames[keybinding], - keyval, mask]) + # print('got here4') # DEBUG + if isinstance(value, str): + # print('got here1') # DEBUG + if value is not None and value != '': + try: + (keyval, mask) = self.keybindings._parsebinding(value) + except KeymapError: + pass + liststore.append([keybinding, self.keybindingnames[keybinding], + keyval, mask, 0, 0]) + # print(f'{keyval=} {mask=}') # DEBUG + self.config['keybindings'][keybinding] = [value, ''] + if isinstance(value, list): + # print('got here3') # DEBUG + bindings_list = [(0, 0), (0, 0)] + for index, val in enumerate(value): + if val is not None and val != '': + try: + keyval, mask = self.keybindings._parsebinding(val) + # print('got here5') # DEBUG + bindings_list[index] = (keyval, mask) + except KeymapError: + pass + # print('got here6') # DEBUG + # print(f'{keyval=} {mask=} {bindings_list=}') # DEBUG + liststore.append([keybinding, self.keybindingnames[keybinding], + # keyval, mask]) # DEBUG + bindings_list[0][0], bindings_list[0][1], + bindings_list[1][0], bindings_list[1][1]]) + # print('got here7') # DEBUG self.treemodelfilter = liststore.filter_new() self.treemodelfilter.set_visible_func(filter_visible, keybindings) widget.set_model(self.treemodelfilter) - ## Plugins tab + # PLUGINS TAB # Populate the plugin list widget = guiget('pluginlist') liststore = widget.get_model() @@ -508,12 +550,12 @@ def on_search_refilter(widget): pluginlist = self.registry.get_available_plugins() self.plugins = {} for plugin in pluginlist: - if plugin[0] != "_": # Do not display hidden plugins + if plugin[0] != "_": # Do not display hidden plugins self.plugins[plugin] = self.registry.is_enabled(plugin) for plugin in self.plugins: self.pluginiters[plugin] = liststore.append([plugin, - self.plugins[plugin]]) + self.plugins[plugin]]) selection = widget.get_selection() selection.connect('changed', self.on_plugin_selection_changed) if len(self.pluginiters) > 0: @@ -526,7 +568,7 @@ def set_profile_values(self, profile): dbg('Setting profile %s' % profile) - ## General tab + # GENERAL TAB # Use system font widget = guiget('system_font_checkbutton') widget.set_active(self.config['use_system_font']) @@ -534,7 +576,7 @@ def set_profile_values(self, profile): # Font selector widget = guiget('font_selector') - if self.config['use_system_font'] == True: + if self.config['use_system_font'] is True: fontname = self.config.get_system_mono_font() if fontname is not None: widget.set_font_name(fontname) @@ -601,7 +643,7 @@ def set_profile_values(self, profile): except: widget.set_color(Gdk.color_parse('#ffffff')) - ## Command tab + # COMMAND TAB # Login shell widget = guiget('login_shell_checkbutton') widget.set_active(self.config['login_shell']) @@ -622,7 +664,7 @@ def set_profile_values(self, profile): # Default is to close the terminal widget.set_active(0) - ## Colors tab + # COLORS TAB # Use system colors widget = guiget('use_theme_colors_checkbutton') widget.set_active(self.config['use_theme_colors']) @@ -680,6 +722,7 @@ def set_profile_values(self, profile): for palette_id in range(0, NUM_PALETTE_COLORS): widget = self.get_palette_widget(palette_id) widget.set_events(Gdk.EventMask.BUTTON_PRESS_MASK) + def on_palette_click(event, data, widget=widget): self.edit_palette_button(widget) widget.connect('button-press-event', on_palette_click) @@ -707,7 +750,7 @@ def on_palette_click(event, data, widget=widget): widget = guiget('custom_url_handler_entry') widget.set_text(self.config['custom_url_handler']) - ## Background tab + # BACKGROUND TAB # Radio values if self.config['background_type'] == 'solid': guiget('solid_radiobutton').set_active(True) @@ -752,8 +795,8 @@ def on_palette_click(event, data, widget=widget): # Background shading widget = guiget('background_darkness_scale') widget.set_value(float(self.config['background_darkness'])) - - ## Scrolling tab + + # SCROLLING TAB # Scrollbar position widget = guiget('scrollbar_position_combobox') value = self.config['scrollbar_position'] @@ -776,7 +819,7 @@ def on_palette_click(event, data, widget=widget): widget = guiget('scroll_on_keystroke_checkbutton') widget.set_active(self.config['scroll_on_keystroke']) - ## Compatibility tab + # COMPATIBILITY TAB # Backspace key widget = guiget('backspace_binding_combobox') value = self.config['backspace_binding'] @@ -800,11 +843,11 @@ def on_palette_click(event, data, widget=widget): else: widget.set_active(0) - ## Titlebar tab + # TITLEBAR TAB # Titlebar colors for bit in ['title_transmit_fg_color', 'title_transmit_bg_color', - 'title_receive_fg_color', 'title_receive_bg_color', - 'title_inactive_fg_color', 'title_inactive_bg_color']: + 'title_receive_fg_color', 'title_receive_bg_color', + 'title_inactive_fg_color', 'title_inactive_bg_color']: widget = guiget(bit) widget.set_color(Gdk.color_parse(self.config[bit])) # Hide size text from the title bar @@ -816,7 +859,7 @@ def on_palette_click(event, data, widget=widget): self.on_title_system_font_checkbutton_toggled(widget) # Font selector widget = guiget('title_font_selector') - if self.config['title_use_system_font'] == True: + if self.config['title_use_system_font'] is True: fontname = self.config.get_system_prop_font() if fontname is not None: widget.set_font_name(fontname) @@ -1033,7 +1076,7 @@ def on_scrollback_infinite_toggled(self, widget): """Scrollback infiniteness changed""" spinbutton = self.builder.get_object('scrollback_lines_spinbutton') value = widget.get_active() - if value == True: + if value is True: spinbutton.set_sensitive(False) else: spinbutton.set_sensitive(True) @@ -1052,7 +1095,7 @@ def on_scrollbar_position_combobox_changed(self, widget): self.config['scrollbar_position'] = value self.config.save() - def on_background_image_file_set(self,widget): + def on_background_image_file_set(self, widget): self.config['background_image'] = widget.get_filename() self.config.save() @@ -1140,7 +1183,7 @@ def on_foreground_colorbutton_draw(self, widget, cr): cr.fill() def on_foreground_colorbutton_click(self, event, data): - dialog = Gtk.ColorChooserDialog("Choose Terminal Text Color") + dialog = Gtk.ColorChooserDialog(_("Choose Terminal Text Color")) fg = self.config['foreground_color'] dialog.set_rgba(Gdk.RGBA.from_color(Gdk.color_parse(self.config['foreground_color']))) dialog.connect('notify::rgba', self.on_foreground_colorpicker_color_change) @@ -1379,7 +1422,7 @@ def on_inactive_color_offset_value_changed(self, widget): """Inactive color offset setting changed""" value = widget.get_value() # This one is rounded according to the UI. if value > 1.0: - value = 1.0 + value = 1.0 self.config['inactive_color_offset'] = value self.config.save() guiget = self.builder.get_object @@ -1390,7 +1433,7 @@ def on_inactive_bg_color_offset_value_changed(self, widget): """Inactive background color offset setting changed""" value = widget.get_value() # This one is rounded according to the UI. if value > 1.0: - value = 1.0 + value = 1.0 self.config['inactive_bg_color_offset'] = value self.config.save() guiget = self.builder.get_object @@ -1494,7 +1537,7 @@ def addprofile(self, name, toclone): treeview = guiget('profilelist') model = treeview.get_model() - values = [ r[0] for r in model ] + values = [r[0] for r in model] newprofile = name if newprofile in values: @@ -1555,7 +1598,7 @@ def on_layoutaddbutton_clicked(self, _button): treeview = guiget('layoutlist') model = treeview.get_model() - values = [ r[0] for r in model ] + values = [r[0] for r in model] name = _('New Layout') if name in values: @@ -1646,8 +1689,8 @@ def on_system_font_checkbutton_toggled(self, checkbox): widget.set_sensitive(not value) self.config['use_system_font'] = value self.config.save() - - if self.config['use_system_font'] == True: + + if self.config['use_system_font'] is True: fontname = self.config.get_system_mono_font() if fontname is not None: widget.set_font_name(fontname) @@ -1665,7 +1708,7 @@ def on_title_system_font_checkbutton_toggled(self, checkbox): self.config['title_use_system_font'] = value self.config.save() - if self.config['title_use_system_font'] == True: + if self.config['title_use_system_font'] is True: fontname = self.config.get_system_prop_font() if fontname is not None: widget.set_font_name(fontname) @@ -1695,9 +1738,9 @@ def update_background_tab(self): imagewidget = guiget('image_radiobutton') transwidget = guiget('transparent_radiobutton') - if imagewidget.get_active() == True: + if imagewidget.get_active() is True: backtype = 'image' - elif transwidget.get_active() == True: + elif transwidget.get_active() is True: backtype = 'transparent' else: backtype = 'solid' @@ -1748,7 +1791,7 @@ def on_plugin_selection_changed(self, selection): self.set_plugin(plugin) self.previous_plugin_selection = plugin - widget = self.builder.get_object('plugintogglebutton') + # widget = self.builder.get_object('plugintogglebutton') def on_plugin_toggled(self, cell, path): """A plugin has been enabled or disabled""" @@ -1767,14 +1810,14 @@ def on_plugin_toggled(self, cell, path): # Update the treeview model[path][1] = self.plugins[plugin] - enabled_plugins = [x for x in self.plugins if self.plugins[x] == True] + enabled_plugins = [x for x in self.plugins if self.plugins[x] is True] self.config['enabled_plugins'] = enabled_plugins self.config.save() def set_plugin(self, plugin): """Show the preferences for the selected plugin, if any""" - pluginpanelabel = self.builder.get_object('pluginpanelabel') - pluginconfig = self.config.plugin_get_config(plugin) + # pluginpanelabel = self.builder.get_object('pluginpanelabel') + # pluginconfig = self.config.plugin_get_config(plugin) # FIXME: Implement this, we need to auto-construct a UI for the plugin def on_profile_name_edited(self, cell, path, newtext): @@ -1882,15 +1925,24 @@ def on_bold_text_is_bright_checkbutton_toggled(self, widget): self.config['bold_is_bright'] = widget.get_active() self.config.save() - def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): - inpath = path #save for debugging + def on_cellrenderer_accel_edited_keybind1(self, liststore, path, key, mods, _code): + self.on_cellrenderer_accel_edited(liststore, path, key, mods, _code, 2, 3, 0) + + def on_cellrenderer_accel_edited_keybind2(self, liststore, path, key, mods, _code): + self.on_cellrenderer_accel_edited(liststore, path, key, mods, _code, 4, 5, 1) + + def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code, key_index, mod_index, binding_index): + inpath = path # save for debugging trpath = Gtk.TreePath.new_from_string(inpath) - path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) + path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) dbg("convert path with filter from: %s to: %s" % - (inpath, path)) + (inpath, path)) """Handle an edited keybinding""" + # print(f'{key=} {mods=} {_code=}') # DEBUG + # print(f'{path=}') # DEBUG # Ignore `Gdk.KEY_Tab` so that `Shift+Tab` is displayed as `Shift+Tab` - # in `Preferences>Keybindings` and NOT `Left Tab` (see `Gdk.KEY_ISO_Left_Tab`). + # in `Preferences>Keybindings` and NOT `Left Tab` + # (see `Gdk.KEY_ISO_Left_Tab`). if mods & Gdk.ModifierType.SHIFT_MASK and key != Gdk.KEY_Tab: key_with_shift = Gdk.Keymap.translate_keyboard_state( self.keybindings.keymap, @@ -1900,7 +1952,7 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): ) keyval_lower, keyval_upper = Gdk.keyval_convert_case(key) - # Remove the Shift modifier from `mods` if a new key binding doesn't + # Remove the Shift modifier from `mods` if a new keybinding doesn't # contain a letter and its key value (`key`) can't be modified by a # Shift key. if key_with_shift.level != 0 and keyval_lower == keyval_upper: @@ -1908,27 +1960,30 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): key = key_with_shift.keyval accel = Gtk.accelerator_name(key, mods) + # print(f'{accel=}') # DEBUG current_binding = liststore.get_value(liststore.get_iter(path), 0) + # print(f'{current_binding=}') # DEBUG parsed_accel = Gtk.accelerator_parse(accel) + # print(f'{parsed_accel=}') # DEBUG - keybindutil = KeyBindUtil() - keybindings = self.config["keybindings"] - #merge give preference to main bindings over plugin - plugin_keyb_act = keybindutil.get_all_act_to_keys() - keybindings = {**plugin_keyb_act, **keybindings} + keybindutil = KeyBindUtil() + keybindings = self.config["keybindings"] + # merge give preference to main bindings over plugin + plugin_keyb_act = keybindutil.get_all_act_to_keys() + # print(f'{keybindings=} {plugin_keyb_act=}') # DEBUG + keybindings = {**plugin_keyb_act, **keybindings} + # print(f'2{keybindings=}') # DEBUG duplicate_bindings = [] - for conf_binding, conf_accel in keybindings.items(): - if conf_accel is None: - continue + for conf_binding, conf_accels in keybindings.items(): + for conf_accel in conf_accels: + if conf_accel is None: + continue - parsed_conf_accel = Gtk.accelerator_parse(conf_accel) + parsed_conf_accel = Gtk.accelerator_parse(conf_accel) - if ( - parsed_accel == parsed_conf_accel - and current_binding != conf_binding - ): - duplicate_bindings.append((conf_binding, conf_accel)) + if (parsed_accel == parsed_conf_accel and current_binding != conf_binding): + duplicate_bindings.append((conf_binding, conf_accel)) if duplicate_bindings: dialog = Gtk.MessageDialog( @@ -1936,7 +1991,7 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): flags=Gtk.DialogFlags.MODAL, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.CLOSE, - text="Duplicate Key Bindings Are Not Allowed", + text=_("Duplicate keybindings are not allowed."), ) accel_label = Gtk.accelerator_get_label(key, mods) @@ -1955,17 +2010,37 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): return - celliter = liststore.get_iter_from_string(path) - liststore.set(celliter, 2, key, 3, mods) - binding = liststore.get_value(liststore.get_iter(path), 0) + print(f'{binding=}') # DEBUG accel = Gtk.accelerator_name(key, mods) - self.config['keybindings'][binding] = accel + print(f'{accel=}') # DEBUG + alt_index = abs(binding_index - 1) + if self.config['keybindings'][binding][alt_index] != accel: + self.config['keybindings'][binding][binding_index] = accel + else: + dialog = Gtk.MessageDialog( + transient_for=self.window, + flags=Gtk.DialogFlags.MODAL, + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.CLOSE, + text=_("Duplicate keybindings are not allowed."), + ) + + self.active_message_dialog = dialog + dialog.run() + dialog.destroy() + self.active_message_dialog = None + + return + + celliter = liststore.get_iter_from_string(path) + print(f'{celliter=}') # DEBUG + liststore.set(celliter, key_index, key, mod_index, mods) plugin_keyb_desc = keybindutil.get_act_to_desc(binding) if plugin_keyb_desc: dbg("modifying plugin binding: %s, %s, %s" % - (plugin_keyb_desc, binding, accel)) + (plugin_keyb_desc, binding, accel)) keybindutil.bindkey([plugin_keyb_desc, binding, accel]) else: dbg("skipping: %s" % binding) @@ -1973,25 +2048,33 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): self.config.save() - def on_cellrenderer_accel_cleared(self, liststore, path): - inpath = path #save for debugging + def on_cellrenderer_accel_cleared_keybind1(self, liststore, path): + self.on_cellrenderer_accel_cleared(liststore, path, 2, 3, 0) + + def on_cellrenderer_accel_cleared_keybind2(self, liststore, path): + self.on_cellrenderer_accel_cleared(liststore, path, 4, 5, 1) + + def on_cellrenderer_accel_cleared(self, liststore, path, key_index, mod_index, binding_index): + inpath = path # save for debugging trpath = Gtk.TreePath.new_from_string(inpath) - path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) + path = str(self.treemodelfilter.convert_path_to_child_path(trpath)) dbg("convert path with filter from: %s to: %s" % - (inpath, path)) + (inpath, path)) + # print(f'{liststore=}') # DEBUG """Handle the clearing of a keybinding accelerator""" celliter = liststore.get_iter_from_string(path) - liststore.set(celliter, 2, 0, 3, 0) + liststore.set(celliter, key_index, 0, mod_index, 0) binding = liststore.get_value(liststore.get_iter(path), 0) - self.config['keybindings'][binding] = "" + self.config['keybindings'][binding][binding_index] = '' self.config.save() def on_open_manual(self, widget): """Open the fine manual""" self.term.key_help() + class LayoutEditor: profile_ids_to_profile = None profile_profile_to_ids = None @@ -2024,7 +2107,6 @@ def set_layout(self, layout_name): store.clear() children = list(layout.keys()) - i = 0 while children != []: child = children.pop() child_type = layout[child]['type'] @@ -2057,7 +2139,7 @@ def set_layout(self, layout_name): def update_profiles(self): """Update the list of profiles""" self.profile_ids_to_profile = {} - self.profile_profile_to_ids= {} + self.profile_profile_to_ids = {} chooser = self.builder.get_object('layout_profile_chooser') profiles = self.config.list_profiles() @@ -2157,6 +2239,7 @@ def on_layout_profile_workingdir_activate(self, widget): layout[self.layout_item]['directory'] = workdir self.config.save() + if __name__ == '__main__': from . import util util.DEBUG = True diff --git a/terminatorlib/terminal_popup_menu.py b/terminatorlib/terminal_popup_menu.py index dc7f95f78..b662b9b70 100644 --- a/terminatorlib/terminal_popup_menu.py +++ b/terminatorlib/terminal_popup_menu.py @@ -52,36 +52,36 @@ def get_menu_item_mask(self, maskstr): return mask def menu_item(self, menutype, actstr, menustr): - act = self.config.base.get_item('keybindings', actstr) - maskstr = act[actstr] if actstr in act else "" - mask = self.get_menu_item_mask(maskstr) + act = self.config.base.get_item('keybindings', actstr) + maskstr = act[actstr][0] if actstr in act else "" + mask = self.get_menu_item_mask(maskstr) accelchar = "" pos = menustr.lower().find("_") if (pos >= 0 and pos+1 < len(menustr)): accelchar = menustr.lower()[pos+1] - #this may require tweak. what about shortcut function keys ? + # this may require tweak. what about shortcut function keys ? if maskstr: mpos = maskstr.rfind(">") - #can't have a char at 0 position as <> is len 2 + # can't have a char at 0 position as <> is len 2 if mpos >= 0 and mpos+1 < len(maskstr): configaccelchar = maskstr[mpos+1:] - #ensure to take only 1 char else ignore + # ensure to take only 1 char else ignore if len(configaccelchar) == 1: dbg("found accelchar in config:%s override:%s" - % (configaccelchar, accelchar)) + % (configaccelchar, accelchar)) accelchar = configaccelchar dbg("action from config:%s for item:%s with shortcut accelchar:(%s)" - % (maskstr, menustr, accelchar)) + % (maskstr, menustr, accelchar)) item = menutype.new_with_mnemonic(_(menustr)) if mask: item.add_accelerator("activate", - self.accelgrp, - Gdk.keyval_from_name(accelchar), - mask, - Gtk.AccelFlags.VISIBLE) + self.accelgrp, + Gdk.keyval_from_name(accelchar), + mask, + Gtk.AccelFlags.VISIBLE) return item def show(self, widget, event=None):