From e2af6868948cb2ae6e5dcb166aef5c4d932c367b Mon Sep 17 00:00:00 2001 From: Vishweshwar Saran Singh Deo Date: Mon, 16 Oct 2023 16:44:43 +0530 Subject: [PATCH] [bug 773] 773-Feature-is-there-any-way-to-Hide-or-Sort-context-menu-items #773 - Adds a context menu (right click) - Gives an UI under Preferences->Plugins with capability to organize items - Supported by PluginEventRegistry which helps to add plugin actions to menu - apart from other plugins using this, for eg. this context_menu_plugin can - register its menu functions while itself creating a menu - ContextMenu items will appear in the order of plugin loading, so may be later we can have a priority / order in plugin loading - Supported by KeyBindUtil for action key / desc matching - Changes made to prefseditor.py for selection of plugin preferences, - update_gui etc - Cleans and identifies common dependencies which can we further worked on - - Gradual removal of menuitems to be done with checking logic and removal of if - based conditions. All cases are being compiled in class Filter below which - have to be removed - --- terminatorlib/config.py | 4 + terminatorlib/plugin.py | 215 ++++- terminatorlib/plugins/PluginContextMenu.glade | 301 +++++++ terminatorlib/plugins/context_menu.py | 750 ++++++++++++++++++ terminatorlib/prefseditor.py | 84 +- terminatorlib/terminal.py | 15 +- terminatorlib/terminal_popup_menu.py | 5 +- 7 files changed, 1343 insertions(+), 31 deletions(-) create mode 100644 terminatorlib/plugins/PluginContextMenu.glade create mode 100644 terminatorlib/plugins/context_menu.py diff --git a/terminatorlib/config.py b/terminatorlib/config.py index 4fbb907d..05580a3e 100644 --- a/terminatorlib/config.py +++ b/terminatorlib/config.py @@ -123,6 +123,10 @@ 'new_tab_after_current_tab': False, }, 'keybindings': { + 'zoom' : '', + 'unzoom' : '', + 'maximise' : '', + 'open_debug_tab' : '', 'zoom_in' : 'plus', 'zoom_out' : 'minus', 'zoom_normal' : '0', diff --git a/terminatorlib/plugin.py b/terminatorlib/plugin.py index 49a972e7..5cd26203 100644 --- a/terminatorlib/plugin.py +++ b/terminatorlib/plugin.py @@ -30,6 +30,8 @@ from .util import dbg, err, get_config_dir from .terminator import Terminator +from .version import APP_NAME + class Plugin(object): """Definition of our base plugin class""" capabilities = None @@ -118,6 +120,11 @@ def load_plugins(self, force=False): self.done = True + def get_plugin_instance(self, plugin): + instance = self.instances.get(plugin, None) + dbg('get plugin: %s instance: %s' % (plugin, instance)) + return instance + def get_plugins_by_capability(self, capability): """Return a list of plugins with a particular capability""" result = [] @@ -221,8 +228,14 @@ class KeyBindUtil: map_act_to_keys = {} map_act_to_desc = {} + #merged keybindings and plugin key bindings + map_all_act_to_keys = {} + map_all_act_to_desc = {} + + config = Config() + def __init__(self, config=None): - self.config = config + self.load_merge_key_maps() #Example # bind @@ -234,6 +247,17 @@ def __init__(self, config=None): # if act == "url_find_next": + def load_merge_key_maps(self): + + cfg_keybindings = KeyBindUtil.config['keybindings'] + + #TODO need to check if cyclic dep here, we only using keybindingnames + from terminatorlib.prefseditor import PrefsEditor + pref_keybindingnames = PrefsEditor.keybindingnames + + #merge give preference to main bindings over plugin + KeyBindUtil.map_all_act_to_keys = {**self.map_act_to_keys, **cfg_keybindings} + KeyBindUtil.map_all_act_to_desc = {**self.map_act_to_desc, **pref_keybindingnames} #check map key_val_mask -> action def _check_keybind_change(self, key): @@ -313,18 +337,33 @@ def keyaction(self, event): dbg("keyaction: (%s)" % str(ret)) return self.map_key_to_act.get(ret, None) + #functions to get actstr to keys / key mappings or desc / desc mapppings + #for plugins or merged keybindings + def get_act_to_keys(self, key): + return self.map_all_act_to_keys.get(key) + + def get_plugin_act_to_keys(self, key): return self.map_act_to_keys.get(key) - def get_all_act_to_keys(self): + def get_all_plugin_act_to_keys(self): return self.map_act_to_keys - def get_all_act_to_desc(self): - return self.map_act_to_desc + def get_all_act_to_keys(self): + return self.map_all_act_to_keys def get_act_to_desc(self, act): + return self.map_all_act_to_desc.get(act) + + def get_plugin_act_to_desc(self, act): return self.map_act_to_desc.get(act) + def get_all_plugin_act_to_desc(self): + return self.map_act_to_desc + + def get_all_act_to_desc(self): + return self.map_all_act_to_desc + #get action to key binding from config def get_act_to_keys_config(self, act): if not self.config: @@ -332,3 +371,171 @@ def get_act_to_keys_config(self, act): keybindings = self.config["keybindings"] return keybindings.get(act) + + + + +#-PluginEventRegistry utility: Vishweshwar Saran Singh Deo +# +# +#use-case: so if a plugin wants to get an action added to context-menu +#it can be done as plugins register their keybindings, but their actions +#are not understood by terminal on_keypress mapping since pluings are +#external to core working. +# +#So for eg: PluginContextMenuAct we are detecting and handling it first before +#passing to terminal on_keypress, but what about if an other Plugin Key Press +#needs to be handled locally in its context. May be we can register +#its function and pass the action to it + +#this class keeps the events to local function instead of sending to +#terminal, since for things like plugins, the terminal key mapper won't +#have the context. lets see what dependencies come up and all Plugin actions +#can be added to Context Menu eg: +# +# so for action PluginUrlActFindNext which is understood by MouseFreeURLHandler +# the plugin can register its action and if that action is added to +# menu, that action will be passed to the plugin +# +# import terminatorlib.plugin as plugin +# +# event_registry = plugin.PluginEventRegistry() +# ... +# MouseFreeURLHandler.event_registry.register(PluginUrlActFindNext, +# self.on_action, +# 'custom-tag') +# ... +# def on_action(self, act): +# self.on_keypress(None, 'event', act) +# + +class PluginEventRegistry: + + Map_Act_Event_Handlers = {} + + def __init__(self): + pass + + def register(self, action, handler, tag_id): + + dbg('register action:(%s) tag_id:(%s)' % (action, tag_id)) + + if action not in PluginEventRegistry.Map_Act_Event_Handlers: + dbg('adding new handler for: %s' % action) + PluginEventRegistry.Map_Act_Event_Handlers[action] = {tag_id: handler} + else: + dbg('appending handler for: %s' % action) + handlers = PluginEventRegistry.Map_Act_Event_Handlers[action] + handlers[tag_id] = handler + + """ + dbg('register: (%s) total_events:(%s)' % + (len(PluginEventRegistry.Map_Act_Event_Handlers), + PluginEventRegistry.Map_Act_Event_Handlers)) + """ + + def call_action_handlers(self, action): + if action not in PluginEventRegistry.Map_Act_Event_Handlers: + dbg('no handers found for action:%s' % action) + return False + + act_items = PluginEventRegistry.Map_Act_Event_Handlers[action].copy() + for key, handler in act_items.items(): + dbg('calling handers: %s for action:%s' % (handler,action)) + handler(action) + + return True + + + def unregister(self, action, handler, tag_id): + + if action not in PluginEventRegistry.Map_Act_Event_Handlers: + dbg('action not found: %s' % action) + return False + + lst = PluginEventRegistry.Map_Act_Event_Handlers[action] + if tag_id in lst: + dbg('removing tag_id:(%s) for act: (%s)' % (tag_id, action)) + del lst[tag_id] + if not len(lst): + dbg('removing empty action:(%s) from registry' % action) + del PluginEventRegistry.Map_Act_Event_Handlers[action] + return True + + return False + + +# +# -PluginGUI utility: Vishweshwar Saran Singh Deo +# +# -To assist in injecting and restoring of Plugin UI interfaces +# -Loading of Glade Files and Glade Data +# +# -Eg. +# +# plugin_builder = self.plugin_gui.get_glade_builder(plugin) +# plugin_window = plugin_builder.get_object('PluginContextMenu') +# +# ... +# ... +# +# #call back from prefseditor.py if func defined +# +# def update_gui(self, widget, visible): +# +# #add UI to Prefs->Plugins->ContextMenu +# prev_widget = self.plugin_gui.add_gui(widget, self.plugin_window) +# #use return value to add back when not visible, later we can +# #handle this automatically + +from . import config + +class PluginGUI: + + def __init__(self): + self.save_prev_child = None + + #adds new UI and saves previous UI + def add_gui(self, parent_widget, child_widget): + + hpane_widget = parent_widget.get_child2() + if hpane_widget: + #if not self.save_prev_child: + self.save_prev_child = hpane_widget + hpane_widget.destroy() + #add plugin gui to prefs + parent_widget.add2(child_widget) + parent_widget.show_all() + return hpane_widget + + + def get_glade_data(self, plugin): + gladedata = '' + try: + # Figure out where our library is on-disk so we can open our + (head, _tail) = os.path.split(config.__file__) + if plugin: + filename = plugin + '.glade' + plugin_glade_file = os.path.join(head, 'plugins', filename) + gladefile = open(plugin_glade_file, 'r') + gladedata = gladefile.read() + gladefile.close() + except Exception as ex: + dbg("Failed to find: ex:%s" % (ex)) + + return gladedata + + + def get_glade_builder(self, plugin): + + gladedata = self.get_glade_data(plugin) + if not gladedata: + return + + plugin_builder = Gtk.Builder() + plugin_builder.set_translation_domain(APP_NAME) + plugin_builder.add_from_string(gladedata) + + return plugin_builder + + diff --git a/terminatorlib/plugins/PluginContextMenu.glade b/terminatorlib/plugins/PluginContextMenu.glade new file mode 100644 index 00000000..6e718fb5 --- /dev/null +++ b/terminatorlib/plugins/PluginContextMenu.glade @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + 1024 + 800 + + + + True + False + 10 + 10 + + + True + False + 5 + 10 + bottom + center + + + gtk-apply + True + True + True + True + + + True + True + 0 + + + + + gtk-discard + True + True + True + True + + + True + True + 1 + + + + + 1 + 3 + + + + + True + False + center + vertical + 2 + start + + + << + True + True + True + + + True + True + 0 + + + + + >> + True + True + True + + + True + True + 1 + + + + + 1 + 2 + + + + + True + True + True + in + + + True + True + True + True + KeybindingListStoreRight + True + + + + + + Name + + + + 0 + + + + + + + Action + + + + 1 + + + + + + + KeyBinding + + + other + + + 2 + 3 + + + + + + + + + 2 + 2 + + + + + True + True + True + in + + + True + True + True + True + KeybindingListStoreLeft + True + True + + + + + + Name + + + + 0 + + + + + + + Action + + + + 1 + + + + + + + KeyBinding + + + other + + + 2 + 3 + + + + + + + + + 0 + 2 + + + + + True + True + edit-find-symbolic + False + False + search + + + 2 + 1 + + + + + True + False + 5 + Select Action Items + + + 2 + 0 + + + + + True + False + 5 + Context Menu Items + + + 0 + 0 + + + + + + + + + + + + + + + + + + + + + diff --git a/terminatorlib/plugins/context_menu.py b/terminatorlib/plugins/context_menu.py new file mode 100644 index 00000000..ddc0e014 --- /dev/null +++ b/terminatorlib/plugins/context_menu.py @@ -0,0 +1,750 @@ +""" +- Context Menu Plugin - Vishweshwar Saran Singh Deo +- +- Adds a context menu (right click) +- Gives an UI under Preferences->Plugins with capability to organize items +- Supported by PluginEventRegistry which helps to add plugin actions to menu +- apart from other plugins using this, for eg. this context_menu_plugin can +- register its menu functions while itself creating a menu +- ContextMenu items will appear in the order of plugin loading, so may be later + we can have a priority / order in plugin loading +- Supported by KeyBindUtil for action key / desc matching +- Changes made to prefseditor.py for selection of plugin preferences, +- update_gui etc +- Cleans and identifies common dependencies which can we further worked on +- +- Gradual removal of menuitems to be done with checking logic and removal of if +- based conditions. All cases are being compiled in class Filter below which +- have to be removed +- +""" + +import gi +import os +import time +gi.require_version('Vte', '2.91') # vte-0.38 (gnome-3.14) +from gi.repository import Vte + +from gi.repository import Gtk, Gdk +from terminatorlib.terminator import Terminator + +from terminatorlib.config import Config +from terminatorlib import config + +import terminatorlib.plugin as plugin +from terminatorlib.keybindings import Keybindings, KeymapError + +from terminatorlib.util import get_config_dir, err, dbg, gerr +from terminatorlib import regex + +from terminatorlib.prefseditor import PrefsEditor + +from terminatorlib.translation import _ + + + +AVAILABLE = ['PluginContextMenu'] + +PluginContextMenuAct = '(Plugin) Edit_menu' +PluginContextMenuDesc= 'Plugin Edit Menu' + +#Main Plugin + +class PluginContextMenu(plugin.MenuItem): + + capabilities = ['terminal_menu'] + handler_name = 'PluginContextMenu' + nameopen = None + namecopy = None + + filter = None #TODO: REMOVE + + config = Config() + plugin_gui = plugin.PluginGUI() + prev_widget = None + + #this won't have all keybinds, eg of plugins as all plugins + #are being loaded now so we will reload it again + keybindutil = plugin.KeyBindUtil() + + event_registry = plugin.PluginEventRegistry() + + #UI elements <> are handled specially as left->right won't append + #to right but a single copy will be there on right + #for left<-right the item in right will remain and an instance of + #that element will be copied to left + + UI_SEPARATOR = '' + ui_elements = { UI_SEPARATOR : { + 'count' : 0, + 'widget': Gtk.SeparatorMenuItem() + } + } + + #PluginContextMenuAct (edit context menu) action will be appended to this + DEFAULT_MENU = {k: v for v, k in + enumerate(['copy', 'paste', 'edit_window_title', + 'split_auto','split_horiz', 'split_vert', + 'new_tab', 'close_term', 'toggle_scrollbar', + 'zoom', 'maximise', 'unzoom', + 'open_debug_tab' + ])} + + #TODO:move this to a better place / redo once code is stabilized + MAP_ACTION_ICONS = { + + 'split_vert' : 'terminator_vert', + 'split_horiz': 'terminator_horiz' + } + + def __init__(self): + + dbg('loading context_menu plugin') + self.connect_signals() + + #call PluginContext Editor in Preferences for action PluginContextMenuAct + self.keybindutil.bindkey_check_config( + [PluginContextMenuDesc, PluginContextMenuAct, "m"]) + + PluginContextMenu.event_registry.register( + PluginContextMenuAct, + self.lauch_pref_context_menu, + self.handler_name) + + self.reload_plugin_config() + + self.accelgrp = Gtk.AccelGroup() + self.left_select_actions = {} + + + def reload_plugin_config(self): + self.config_keyb = self.config.plugin_get_config(self.handler_name) + if not self.config_keyb: + self.config_keyb = {} + + + def connect_signals(self): + + self.windows = Terminator().get_windows() + for window in self.windows: + window.connect('key-press-event', self.on_keypress) + + + def update_gui(self, widget, visible): + + #restore prev UI to Prefs->Plugins->ContextMenu + if not visible and self.prev_widget: + self.plugin_gui.add_gui(widget, self.prev_widget) + return + + plugin = self.__class__.__name__ + + plugin_builder = self.plugin_gui.get_glade_builder(plugin) + self.plugin_builder = plugin_builder + + self.plugin_window = plugin_builder.get_object('PluginContextMenu') + + tview_left = plugin_builder.get_object('PluginContextMenuListLeft') + tview_right = plugin_builder.get_object('PluginContextMenuListRight') + + self.tview_left = tview_left + self.tview_right = tview_right + + self.store_left = plugin_builder.get_object('KeybindingListStoreLeft') + self.store_right = plugin_builder.get_object('KeybindingListStoreRight') + + + TARGETS = [ ('TREE_MODEL_ROW', Gtk.TargetFlags.SAME_WIDGET, 0) ] + self.tview_left.enable_model_drag_source( Gdk.ModifierType.BUTTON1_MASK, + TARGETS, + Gdk.DragAction.DEFAULT| + Gdk.DragAction.MOVE) + + tview_left.enable_model_drag_dest(TARGETS, Gdk.DragAction.DEFAULT) + + tview_left.connect("drag-data-get", self.on_drag_data_get) + tview_left.connect("drag-data-received",self.on_drag_data_received) + + button_mv_left = plugin_builder.get_object('ContextMenuPluginMoveLeft') + button_mv_left.connect('clicked', self.on_button_mv_left, + tview_left, tview_right) + + button_mv_right = plugin_builder.get_object('ContextMenuPluginMoveRight') + button_mv_right.connect('clicked', self.on_button_mv_right, + tview_left, tview_right) + + button_discard = plugin_builder.get_object('PluginContextMenuButtonDiscard') + button_discard.connect('clicked', self.on_button_discard_clicked) + + button_add = plugin_builder.get_object('PluginContextMenuButtonApply') + button_add.connect('clicked', self.on_button_add_clicked) + + #add UI to Prefs->Plugins->ContextMenu + prev_widget = self.plugin_gui.add_gui(widget, self.plugin_window) + if not self.prev_widget: + self.prev_widget = prev_widget + + self.setup_data() + + + def setup_data(self): + self.left_select_actions = {} + self.store_left.clear() + self.store_right.clear() + + self.set_context_menu_items(self.tview_left) + + #reset filter layer before setting up + self.tview_right.set_model(self.store_right) + self.set_act_menu_items(self.tview_right) + + + def on_drag_data_get(self, tv, drag_context, selection, info, time): + treeselection = tv.get_selection() + model, iter = treeselection.get_selected() + path = model.get_path(iter) + row = path.get_indices()[0] + dbg('on_drag_data_get :%s model:%s row:%s' % (selection, model, row)) + selection.set(selection.get_target(), 8, b'%d' % (row)) + + + def on_drag_data_received(self, tv, context, x, y, selection, info, time): + + model = tv.get_model() + sel_row = int(selection.get_data()) + sel_iter = model.get_iter(sel_row) + drop_info = tv.get_dest_row_at_pos(x, y) + data = list(model[sel_iter]) + if drop_info: + path, position = drop_info + dbg('on_drag_data_received: data: %s path:%s pos: %s' + % (data, path, position)) + iter = model.get_iter(path) + if (position == Gtk.TreeViewDropPosition.BEFORE + or position == Gtk.TreeViewDropPosition.INTO_OR_BEFORE): + model.insert_before(iter, data) + else: + model.insert_after(iter, data) + model.remove(sel_iter) + #else: + # model.append(data) + if context.get_actions() == Gdk.DragAction.MOVE: + context.finish(True, True, etime) + + return + + + def get_cfg_context_menu_items(self): + + self.reload_plugin_config() + plugin_keybindings = self.config_keyb.get('plugin_keybindings', {}) + + #if any entries in section else use default + cfg_keys = list(plugin_keybindings.keys()) + plugin_keybindings_len = len(list(plugin_keybindings.keys())) + + if not plugin_keybindings_len: + cfg_keys = list(self.DEFAULT_MENU.keys()) + #edit of context menu should be default + cfg_keys.append(PluginContextMenuAct) + + return cfg_keys + + + def set_context_menu_items(self, widget): + + liststore = widget.get_model() + #liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) + + keybindings = Keybindings() + key_action_map = self.keybindutil.get_all_act_to_keys() + key_action_desc_map = self.keybindutil.get_all_act_to_desc() + + plugin_keybindings = self.config_keyb.get('plugin_keybindings', {}) + + + cfg_keys = self.get_cfg_context_menu_items() + for action in cfg_keys: + + actkey = key_action_map.get(action, '') + + if len(plugin_keybindings): + position = int(plugin_keybindings.get(action,{}).get('position', 0)) + dbg('inserting at config pos:%s action:%s' % (position, action)) + else: + position = self.DEFAULT_MENU.get(action, -1) + dbg('inserting at default:%s action:%s' % (position, action)) + + self.left_select_actions[action] = True + + keyval = 0 + mask = 0 + try: + (keyval, mask) = keybindings._parsebinding(actkey) + except KeymapError: + dbg('no keyval for:%s' % actkey) + pass + + (parsed_action, count) = self.parse_ui_element(action) + actdesc = key_action_desc_map.get(parsed_action, '') + + if parsed_action and count: + actdesc = parsed_action #for ui elements action and desc same + saved_count = PluginContextMenu.ui_elements[parsed_action]['count'] + if count > saved_count: + PluginContextMenu.ui_elements[parsed_action]['count'] = count + + liststore.insert(position, [action, actdesc, keyval, mask]) + + + def set_act_menu_items(self, widget): + ## Keybindings tab + + self.treemodelfilter = None + right_kbsearch = self.plugin_builder.get_object('PluginContextMenuSearchRight') + self.keybind_filter_str = "" + + #lets hide whatever we can in nested scope + def filter_visible(model, treeiter, data): + 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): + act = "Disabled" + + self.keybind_filter_str = self.keybind_filter_str.lower() + searchtxt = (act + " " + keys + " " + desc).lower() + pos = searchtxt.find(self.keybind_filter_str) + if (pos >= 0): + dbg("filter find:%s in search text: %s" % + (self.keybind_filter_str, searchtxt)) + return True + + return False + + #local scoped func + 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): + dbg("filter search str: %s" % self.keybind_filter_str) + self.treemodelfilter.refilter() + + def on_search_refilter(widget): + dbg("refilter") + self.treemodelfilter.refilter() + + right_kbsearch.connect('key-press-event', on_search) + right_kbsearch.connect('backspace', on_search_refilter) + + liststore = widget.get_model() + liststore.set_sort_column_id(0, Gtk.SortType.ASCENDING) + liststore.append([ + PluginContextMenu.UI_SEPARATOR, + PluginContextMenu.UI_SEPARATOR, 0, 0]) + + #to keep separator and other items first + def compare(model, row1, row2, user_data): + sort_column, _ = model.get_sort_column_id() + value1 = model.get_value(row1, sort_column) + value2 = model.get_value(row2, sort_column) + if value1 < value2: + return -1 + elif value1 == value2: + return 0 + else: + return 1 + + liststore.set_sort_func(0, compare, None) + + keyb = Keybindings() + + key_action_map = self.keybindutil.get_all_act_to_keys() + key_action_desc_map = self.keybindutil.get_all_act_to_desc() + for action in key_action_map: + keyval = 0 + mask = 0 + actkey = key_action_map[action] + if actkey is not None and actkey != '': + try: + (keyval, mask) = keyb._parsebinding(actkey) + except KeymapError: + dbg('no keyval for:%s act:%s' % (actkey, action)) + pass + + if not action in self.left_select_actions: + desc = key_action_desc_map[action] + liststore.append([action, desc, keyval, mask]) + else: + dbg('skipping item:%s' % action) + + self.treemodelfilter = liststore.filter_new() + self.treemodelfilter.set_visible_func(filter_visible, key_action_map) + widget.set_model(self.treemodelfilter) + + + def parse_ui_element(self, actstr): + s = actstr.find('<') + e = actstr.rfind('>') + + count = 0 + if s == 0 and e > 0: + act = actstr[s:e+1] + if e+1 < len(actstr): + count = int(actstr[e+1:]) + dbg('action string: %s count: %s' % (act, count)) + return (act, count) + + dbg('action string: %s count: %s' % (actstr, count)) + return (actstr, count) + + + #ui items may have to be repeated like since we store + #items in config, we need to make these unique + def append_count_ui_act(self, actstr): + + (parsed_action, count) = self.parse_ui_element(actstr) + + ui_element = PluginContextMenu.ui_elements.get(parsed_action, None) + if ui_element: + ui_element['count'] += 1 + dbg('ui_elements: (%s)' % PluginContextMenu.ui_elements) + return parsed_action + str(ui_element['count']) + + return actstr + + + def on_button_mv_left(self, button, tview_left, tview_right): + #left<-right + right_select = tview_right.get_selection() + model, paths = right_select.get_selected_rows() + + for path in paths: + iter = model.get_iter(path) + # Remove the ListStore row referenced by iter + row = model.get(iter, 0,1,2,3) + index = path.get_indices()[0] + dbg('moving right->left item :%s ui index:%s' % (row, index)) + + # append number to ui elements so they are unique + row = list(row) + checked_actstr = self.append_count_ui_act(row[0]) + row[0] = checked_actstr + + tview_left.get_model().append(row) + + tview_right_filt = tview_right.get_model() + tview_right_filt_iter = tview_right_filt.convert_iter_to_child_iter(iter) + + actstr = row[0] + (parsed_action, count) = self.parse_ui_element(actstr) + + #ui items have a single entry + #are not moved they remain in list but are copied + if not (parsed_action in PluginContextMenu.ui_elements): + tview_right_filt.get_model().remove(tview_right_filt_iter) + + + #re-index the ui_elements + def refresh_ui_counts(self, actstr, store): + rindex = 0 + for row in store: + actstr = row[0] + (parsed_action, count) = self.parse_ui_element(actstr) + if parsed_action in PluginContextMenu.ui_elements: + edited_actstr = self.append_count_ui_act(parsed_action) + store[rindex][0] = edited_actstr + rindex += 1 + + + def on_button_mv_right(self, button, tview_left, tview_right): + #left->right + left_select = tview_left.get_selection() + model, paths = left_select.get_selected_rows() + + for path in paths: + iter = model.get_iter(path) + # Remove the ListStore row referenced by iter + row = model.get(iter, 0,1,2,3) + index = path.get_indices()[0] + dbg('moving right<-left item:%s ui index:%s' % (row, index)) + + tview_right_filt = tview_right.get_model() + + #ui items have a single entry ensure 1 ... + #are not added only exists + + actstr = row[0] + (parsed_action, count) = self.parse_ui_element(actstr) + + tview_left.get_model().remove(iter) + + if not (parsed_action in PluginContextMenu.ui_elements): + tview_right_filt.get_model().append(row) + else: + saved_count = PluginContextMenu.ui_elements[parsed_action]['count'] + if saved_count > 0: + PluginContextMenu.ui_elements[parsed_action]['count'] = 0 + self.refresh_ui_counts(actstr, model) + + + + def on_button_discard_clicked(self, button): + dbg('on_button_discard_clicked') + self.setup_data() + pass + + + def on_button_add_clicked(self, button): + position = 0 + self.config_keyb = {} + for row in self.store_left: + + dbg('adding to config: %s' % row[0]) + actstr = row[0] + desc = row[1] + key = row[2] + mods = row[3] + + accel = Gtk.accelerator_name(key, Gdk.ModifierType(mods)) + + config_key_sec = {} + + config_key_sec['accel'] = accel + config_key_sec['desc'] = desc + config_key_sec['position'] = position + self.config_keyb[actstr] = config_key_sec + + position += 1 + + dbg('saving config: [%s] (%s)' % (self.handler_name, self.config_keyb)) + + #Note: we can't keep the name of section 'keybindings' else it throw error + #in config.save which calls dict_diff, TODO: clean up old code + self.config.plugin_set_config(self.handler_name, + {'plugin_keybindings' : self.config_keyb}) + self.config.save() + + + #menu label that gets displayed to user + def get_menu_label(self, actstr): + desc = self.keybindutil.get_act_to_desc(actstr) + desc = desc.title() + return desc + + + #we get a callback for menu + def callback(self, menuitems, menu, terminal): + self.terminal = terminal + + cfg_keys = self.get_cfg_context_menu_items() + + for row in cfg_keys: + dbg('adding to menu: %s' % row) + actstr = row + item = self.menu_item(Gtk.ImageMenuItem, actstr) + if not item: + continue + + img_name = PluginContextMenu.MAP_ACTION_ICONS.get(actstr, None) + if (img_name): + image = Gtk.Image() + image.set_from_icon_name(img_name, Gtk.IconSize.MENU) + item.set_image(image) + if hasattr(item, 'set_always_show_image'): + item.set_always_show_image(True) + + menuitems.append(item) + + + #intercept function, handle as per PluginEventRegistry else + #allow to propogate to terminal key_ mappings + def on_keypress_external(self, widget, actstr): + + if self.event_registry.call_action_handlers(actstr): + dbg('handler found and called, not propogating event to terminal') + return True + + try: + terminal = self.terminal + terminal.on_keypress(terminal.get_window(), None, actstr) + except: + gerr(_("Action not registered by termial/plugin for action: %s") + % actstr) + + + #TODO from terminal_popup_menu may require futher cleanup + def menu_item(self, menutype, actstr): + + + #this won't have all keybinds, eg of plugins as all plugins + #are being loaded now so we will reload it again + self.keybindutil.load_merge_key_maps() + + key_action_map = self.keybindutil.get_all_act_to_keys() + + maskstr = key_action_map.get(actstr, '') + + (actstr, count) = self.parse_ui_element(actstr) + #check ui element using desc + menu_ui = PluginContextMenu.ui_elements.get(actstr, None) + if menu_ui: + dbg('adding ui_element: %s' % actstr) + return menu_ui['widget'].new() + + keybindings = Keybindings() + + keyval = 0 + mask = 0 + try: + (keyval, mask) = keybindings._parsebinding(maskstr) + except KeymapError: + dbg('no keyval :%s action:%s' % (maskstr, actstr)) + pass + + mask = Gdk.ModifierType(mask) + + menustr = self.get_menu_label(actstr) + + 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 ? + if maskstr: + mpos = maskstr.rfind(">") + #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 + if len(configaccelchar) == 1: + dbg("found accelchar in config:%s override:%s" + % (configaccelchar, accelchar)) + accelchar = configaccelchar + + dbg("action from config:%s for item:%s with shortcut accelchar:(%s)" + % (maskstr, menustr, accelchar)) + + item = menutype.new_with_mnemonic(_(menustr)) + + #TODO:REMOVE + #filter use cases for now, will be removed once + #all if-then else from terminal_popup_menu are + #cleaned and refactored + if not self.filter: + self.filter = Filter(self.terminal) + if not self.filter.filter_act(actstr, item): + item = None + return item + + item.connect('activate', self.on_keypress_external, actstr) + if mask: + item.add_accelerator("activate", + self.accelgrp, + Gdk.keyval_from_name(accelchar), + mask, + Gtk.AccelFlags.VISIBLE) + return item + + + def unload(self): + dbg("unloading") + for window in self.windows: + try: + window.disconnect_by_func(self.on_keypress) + except: + dbg("no connected signals") + + self.keybindutil.unbindkey( + [PluginContextMenuDesc , PluginContextMenuAct, "m"]) + + PluginContextMenu.event_registry.unregister( + PluginContextMenuAct, + self.lauch_pref_context_menu, + self.handler_name) + + + def get_term(self): + return Terminator().last_focused_term + + + def lauch_pref_context_menu(self, actstr): + dbg("open context menu preferences") + pe = PrefsEditor(self.get_term(), cur_page = 4) + pe.select_plugin_in_pref(self.handler_name) + + + def on_keypress(self, widget, event): + act = self.keybindutil.keyaction(event) + dbg("keyaction: (%s) (%s)" % (str(act), event.keyval)) + + if act == PluginContextMenuAct: + self.lauch_pref_context_menu(act) + return True + + + + +#TODO:REMOVE ,if-then based conditions from terminal_popup_menu, +#lets get them together first #to check all conditions and start cleaning up +# +#this also show cases how PluginEventRegistry comes handy and actions can be +#intercepted and action be taken we intercept these because these dont have +#a key_ mapping in terminal +# +#DO NOT ADD new dependencies here this will be REMOVED + +class Filter: + terminal = None + + def __init__(self, terminal): + + self.terminal = terminal + event_registry = plugin.PluginEventRegistry() + act_id = 'context_menu' + PluginContextMenu.event_registry.register('zoom', terminal.zoom, act_id) + PluginContextMenu.event_registry.register('maximise',terminal.maximise, act_id) + PluginContextMenu.event_registry.register('unzoom', terminal.unzoom, act_id) + PluginContextMenu.event_registry.register('open_debug_tab', + lambda x: terminal.emit('tab-new', True, terminal), + act_id) + + def filter_act(self, actstr, menuitem): + + #following actions were missing from config and from key_ mapping in + #terminal so to get them working temp + terminal = self.terminal + if actstr == 'copy': + menuitem.set_sensitive(terminal.vte.get_has_selection()) + return True + + acts = ['split_auto', 'split_horiz', 'split_vert', + 'new_tab', 'open_debug_tab', + 'zoom', 'maximise', + 'unzoom' ] + + if actstr in acts: + if not terminal.is_zoomed(): + if actstr in ['zoom', 'maximise']: + sensitive = not terminal.get_toplevel() == terminal.get_parent() + menuitem.set_sensitive(sensitive) + return True + elif actstr in ['open_debug_tab']: + return Terminator().debug_address + elif actstr in ['unzoom']: + return False + + elif actstr in ['unzoom']: + return True + else: + dbg('terminal zoomed remove actstr: %s' % actstr) + return False + + return True + diff --git a/terminatorlib/prefseditor.py b/terminatorlib/prefseditor.py index d5933c4b..bb295b8c 100755 --- a/terminatorlib/prefseditor.py +++ b/terminatorlib/prefseditor.py @@ -45,6 +45,7 @@ class PrefsEditor: term = None builder = None layouteditor = None + previous_plugin_selection = None previous_layout_selection = None previous_profile_selection = None colorschemevalues = {'black_on_yellow': 0, @@ -103,7 +104,12 @@ 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'), + keybindingnames = { + 'zoom' : _('Zoom terminal'), + 'unzoom' : _('Restore all terminals'), + 'maximise' : _('Maximize terminal'), + 'open_debug_tab' : _('Open Debug Tab'), + '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'), @@ -473,20 +479,15 @@ def on_search_refilter(widget): liststore = widget.get_model() 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} - self.keybindingnames = {**plugin_keyb_desc, **self.keybindingnames} - #dbg("appended actions %s names %s" % (keybindings, self.keybindingnames)) + keybindutil = KeyBindUtil() + act_to_key_map = keybindutil.get_all_act_to_keys() + self.keybindingnames = keybindutil.get_all_act_to_desc() - for keybinding in keybindings: + for keybinding in act_to_key_map: keyval = 0 mask = 0 - value = keybindings[keybinding] + value = act_to_key_map[keybinding] if value is not None and value != '': try: (keyval, mask) = self.keybindings._parsebinding(value) @@ -496,7 +497,7 @@ def on_search_refilter(widget): keyval, mask]) self.treemodelfilter = liststore.filter_new() - self.treemodelfilter.set_visible_func(filter_visible, keybindings) + self.treemodelfilter.set_visible_func(filter_visible, act_to_key_map) widget.set_model(self.treemodelfilter) ## Plugins tab @@ -519,6 +520,19 @@ def on_search_refilter(widget): if len(self.pluginiters) > 0: selection.select_iter(liststore.get_iter_first()) + #this function will allow plugins to directly select their settings in prefs menu + def select_plugin_in_pref(self, plugin_name): + guiget = self.builder.get_object + widget = guiget('pluginlist') + liststore = widget.get_model() + + count = 0 + for plugin_row in liststore: + if plugin_row[0] == plugin_name: + widget.set_cursor(count) + break + count += 1 + def set_profile_values(self, profile): """Update the profile values for a given profile""" self.config.set_profile(profile) @@ -1745,7 +1759,13 @@ def on_plugin_selection_changed(self, selection): selection.select_iter(liststore.get_iter_first()) return plugin = listmodel.get_value(rowiter, 0) - self.set_plugin(plugin) + + #in on_plugin_selection_changed(self, selection) calls set_plugin but + #on_plugin_toggled calls init on plugin, this causes, set_plugin + #to be called when plugin init hasn't happened if plugin is selected but + #not toggled via check box + self.set_plugin(self.previous_plugin_selection, visible = False) + self.set_plugin(plugin, visible = True) self.previous_plugin_selection = plugin widget = self.builder.get_object('plugintogglebutton') @@ -1759,7 +1779,11 @@ def on_plugin_toggled(self, cell, path): if not self.plugins[plugin]: # Plugin is currently disabled, load it self.registry.enable(plugin) + #show plugin gui after plugin init + self.set_plugin(plugin, visible = True) else: + #remove plugin gui before plugin deinit + self.set_plugin(plugin, visible = False) # Plugin is currently enabled, unload it self.registry.disable(plugin) @@ -1771,11 +1795,34 @@ def on_plugin_toggled(self, cell, path): self.config['enabled_plugins'] = enabled_plugins self.config.save() - def set_plugin(self, plugin): + def set_plugin(self, plugin, visible = False): """Show the preferences for the selected plugin, if any""" 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 + self.update_plugin_ui(plugin, visible) + + def update_plugin_ui(self, plugin, visible = False): + if not plugin: + return + + # let the plugin construct an UI for itself + # this will allow the plugins to inject their UI + + dbg("plugin: %s" % plugin) + plugin_instance = self.registry.get_plugin_instance(plugin) + + #the 2nd rhs child of the pane would be set by plugins as + #per their ui needs + plugin_hpaned3 = self.builder.get_object('hpaned3') + + #call the plugin instance gui function + if ('update_gui' in dir(plugin_instance)): + if self.registry.is_enabled(plugin): + plugin_instance.update_gui(plugin_hpaned3, visible) + else: + dbg('non existant func:update_gui in plugin, skipping: %s ...' + % plugin) def on_profile_name_edited(self, cell, path, newtext): """Update a profile name""" @@ -1911,14 +1958,11 @@ def on_cellrenderer_accel_edited(self, liststore, path, key, mods, _code): current_binding = liststore.get_value(liststore.get_iter(path), 0) parsed_accel = Gtk.accelerator_parse(accel) - 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() + act_to_key_map = keybindutil.get_all_act_to_keys() duplicate_bindings = [] - for conf_binding, conf_accel in keybindings.items(): + for conf_binding, conf_accel in act_to_key_map.items(): if conf_accel is None: continue diff --git a/terminatorlib/terminal.py b/terminatorlib/terminal.py index 7ed248b0..d54e252c 100644 --- a/terminatorlib/terminal.py +++ b/terminatorlib/terminal.py @@ -947,14 +947,18 @@ def on_group_button_press(self, widget, event): dbg('on_group_button_press: unknown group button interaction') return False - def on_keypress(self, widget, event): + + #adding optional mapping field, incase we know the action + + def on_keypress(self, widget, event, mapping = None): """Handler for keyboard events""" - if not event: - dbg('Called on %s with no event' % widget) + if not (event or mapping): + dbg('Called on %s with no event or mapping' % widget) return False # FIXME: Does keybindings really want to live in Terminator()? - mapping = self.terminator.keybindings.lookup(event) + if not mapping: + mapping = self.terminator.keybindings.lookup(event) if mapping == "hide_window": return False @@ -965,7 +969,8 @@ def on_keypress(self, widget, event): # handle the case where user has re-bound copy to ctrl+ # we only copy if there is a selection otherwise let it fall through # to ^ - if (mapping == "copy" and event.get_state() & Gdk.ModifierType.CONTROL_MASK): + if (mapping == "copy" and + (event and (event.get_state() & Gdk.ModifierType.CONTROL_MASK))): if self.vte.get_has_selection(): getattr(self, "key_" + mapping)() return True diff --git a/terminatorlib/terminal_popup_menu.py b/terminatorlib/terminal_popup_menu.py index dc7f95f7..8449840c 100644 --- a/terminatorlib/terminal_popup_menu.py +++ b/terminatorlib/terminal_popup_menu.py @@ -156,6 +156,7 @@ def show(self, widget, event=None): menu.append(Gtk.SeparatorMenuItem()) + """ item = self.menu_item(Gtk.ImageMenuItem, 'copy', '_Copy') item.connect('activate', lambda x: terminal.vte.copy_clipboard()) item.set_sensitive(terminal.vte.get_has_selection()) @@ -176,13 +177,12 @@ def show(self, widget, event=None): if not terminal.is_zoomed(): item = self.menu_item(Gtk.ImageMenuItem, 'split_auto', 'Split _Auto') - """ image = Gtk.Image() image.set_from_icon_name(APP_NAME + '_auto', Gtk.IconSize.MENU) item.set_image(image) if hasattr(item, 'set_always_show_image'): item.set_always_show_image(True) - """ + item.connect('activate', lambda x: terminal.emit('split-auto', self.terminal.get_cwd())) menu.append(item) @@ -249,6 +249,7 @@ def show(self, widget, event=None): menu.append(item) menu.append(Gtk.SeparatorMenuItem()) + """ if self.config['show_titlebar'] == False: item = Gtk.MenuItem.new_with_mnemonic(_('Grouping'))