diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/Changelog.md b/Changelog.md index f64f0b1..14c05ea 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,12 @@ # Changelog +## v0.3.2 (2018-03-13) +* Added Ctrl-Esc to cancel tab switching, causes the initial tab (before + switching began) to be active again +* Prevent all input during tab switching, if the tab switching window is + visible, instead of cancelling switching +* Fixed copyright notices + ## v0.3.1 (2017-10-17) * Show a debug message if the settings schema could not be loaded * Fixed division by zero error diff --git a/README.md b/README.md index d048ddc..2ba1829 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Switch between document tabs using Ctrl+Tab / Ctrl+Shift+Tab and Ctrl+PageUp / Ctrl+PageDown -v0.3.1 +v0.3.2 All bug reports, feature requests and miscellaneous comments are welcome at the [project issue tracker][]. @@ -30,16 +30,19 @@ gedit 2 is [v0.1.2][]. Ctrl+Shift+Tab - Switch tabs in most recently used order. * Ctrl+Page Up / - Ctrl+Page Down - Switch tabs in tabbar order. + Ctrl+Page Down - Switch tabs in tab row order. + +Hold down Ctrl to continue tab switching. Press +Esc while switching to cancel and return to the initial tab. ## Preferences In gedit 3.4 or later, the plugin supports these preferences: -* `Use tabbar order for Ctrl+Tab / Ctrl+Shift+Tab` - Change +* `Use tab row order for Ctrl+Tab / Ctrl+Shift+Tab` - Change Ctrl+Tab / Ctrl+Shift+Tab to switch tabs in - tabbar order instead of most recently used order. + tab row order instead of most recently used order. ## Development @@ -57,7 +60,7 @@ Inspired by: ## License -Copyright © 2010-2014, 2016-2017 Jeffery To +Copyright © 2010-2013, 2017-2018 Jeffery To Available under GNU General Public License version 3 diff --git a/controlyourtabs.plugin b/controlyourtabs.plugin index 2b6a22d..3ae8816 100644 --- a/controlyourtabs.plugin +++ b/controlyourtabs.plugin @@ -6,6 +6,6 @@ IAge=3 Name=Control Your Tabs Description=Switch between document tabs using Ctrl+Tab / Ctrl+Shift+Tab and Ctrl+PageUp / Ctrl+PageDown Authors=Jeffery To -Copyright=Copyright © 2010-2014, 2016-2017 Jeffery To +Copyright=Copyright © 2010-2013, 2017-2018 Jeffery To Website=https://github.com/jefferyto/gedit-control-your-tabs -Version=0.3.1 +Version=0.3.2 diff --git a/controlyourtabs.plugin.python2 b/controlyourtabs.plugin.python2 index 3523085..cf903e8 100644 --- a/controlyourtabs.plugin.python2 +++ b/controlyourtabs.plugin.python2 @@ -6,6 +6,6 @@ IAge=3 Name=Control Your Tabs Description=Switch between document tabs using Ctrl+Tab / Ctrl+Shift+Tab and Ctrl+PageUp / Ctrl+PageDown Authors=Jeffery To -Copyright=Copyright © 2010-2014, 2016-2017 Jeffery To +Copyright=Copyright © 2010-2013, 2017-2018 Jeffery To Website=https://github.com/jefferyto/gedit-control-your-tabs -Version=0.3.1 +Version=0.3.2 diff --git a/controlyourtabs/__init__.py b/controlyourtabs/__init__.py index bcf3eac..ef9772c 100644 --- a/controlyourtabs/__init__.py +++ b/controlyourtabs/__init__.py @@ -3,7 +3,7 @@ # __init__.py # This file is part of Control Your Tabs, a plugin for gedit # -# Copyright (C) 2010-2014, 2016-2017 Jeffery To +# Copyright (C) 2010-2013, 2017-2018 Jeffery To # https://github.com/jefferyto/gedit-control-your-tabs # # This program is free software: you can redistribute it and/or modify @@ -23,100 +23,51 @@ gi.require_version('Gtk', '3.0') gi.require_version('Gedit', '3.0') -import gettext import math import os.path -from gi.repository import GObject, GLib, Gtk, Gdk, GdkPixbuf, Gio, GtkSource, Gedit, PeasGtk -from xml.sax.saxutils import escape +from functools import wraps +from gi.repository import GObject, GLib, Gtk, Gdk, GdkPixbuf, Gio, Gedit, PeasGtk from .utils import connect_handlers, disconnect_handlers +from . import keyinfo, log, tabinfo, tabinfo_pre312 -GETTEXT_PACKAGE = 'gedit-control-your-tabs' BASE_PATH = os.path.dirname(os.path.realpath(__file__)) LOCALE_PATH = os.path.join(BASE_PATH, 'locale') try: - gettext.bindtextdomain(GETTEXT_PACKAGE, LOCALE_PATH) - _ = lambda s: gettext.dgettext(GETTEXT_PACKAGE, s); + import gettext + gettext.bindtextdomain('gedit-control-your-tabs', LOCALE_PATH) + _ = lambda s: gettext.dgettext('gedit-control-your-tabs', s) except: _ = lambda s: s +try: + debug_plugin_message = Gedit.debug_plugin_message +except: # before gedit 3.4 + debug_plugin_message = lambda fmt, *fmt_args: None -class ControlYourTabsPlugin(GObject.Object, Gedit.WindowActivatable, PeasGtk.Configurable): - __gtype_name__ = 'ControlYourTabsPlugin' - window = GObject.property(type=Gedit.Window) +class ControlYourTabsWindowActivatable(GObject.Object, Gedit.WindowActivatable): - SELECTED_TAB_COLUMN = 3 + __gtype_name__ = 'ControlYourTabsWindowActivatable' - META_KEYS = ['Shift_L', 'Shift_R', - 'Control_L', 'Control_R', - 'Meta_L', 'Meta_R', - 'Super_L', 'Super_R', - 'Hyper_L', 'Hyper_R', - 'Alt_L', 'Alt_R'] - # Compose, Apple? + window = GObject.property(type=Gedit.Window) # before pygobject 3.2, lowercase 'p' MAX_TAB_WINDOW_ROWS = 9 MAX_TAB_WINDOW_HEIGHT_PERCENTAGE = 0.5 - # based on MAX_DOC_NAME_LENGTH in gedit-documents-panel.c - MAX_DOC_NAME_LENGTH = 60 - - # based on formats in tab_get_name() in gedit-documents-panel.c < 3.12 - TAB_NAME_GEDITPANEL_FORMATS = { - 'modified': "%s", - 'readonly': " [%s]" - } - - # based on formats in document_row_sync_tab_name_and_icon() in gedit-documents-panel.c >= 3.12 - TAB_NAME_LISTBOX_FORMATS = { - 'modified': "%s", - 'readonly': " [%s]" - } - - # based on switch statement in _gedit_tab_get_icon() in gedit-tab.c < 3.12 - TAB_STATE_TO_STOCK_ICON = { - Gedit.TabState.STATE_LOADING: Gtk.STOCK_OPEN, - Gedit.TabState.STATE_REVERTING: Gtk.STOCK_REVERT_TO_SAVED, - Gedit.TabState.STATE_SAVING: Gtk.STOCK_SAVE, - Gedit.TabState.STATE_PRINTING: Gtk.STOCK_PRINT, - Gedit.TabState.STATE_PRINT_PREVIEWING: Gtk.STOCK_PRINT_PREVIEW, - Gedit.TabState.STATE_SHOWING_PRINT_PREVIEW: Gtk.STOCK_PRINT_PREVIEW, - Gedit.TabState.STATE_LOADING_ERROR: Gtk.STOCK_DIALOG_ERROR, - Gedit.TabState.STATE_REVERTING_ERROR: Gtk.STOCK_DIALOG_ERROR, - Gedit.TabState.STATE_SAVING_ERROR: Gtk.STOCK_DIALOG_ERROR, - Gedit.TabState.STATE_GENERIC_ERROR: Gtk.STOCK_DIALOG_ERROR, - Gedit.TabState.STATE_EXTERNALLY_MODIFIED_NOTIFICATION: Gtk.STOCK_DIALOG_WARNING - } - - # based on switch statement in _gedit_tab_get_icon() in gedit-tab.c >= 3.12 - TAB_STATE_TO_NAMED_ICON = { - Gedit.TabState.STATE_PRINTING: 'printer-printing-symbolic', - Gedit.TabState.STATE_PRINT_PREVIEWING: 'printer-symbolic', - Gedit.TabState.STATE_SHOWING_PRINT_PREVIEW: 'printer-symbolic', - Gedit.TabState.STATE_LOADING_ERROR: 'dialog-error-symbolic', - Gedit.TabState.STATE_REVERTING_ERROR: 'dialog-error-symbolic', - Gedit.TabState.STATE_SAVING_ERROR: 'dialog-error-symbolic', - Gedit.TabState.STATE_GENERIC_ERROR: 'dialog-error-symbolic', - Gedit.TabState.STATE_EXTERNALLY_MODIFIED_NOTIFICATION: 'dialog-warning-symbolic' - } - - SETTINGS_SCHEMA_ID = 'com.thingsthemselves.gedit.plugins.controlyourtabs' - - USE_TABBAR_ORDER = 'use-tabbar-order' - - - # gedit plugin api def __init__(self): GObject.Object.__init__(self) def do_activate(self): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self.window)) + window = self.window - notebooks = {} + tab_models = {} - tabwin = Gtk.Window(type=Gtk.WindowType.POPUP) + tabwin = Gtk.Window.new(Gtk.WindowType.POPUP) tabwin.set_transient_for(window) tabwin.set_destroy_with_parent(True) tabwin.set_accept_focus(False) @@ -127,25 +78,26 @@ def do_activate(self): tabwin.set_skip_taskbar_hint(False) tabwin.set_skip_pager_hint(False) - sw = Gtk.ScrolledWindow() + sw = Gtk.ScrolledWindow.new(None, None) sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sw.show() tabwin.add(sw) - view = Gtk.TreeView() + view = Gtk.TreeView.new() view.set_enable_search(False) view.set_headers_visible(False) view.show() sw.add(view) - col = Gtk.TreeViewColumn(_("Documents")) + col = Gtk.TreeViewColumn.new() + col.set_title(_("Documents")) col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - icon_cell = Gtk.CellRendererPixbuf() - name_cell = Gtk.CellRendererText() - space_cell = Gtk.CellRendererPixbuf() + icon_cell = Gtk.CellRendererPixbuf.new() + name_cell = Gtk.CellRendererText.new() + space_cell = Gtk.CellRendererPixbuf.new() col.pack_start(icon_cell, False) col.pack_start(name_cell, True) @@ -167,59 +119,69 @@ def do_activate(self): except AttributeError: is_side_panel_stack = False else: - is_side_panel_stack = isinstance(window.get_side_panel(), GtkStack) # since 3.12 + is_side_panel_stack = isinstance(window.get_side_panel(), GtkStack) # since gedit 3.12 - self._tabbing = False - self._paging = False - self._switching = False - self._ctrl_l = False - self._ctrl_r = False + if log.query(log.DEBUG): + debug_plugin_message(log.format("using %s tab names/icons", "current" if is_side_panel_stack else "pre-3.12")) + + self._is_switching = False + self._is_tabwin_visible = False + self._is_control_held = keyinfo.default_control_held() + self._initial_tab = None self._multi = None - self._notebooks = notebooks + self._tab_models = tab_models self._tabwin = tabwin self._view = view self._sw = sw self._icon_cell = icon_cell self._space_cell = space_cell self._tabwin_resize_id = None - self._settings = self._get_settings() - self._is_side_panel_stack = is_side_panel_stack + self._settings = get_settings() + self._tabinfo = tabinfo if is_side_panel_stack else tabinfo_pre312 tab = window.get_active_tab() + if tab: - self._setup(window, tab, notebooks, view) + if log.query(log.DEBUG): + debug_plugin_message(log.format("found active tab %s, setting up now", tab)) + + self.setup(window, tab, tab_models) + if self._multi: - self.on_window_active_tab_changed(window, tab, notebooks, view) + self.active_tab_changed(tab, tab_models[tab.get_parent()]) + else: - connect_handlers(self, window, ['tab-added'], 'window') + if log.query(log.DEBUG): + debug_plugin_message(log.format("waiting for new tab")) + + connect_handlers(self, window, ['tab-added'], 'setup', tab_models) def do_deactivate(self): - window = self.window + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self.window)) - disconnect_handlers(self, window) + multi = self._multi + tab_models = self._tab_models - if self._multi: - disconnect_handlers(self, self._multi) + for notebook in list(tab_models.keys()): + self.untrack_notebook(notebook, tab_models) - notebooks = self._notebooks - for notebook in notebooks: - disconnect_handlers(self, notebooks[notebook][1]) + if multi: + disconnect_handlers(self, multi) - for doc in window.get_documents(): - disconnect_handlers(self, Gedit.Tab.get_from_document(doc)) + disconnect_handlers(self, self.window) - self._cancel_tabwin_resize() - self._end_switching() + self.cancel_tabwin_resize() + self.end_switching() self._tabwin.destroy() - self._tabbing = None - self._paging = None - self._switching = None - self._ctrl_l = None - self._ctrl_r = None + self._is_switching = None + self._is_tabwin_visible = None + self._is_control_held = None + self._initial_tab = None self._multi = None - self._notebooks = None + self._tab_models = None self._tabwin = None self._view = None self._sw = None @@ -227,410 +189,506 @@ def do_deactivate(self): self._space_cell = None self._tabwin_resize_id = None self._settings = None - self._is_side_panel_stack = None + self._tabinfo = None def do_update_state(self): pass - # settings ui + # plugin setup - def do_create_configure_widget(self): - settings = self._get_settings() - if settings: - widget = Gtk.CheckButton(_("Use tabbar order for Ctrl+Tab / Ctrl+Shift+Tab")) - widget.set_active(settings.get_boolean(self.USE_TABBAR_ORDER)) - connect_handlers(self, widget, ['toggled'], 'configure_check_button', settings) - connect_handlers(self, settings, ['changed::' + self.USE_TABBAR_ORDER], 'configure_settings', widget) - else: - widget = Gtk.Box() - widget.add(Gtk.Label(_("Sorry, no preferences are available for this version of gedit."))) - widget.set_border_width(5) - return widget + def on_setup_tab_added(self, window, tab, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", window, tab)) - def on_configure_check_button_toggled(self, widget, settings): - settings.set_boolean(self.USE_TABBAR_ORDER, widget.get_active()) + disconnect_handlers(self, window) - def on_configure_settings_changed_use_tabbar_order(self, settings, prop, widget): - widget.set_active(settings.get_boolean(self.USE_TABBAR_ORDER)) + self.setup(window, tab, tab_models) + def setup(self, window, tab, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", window, tab)) - # plugin setup + icon_size = self._tabinfo.get_tab_icon_size(tab) - def on_window_tab_added(self, window, tab): - disconnect_handlers(self, window) - self._setup(window, tab, self._notebooks, self._view) + self._icon_cell.set_fixed_size(icon_size, icon_size) + self._space_cell.set_fixed_size(icon_size, icon_size) - def _setup(self, window, tab, notebooks, view): - if self._is_side_panel_stack: - is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup(Gtk.IconSize.MENU) - else: - is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup_for_settings(tab.get_settings(), Gtk.IconSize.MENU) + multi = get_multi_notebook(tab) - self._icon_cell.set_fixed_size(icon_size_height, icon_size_height) - self._space_cell.set_fixed_size(icon_size_height, icon_size_height) + if not multi: + if log.query(log.ERROR): + debug_plugin_message(log.format("cannot find multi notebook from %s", tab)) - multi = self._get_multi_notebook(tab) + return - if multi: - self._multi = multi + connect_handlers( + self, multi, + [ + 'notebook-added', + 'notebook-removed', + 'tab-added', + 'tab-removed' + ], + 'multi_notebook', + tab_models + ) + connect_handlers( + self, window, + [ + 'active-tab-changed', + 'key-press-event', + 'key-release-event', + 'focus-out-event', + 'configure-event' + ], + 'window', + tab_models + ) - for doc in window.get_documents(): - self.on_multi_notebook_notebook_added(multi, Gedit.Tab.get_from_document(doc).get_parent(), notebooks, view) + self._multi = multi - connect_handlers(self, multi, ['notebook-added', 'notebook-removed', 'tab-added', 'tab-removed'], 'multi_notebook', notebooks, view) - connect_handlers(self, window, ['tabs-reordered', 'active-tab-changed', 'key-press-event', 'key-release-event', 'focus-out-event', 'configure-event'], 'window', notebooks, view) + for document in window.get_documents(): + notebook = Gedit.Tab.get_from_document(document).get_parent() + self.track_notebook(notebook, tab_models) - else: - try: - Gedit.debug_plugin_message("cannot find multi notebook from %s", tab) - except AttributeError: - pass - - - # signal handlers / main logic - - def on_multi_notebook_notebook_added(self, multi, notebook, notebooks, view): - if notebook not in notebooks: - model = Gtk.ListStore(GdkPixbuf.Pixbuf, str, Gedit.Tab, 'gboolean') - connect_handlers(self, model, ['row-inserted', 'row-deleted', 'row-changed'], 'model', view, view.get_selection()) - notebooks[notebook] = ([], model) - - for tab in notebook.get_children(): - self.on_multi_notebook_tab_added(multi, notebook, tab, notebooks, view) - - def on_multi_notebook_notebook_removed(self, multi, notebook, notebooks, view): - if notebook in notebooks: - for tab in notebook.get_children(): - self.on_multi_notebook_tab_removed(multi, notebook, tab, notebooks, view) - - stack, model = notebooks[notebook] - if view.get_model() is model: - view.set_model(None) - disconnect_handlers(self, model) - del notebooks[notebook] - - def on_multi_notebook_tab_added(self, multi, notebook, tab, notebooks, view): - stack, model = notebooks[notebook] - if tab not in stack: - stack.append(tab) - model.append((self._get_tab_icon(tab), self._get_tab_name(tab), tab, False)) - connect_handlers(self, tab, ['notify::name', 'notify::state'], self.on_sync_icon_and_name, notebooks) - - def on_multi_notebook_tab_removed(self, multi, notebook, tab, notebooks, view): - stack, model = notebooks[notebook] - if tab in stack: - disconnect_handlers(self, tab) - model.remove(model.get_iter(stack.index(tab))) - stack.remove(tab) - - def on_window_tabs_reordered(self, window, notebooks, view): - multi = self._multi - tab = window.get_active_tab() - new_notebook = tab.get_parent() - if tab not in notebooks[new_notebook][0]: - old_notebook = None - for notebook in notebooks: - if tab in notebooks[notebook][0]: - old_notebook = notebook - break - if old_notebook: - self.on_multi_notebook_tab_removed(multi, old_notebook, tab, notebooks, view) - self.on_multi_notebook_tab_added(multi, new_notebook, tab, notebooks, view) - - def on_window_active_tab_changed(self, window, tab, notebooks, view): - if not self._switching: - stack, model = notebooks[tab.get_parent()] - - if view.get_model() is not model: - view.set_model(model) - self._schedule_tabwin_resize() - - for row in model: - row[self.SELECTED_TAB_COLUMN] = False - - if not self._tabbing and not self._paging: - if tab in stack: - model.move_after(model.get_iter(stack.index(tab)), None) - stack.remove(tab) - else: - model.insert(0, (self._get_tab_icon(tab), self._get_tab_name(tab), tab, False)) - - stack.insert(0, tab) - model[0][self.SELECTED_TAB_COLUMN] = True - else: - model[stack.index(tab)][self.SELECTED_TAB_COLUMN] = True + # tracking notebooks / tabs - def on_window_key_press_event(self, window, event, notebooks, view): - key = Gdk.keyval_name(event.keyval) - state = event.state & Gtk.accelerator_get_default_mod_mask() + def track_notebook(self, notebook, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, notebook)) - if key == 'Control_L': - self._ctrl_l = True + if notebook in tab_models: + if log.query(log.WARNING): + debug_plugin_message(log.format("already tracking notebook")) - if key == 'Control_R': - self._ctrl_r = True + return - if key in self.META_KEYS or not state & Gdk.ModifierType.CONTROL_MASK: - return False + tab_model = ControlYourTabsTabModel(self._tabinfo) - is_ctrl = state == Gdk.ModifierType.CONTROL_MASK - is_ctrl_shift = state == Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK - is_tab_key = key in ['ISO_Left_Tab', 'Tab'] - is_page_key = key in ['Page_Up', 'Page_Down'] - is_up_dir = key in ['ISO_Left_Tab', 'Page_Up'] + connect_handlers( + self, tab_model, + [ + 'row-inserted', + 'row-deleted', + 'row-changed' + ], + self.on_tab_model_row_changed + ) + connect_handlers( + self, tab_model, + [ + 'selected-path-changed' + ], + 'tab_model' + ) - if not (((is_ctrl or is_ctrl_shift) and is_tab_key) or (is_ctrl and is_page_key)): - self._end_switching() - return False + tab_models[notebook] = tab_model - cur = window.get_active_tab() - if cur: - settings = self._settings - notebook = cur.get_parent() - stack, model = notebooks[notebook] - is_tabbing = is_tab_key and not (settings and settings.get_boolean(self.USE_TABBAR_ORDER)) - tabs = stack if is_tabbing else notebook.get_children() - tlen = len(tabs) + for tab in notebook.get_children(): + self.track_tab(tab, tab_model) - if tlen > 1 and cur in tabs: - i = -1 if is_up_dir else 1 - next = tabs[(tabs.index(cur) + i) % tlen] + def untrack_notebook(self, notebook, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, notebook)) - model[stack.index(cur)][self.SELECTED_TAB_COLUMN] = False - model[stack.index(next)][self.SELECTED_TAB_COLUMN] = True + if notebook not in tab_models: + if log.query(log.WARNING): + debug_plugin_message(log.format("not tracking notebook")) - if is_tabbing: - tabwin = self._tabwin + return - if not self._tabbing: - view.scroll_to_cell(Gtk.TreePath.new_first(), None, True, 0, 0) - tabwin.show_all() - else: - tabwin.present_with_time(event.time) + tab_model = tab_models[notebook] - self._tabbing = True - else: - self._paging = True + for tab in notebook.get_children(): + self.untrack_tab(tab, tab_model) - self._switching = True - window.set_active_tab(next) - self._switching = False + if self.is_active_view_model(tab_model): + self.set_active_view_model(None) - return True + disconnect_handlers(self, tab_model) - def on_window_key_release_event(self, window, event, notebooks, view): - key = Gdk.keyval_name(event.keyval) + del tab_models[notebook] - if key == 'Control_L': - self._ctrl_l = False + def track_tab(self, tab, tab_model): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, tab)) - if key == 'Control_R': - self._ctrl_r = False + if tab in tab_model: + if log.query(log.WARNING): + debug_plugin_message(log.format("already tracking tab")) - if not self._ctrl_l and not self._ctrl_r: - self._end_switching() + return - def on_window_focus_out_event(self, window, event, notebooks, view): - self._end_switching() + tab_model.append(tab) - def on_window_configure_event(self, window, event, notebooks, view): - self._schedule_tabwin_resize() + connect_handlers( + self, tab, + [ + 'notify::name', + 'notify::state' + ], + self.on_tab_notify_name_state, + tab_model + ) - def _end_switching(self): - if self._tabbing or self._paging: - self._tabbing = False - self._paging = False - self._switching = False - self._ctrl_l = False - self._ctrl_r = False - self._tabwin.hide() + def untrack_tab(self, tab, tab_model): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, tab)) - window = self.window - tab = window.get_active_tab() - if tab: - self.on_window_active_tab_changed(window, tab, self._notebooks, self._view) + if tab == self._initial_tab: + if log.query(log.DEBUG): + debug_plugin_message(log.format("tab is initial tab, clearing")) - def on_model_row_inserted(self, model, path, iter, view, sel): - if view.get_model() is model: - self._schedule_tabwin_resize() + self._initial_tab = None - def on_model_row_deleted(self, model, path, view, sel): - if view.get_model() is model: - self._schedule_tabwin_resize() + if tab not in tab_model: + if log.query(log.WARNING): + debug_plugin_message(log.format("not tracking tab")) + + return + + disconnect_handlers(self, tab) + + tab_model.remove(tab) + + def active_tab_changed(self, tab, tab_model): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, tab)) + + if not self._is_switching: + tab_model.move_after(tab) + + tab_model.select(tab) + + if not self.is_active_view_model(tab_model): + self.set_active_view_model(tab_model) + self.schedule_tabwin_resize() + + + # signal handlers + + def on_multi_notebook_notebook_added(self, multi, notebook, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, notebook)) + + self.track_notebook(notebook, tab_models) + + def on_multi_notebook_notebook_removed(self, multi, notebook, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, notebook)) + + self.untrack_notebook(notebook, tab_models) + + def on_multi_notebook_tab_added(self, multi, notebook, tab, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, %s", self.window, notebook, tab)) + + self.track_tab(tab, tab_models[notebook]) + + def on_multi_notebook_tab_removed(self, multi, notebook, tab, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, %s", self.window, notebook, tab)) + + self.untrack_tab(tab, tab_models[notebook]) + + def on_window_active_tab_changed(self, window, tab, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", window, tab)) + + self.active_tab_changed(tab, tab_models[tab.get_parent()]) + + def on_window_key_press_event(self, window, event, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, key=%s", window, Gdk.keyval_name(event.keyval))) + + self._is_control_held = keyinfo.update_control_held(event, self._is_control_held, True) + + return self.key_press_event(event) + + def on_window_key_release_event(self, window, event, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, key=%s", self.window, Gdk.keyval_name(event.keyval))) + + self._is_control_held = keyinfo.update_control_held(event, self._is_control_held, False) + + if not any(self._is_control_held): + if log.query(log.INFO): + debug_plugin_message(log.format("no control keys held down")) + + self.end_switching() - def on_model_row_changed(self, model, path, iter, view, sel): - if view.get_model() is model: - if model[path][self.SELECTED_TAB_COLUMN]: - sel.select_path(path) - view.scroll_to_cell(path, None, True, 0.5, 0) - else: - sel.unselect_path(path) - self._schedule_tabwin_resize() - - def on_sync_icon_and_name(self, tab, pspec, notebooks): - stack, model = notebooks[tab.get_parent()] - if tab in stack: - path = stack.index(tab) - model[path][0] = self._get_tab_icon(tab) - model[path][1] = self._get_tab_name(tab) - - - # tab name / icon - - # based on - # < 3.12: tab_get_name() in gedit-documents-panel.c - # >= 3.12: doc_get_name() and document_row_sync_tab_name_and_icon() in gedit-documents-panel.c - def _get_tab_name(self, tab): - doc = tab.get_document() - name = doc.get_short_name_for_display() - docname = Gedit.utils_str_middle_truncate(name, self.MAX_DOC_NAME_LENGTH) - tab_name_formats = self.TAB_NAME_LISTBOX_FORMATS if self._is_side_panel_stack else self.TAB_NAME_GEDITPANEL_FORMATS - - if not doc.get_modified(): - tab_name = escape(docname) else: - tab_name = tab_name_formats['modified'] % escape(docname) + if log.query(log.DEBUG): + debug_plugin_message(log.format("one or more control keys held down")) - try: - file = doc.get_file() - is_readonly = GtkSource.File.is_readonly(file) - except AttributeError: - is_readonly = doc.get_readonly() # deprecated since 3.18 + def on_window_focus_out_event(self, window, event, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", window)) + + self.end_switching() + + def on_window_configure_event(self, window, event, tab_models): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", window)) + + self.schedule_tabwin_resize() + + def on_tab_notify_name_state(self, tab, pspec, tab_model): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, tab)) + + tab_model.update(tab) + + def on_tab_model_row_changed(self, tab_model, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self.window, path)) + + if not self.is_active_view_model(tab_model): + if log.query(log.DEBUG): + debug_plugin_message(log.format("tab model not active")) + + return + + self.schedule_tabwin_resize() + + def on_tab_model_selected_path_changed(self, tab_model, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self.window, path)) - if is_readonly: - tab_name += tab_name_formats['readonly'] % escape(_("Read-Only")) + if not self.is_active_view_model(tab_model): + if log.query(log.DEBUG): + debug_plugin_message(log.format("tab model not active")) - return tab_name + return + + self.set_view_selection(path) + + + # tree view + + def is_active_view_model(self, tab_model): + model = tab_model.model if tab_model else None + return self._view.get_model() is model + + def set_active_view_model(self, tab_model): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self.window, tab_model)) + + model = None + selected_path = None + + if tab_model: + model = tab_model.model + selected_path = tab_model.get_selected_path() + + self._view.set_model(model) + self.set_view_selection(selected_path) + + def set_view_selection(self, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self.window, path)) + + view = self._view + selection = view.get_selection() + + if path: + selection.select_path(path) + view.scroll_to_cell(path, None, True, 0.5, 0) - def _get_tab_icon(self, tab): - if self._is_side_panel_stack: - icon = self._get_named_tab_icon(tab) else: - icon = self._get_stock_tab_icon(tab) - return icon - - # based on _gedit_tab_get_icon() in gedit-tab.c >= 3.12 - def _get_named_tab_icon(self, tab): - icon_name = None - pixbuf = None - state = tab.get_state() - - if state in self.TAB_STATE_TO_NAMED_ICON: - icon_name = self.TAB_STATE_TO_NAMED_ICON[state] - - if icon_name: - theme = Gtk.IconTheme.get_for_screen(tab.get_screen()) - is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup(Gtk.IconSize.MENU) - pixbuf = Gtk.IconTheme.load_icon(theme, icon_name, icon_size_height, 0) - - return pixbuf - - # based on _gedit_tab_get_icon() in gedit-tab.c < 3.12 - def _get_stock_tab_icon(self, tab): - theme = Gtk.IconTheme.get_for_screen(tab.get_screen()) - is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup_for_settings(tab.get_settings(), Gtk.IconSize.MENU) - state = tab.get_state() - - if state in self.TAB_STATE_TO_STOCK_ICON: - try: - pixbuf = self._get_stock_icon(theme, self.TAB_STATE_TO_STOCK_ICON[state], icon_size_height) - except GObject.GError: - pixbuf = None + selection.unselect_all() + + + # tab switching + + def key_press_event(self, event): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, key=%s", self.window, Gdk.keyval_name(event.keyval))) + + settings = self._settings + is_control_tab, is_control_page, is_control_escape = keyinfo.is_control_keys(event) + block_event = True + + if is_control_tab and settings and settings['use-tabbar-order']: + if log.query(log.DEBUG): + debug_plugin_message(log.format("coercing ctrl-tab into ctrl-page because of settings")) + + is_control_tab = False + is_control_page = True + + if self._is_switching and is_control_escape: + if log.query(log.DEBUG): + debug_plugin_message(log.format("ctrl-esc while switching")) + + self.end_switching(True) + + elif is_control_tab or is_control_page: + if log.query(log.DEBUG): + debug_plugin_message(log.format("ctrl-tab or ctrl-page")) + + self.switch_tab(is_control_tab, keyinfo.is_next_key(event), event.time) + + elif self._is_switching and not self._is_tabwin_visible: + if log.query(log.DEBUG): + debug_plugin_message(log.format("normal key while switching and tabwin not visible")) + + self.end_switching() + block_event = False + else: - pixbuf = None + if log.query(log.DEBUG): + debug_plugin_message(log.format("normal key while %s", "switching" if self._is_switching else "not switching")) - if not pixbuf: - pixbuf = self._get_icon(theme, tab.get_document().get_location(), icon_size_height) + block_event = self._is_switching - return pixbuf + return block_event - # based on get_stock_icon() in gedit-tab.c in < 3.12 - def _get_stock_icon(self, theme, stock, size): - pixbuf = theme.load_icon(stock, size, 0) - return self._resize_icon(pixbuf, size) + def switch_tab(self, use_mru_order, to_next_tab, time): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, use_mru_order=%s, to_next_tab=%s, time=%s", self.window, use_mru_order, to_next_tab, time)) - # based on get_icon() in gedit-tab.c in < 3.12 - def _get_icon(self, theme, location, size): - if not location: - return self._get_stock_icon(theme, Gtk.STOCK_FILE, size) + window = self.window + current_tab = window.get_active_tab() - # FIXME: Doing a sync stat is bad, this should be fixed - try: - info = location.query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, None) - except GObject.GError: - info = None + if not current_tab: + if log.query(log.DEBUG): + debug_plugin_message(log.format("no tabs")) + + return + + notebook = current_tab.get_parent() + + tabs = self._tab_models[notebook] if use_mru_order else notebook.get_children() + num_tabs = len(tabs) + + if num_tabs < 2: + if log.query(log.DEBUG): + debug_plugin_message(log.format("only 1 tab")) - if not info: - return self._get_stock_icon(theme, Gtk.STOCK_FILE, size) + return - icon = info.get_icon() + current_index = tabs.index(current_tab) + step = 1 if to_next_tab else -1 + next_index = (current_index + step) % num_tabs - if not icon: - return self._get_stock_icon(theme, Gtk.STOCK_FILE, size) + next_tab = tabs[next_index] - icon_info = theme.lookup_by_gicon(icon, size, 0); + if log.query(log.DEBUG): + debug_plugin_message(log.format("switching from %s to %s", current_tab, next_tab)) - if not icon_info: - return self._get_stock_icon(theme, Gtk.STOCK_FILE, size) + if not self._is_switching: + if log.query(log.DEBUG): + debug_plugin_message(log.format("saving %s as initial tab", current_tab)) - pixbuf = icon_info.load_icon() + self._initial_tab = current_tab - if not pixbuf: - return self._get_stock_icon(theme, Gtk.STOCK_FILE, size) + self._is_switching = True - return self._resize_icon(pixbuf, size) + window.set_active_tab(next_tab) - # based on resize_icon() in gedit-tab.c in < 3.12 - def _resize_icon(self, pixbuf, size): - width = pixbuf.get_width() - height = pixbuf.get_height() + if use_mru_order: + tabwin = self._tabwin + + if not self._is_tabwin_visible: + if log.query(log.DEBUG): + debug_plugin_message(log.format("showing tabwin")) + + tabwin.show_all() - # if the icon is larger than the nominal size, scale down - if max(width, height) > size: - if width > height: - height = height * size / width - width = size else: - width = width * size / height - height = size + if log.query(log.DEBUG): + debug_plugin_message(log.format("presenting tabwin")) + + tabwin.present_with_time(time) + + self._is_tabwin_visible = True - pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) + def end_switching(self, do_revert=False): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, do_revert=%s", self.window, do_revert)) + + if not self._is_switching: + if log.query(log.DEBUG): + debug_plugin_message(log.format("not switching")) + + return + + window = self.window + initial_tab = self._initial_tab - return pixbuf + self._tabwin.hide() + + self._is_switching = False + self._is_tabwin_visible = False + self._initial_tab = None + + if do_revert and initial_tab: + if log.query(log.DEBUG): + debug_plugin_message(log.format("switching to initial tab %s", initial_tab)) + + window.set_active_tab(initial_tab) + + else: + tab = window.get_active_tab() + + if tab: + self.active_tab_changed(tab, self._tab_models[tab.get_parent()]) # tab window resizing - def _schedule_tabwin_resize(self): - if not self._tabwin_resize_id: - # need to wait a little before asking the treeview for its preferred size - # maybe because treeview rendering is async? - # this feels like a giant hack - try: - resize_id = GLib.idle_add(self._do_tabwin_resize) - except TypeError: - # gedit 3.0 - resize_id = GObject.idle_add(self._do_tabwin_resize) - - self._tabwin_resize_id = resize_id - - def _cancel_tabwin_resize(self): + def schedule_tabwin_resize(self): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self.window)) + if self._tabwin_resize_id: - GLib.source_remove(self._tabwin_resize_id) - self._tabwin_resize_id = None + if log.query(log.INFO): + debug_plugin_message(log.format("already scheduled")) + + return + + # need to wait a little before asking the treeview for its preferred size + # maybe because treeview rendering is async? + # this feels like a giant hack + try: + resize_id = GLib.idle_add(self.do_tabwin_resize) + except TypeError: # before pygobject 3.0 + resize_id = GObject.idle_add(self.do_tabwin_resize) + + self._tabwin_resize_id = resize_id + + def cancel_tabwin_resize(self): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self.window)) + + if not self._tabwin_resize_id: + if log.query(log.DEBUG): + debug_plugin_message(log.format("not scheduled")) + + return + + GLib.source_remove(self._tabwin_resize_id) + + self._tabwin_resize_id = None + + def do_tabwin_resize(self): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self.window)) - def _do_tabwin_resize(self): view = self._view sw = self._sw view_min_size, view_nat_size = view.get_preferred_size() view_height = max(view_min_size.height, view_nat_size.height) - num_rows = max(len(view.get_model()), 2) - row_height = math.ceil(view_height / num_rows) - max_rows_height = self.MAX_TAB_WINDOW_ROWS * row_height + num_rows = len(view.get_model()) + if num_rows: + row_height = math.ceil(view_height / num_rows) + max_rows_height = self.MAX_TAB_WINDOW_ROWS * row_height + else: + max_rows_height = float('inf') win_width, win_height = self.window.get_size() max_win_height = round(self.MAX_TAB_WINDOW_HEIGHT_PERCENTAGE * win_height) @@ -654,36 +712,350 @@ def _do_tabwin_resize(self): tabwin_width = max(sw_min_size.width, sw_nat_size.width) tabwin_height = min(view_height, max_height) + if log.query(log.DEBUG): + debug_plugin_message(log.format("view height = %s", view_height)) + debug_plugin_message(log.format("max rows height = %s", max_rows_height)) + debug_plugin_message(log.format("max win height = %s", max_win_height)) + debug_plugin_message(log.format("tabwin height = %s", tabwin_height)) + debug_plugin_message(log.format("tabwin width = %s", tabwin_width)) + self._tabwin.set_size_request(tabwin_width, tabwin_height) self._tabwin_resize_id = None + return False - # misc +class ControlYourTabsTabModel(GObject.Object): - # this is a /hack/ - def _get_multi_notebook(self, tab): - multi = tab.get_parent() - while multi: - if multi.__gtype__.name == 'GeditMultiNotebook': - break - multi = multi.get_parent() - return multi + __gtype_name__ = 'ControlYourTabsTabModel' + + __gsignals__ = { # before pygobject 3.4 + 'row-inserted': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreePath,)), + 'row-deleted': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreePath,)), + 'row-changed': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreePath,)), + 'rows-reordered': (GObject.SignalFlags.RUN_FIRST, None, ()), + 'selected-path-changed': (GObject.SignalFlags.RUN_FIRST, None, (Gtk.TreePath,)) + } + + + def _model_modifier(fn): + @wraps(fn) + def wrapper(self, *args, **kwargs): + prev_path = self.get_selected_path() + + result = fn(self, *args, **kwargs) + + cur_path = self.get_selected_path() + + if cur_path != prev_path: + self.emit('selected-path-changed', cur_path) + + return result + + return wrapper + + + def __init__(self, tabinfo): + GObject.Object.__init__(self) + + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self)) + + self._model = Gtk.ListStore.new((GdkPixbuf.Pixbuf, str, Gedit.Tab)) + self._references = {} + self._selected = None + self._tabinfo = tabinfo + + connect_handlers( + self, self._model, + [ + 'row-inserted', + 'row-deleted', + 'row-changed', + 'rows-reordered' + ], + 'model' + ) + + def __len__(self): + return len(self._model) + + def __getitem__(self, key): + return self._model[key][2] + + @_model_modifier + def __delitem__(self, key): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, key=%s", self, key)) + + tab = self._model[key][2] + + if self._selected == tab: + self._selected = None + + del self._references[tab] + + # before pygobject 3.2, cannot del model[path] + self._model.remove(self._model.get_iter(key)) + + def __iter__(self): + return [row[2] for row in self._model] + + def __contains__(self, item): + return item in self._references + + @property + def model(self): + return self._model + + def on_model_row_inserted(self, model, path, iter_): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, path=%s", self, model, path)) + + self.emit('row-inserted', path) + + def on_model_row_deleted(self, model, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, path=%s", self, model, path)) + + self.emit('row-deleted', path) + + def on_model_row_changed(self, model, path, iter_): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, path=%s", self, model, path)) + + self.emit('row-changed', path) + + def on_model_rows_reordered(self, model, path, iter_, new_order): + if log.query(log.INFO): + # path is suppose to point to the parent node of the reordered rows + # if top level rows are reordered, path is invalid (null?) + # so don't print it out here, because will throw an error + debug_plugin_message(log.format("%s, %s", self, model)) + + self.emit('rows-reordered') + + def do_row_inserted(self, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self, path)) + + def do_row_deleted(self, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self, path)) + + def do_row_changed(self, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self, path)) + + def do_rows_reordered(self): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self)) + + def do_selected_path_changed(self, path): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, path=%s", self, path)) + + @_model_modifier + def insert(self, position, tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, position=%s, %s", self, position, tab)) + + tab_iter = self._model.insert( + position, + ( + self._tabinfo.get_tab_icon(tab), + self._tabinfo.get_tab_name(tab), + tab + ) + ) + + self._references[tab] = Gtk.TreeRowReference.new(self._model, self._model.get_path(tab_iter)) + + def append(self, tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self, tab)) + + self.insert(len(self._model), tab) # before pygobject 3.2, -1 position does not work + + def prepend(self, tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self, tab)) + + self.insert(0, tab) + + def remove(self, tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self, tab)) + + del self[self.get_path(tab)] + + @_model_modifier + def move(self, tab, sibling, move_before): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, %s, move_before=%s", self, tab, sibling, move_before)) + + tab_iter = self._get_iter(tab) + sibling_iter = self._get_iter(sibling) if sibling else None + + if move_before: + self._model.move_before(tab_iter, sibling_iter) + else: + self._model.move_after(tab_iter, sibling_iter) + + def move_before(self, tab, sibling=None): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, %s", self, tab, sibling)) + + self.move(tab, sibling, True) + + def move_after(self, tab, sibling=None): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, %s", self, tab, sibling)) + + self.move(tab, sibling, False) + + def get_path(self, tab): + return self._references[tab].get_path() + + def index(self, tab): + return int(str(self.get_path(tab))) + + def _get_iter(self, tab): + return self._model.get_iter(self.get_path(tab)) + + @_model_modifier + def select(self, tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self, tab)) + + self._selected = tab + + def unselect(self): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", self)) + + self.select(None) + + def get_selected(self): + return self._selected + + def get_selected_path(self): + return self.get_path(self._selected) if self._selected else None + + def update(self, tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s", self, tab)) + + path = self.get_path(tab) + + self._model[path][0] = self._tabinfo.get_tab_icon(tab) + self._model[path][1] = self._tabinfo.get_tab_name(tab) + + +class ControlYourTabsConfigurable(GObject.Object, PeasGtk.Configurable): + + __gtype_name__ = 'ControlYourTabsConfigurable' + + + def do_create_configure_widget(self): + if log.query(log.INFO): + debug_plugin_message(log.format("")) + + settings = get_settings() + + if settings: + if log.query(log.DEBUG): + debug_plugin_message(log.format("have settings")) + + widget = Gtk.CheckButton.new_with_label( + _("Use tab row order for Ctrl+Tab / Ctrl+Shift+Tab") + ) + + settings.bind( + 'use-tabbar-order', + widget, 'active', + Gio.SettingsBindFlags.DEFAULT + ) + + widget._settings = settings + + else: + if log.query(log.DEBUG): + debug_plugin_message(log.format("no settings")) + + widget = Gtk.Label.new( + _("Sorry, no preferences are available for this version of gedit.") + ) + + box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + box.set_border_width(5) + box.add(widget) + + return box + + +# this is a /hack/ +# can do window.get_template_child(Gedit.Window, 'multi_notebook') since gedit 3.12 +def get_multi_notebook(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + widget = tab.get_parent() + + while widget: + if widget.__gtype__.name == 'GeditMultiNotebook': + break + + widget = widget.get_parent() + + return widget + +def get_settings(): + if log.query(log.INFO): + debug_plugin_message(log.format("")) + + schemas_path = os.path.join(BASE_PATH, 'schemas') + + try: + schema_source = Gio.SettingsSchemaSource.new_from_directory( + schemas_path, + Gio.SettingsSchemaSource.get_default(), + False + ) + + except AttributeError: # before gedit 3.4 + if log.query(log.DEBUG): + debug_plugin_message(log.format("relocatable schemas not supported")) + + schema_source = None + + except: + if log.query(log.WARNING): + debug_plugin_message(log.format("could not load settings schema source from %s", schemas_path)) + + schema_source = None + + if not schema_source: + if log.query(log.DEBUG): + debug_plugin_message(log.format("no schema source")) + + return None + + schema = schema_source.lookup( + 'com.thingsthemselves.gedit.plugins.controlyourtabs', + False + ) + + if not schema: + if log.query(log.WARNING): + debug_plugin_message(log.format("could not lookup schema")) + + return None + + return Gio.Settings.new_full( + schema, + None, + '/com/thingsthemselves/gedit/plugins/controlyourtabs/' + ) - def _get_settings(self): - schemas_path = os.path.join(BASE_PATH, 'schemas') - try: - # available in gedit >= 3.4 - schema_source = Gio.SettingsSchemaSource.new_from_directory(schemas_path, Gio.SettingsSchemaSource.get_default(), False) - schema = Gio.SettingsSchemaSource.lookup(schema_source, self.SETTINGS_SCHEMA_ID, False) - settings = Gio.Settings.new_full(schema, None, None) if schema else None - except AttributeError: - settings = None - except: - try: - Gedit.debug_plugin_message("could not load settings schema from %s", schemas_path) - except AttributeError: - pass - settings = None - return settings diff --git a/controlyourtabs/keyinfo.py b/controlyourtabs/keyinfo.py new file mode 100644 index 0000000..1531abf --- /dev/null +++ b/controlyourtabs/keyinfo.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# +# keyinfo.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018 Jeffery To +# https://github.com/jefferyto/gedit-control-your-tabs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from gi.repository import Gtk, Gdk, Gedit +from . import log + +try: + debug_plugin_message = Gedit.debug_plugin_message +except: # before gedit 3.4 + debug_plugin_message = lambda fmt, *fmt_args: None + + +CONTROL_MASK = Gdk.ModifierType.CONTROL_MASK + +CONTROL_SHIFT_MASK = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK + +CONTROL_KEY_LIST = [Gdk.KEY_Control_L, Gdk.KEY_Control_R] # will need to iterate through this list + +TAB_KEY_SET = set([Gdk.KEY_ISO_Left_Tab, Gdk.KEY_Tab]) + +PAGE_KEY_SET = set([Gdk.KEY_Page_Up, Gdk.KEY_Page_Down]) + +NEXT_KEY_SET = set([Gdk.KEY_Tab, Gdk.KEY_Page_Down]) + +ESCAPE_KEY = Gdk.KEY_Escape + + +def default_control_held(): + return [False for control_key in CONTROL_KEY_LIST] + +def update_control_held(event, prev_statuses, new_status): + keyval = event.keyval + + if log.query(log.INFO): + debug_plugin_message(log.format("key=%s, %s, new_status=%s", Gdk.keyval_name(keyval), prev_statuses, new_status)) + + new_statuses = [ + new_status if keyval == control_key else prev_status + for control_key, prev_status in zip(CONTROL_KEY_LIST, prev_statuses) + ] + + if log.query(log.DEBUG): + debug_plugin_message(log.format("new_statuses=%s", new_statuses)) + + return new_statuses + +def is_control_keys(event): + keyval = event.keyval + state = event.state & Gtk.accelerator_get_default_mod_mask() + + if log.query(log.INFO): + debug_plugin_message(log.format("key=%s, state=%s", Gdk.keyval_name(keyval), state)) + + is_control = state == CONTROL_MASK + is_control_shift = state == CONTROL_SHIFT_MASK + + is_tab = keyval in TAB_KEY_SET + is_page = keyval in PAGE_KEY_SET + is_escape = keyval == ESCAPE_KEY + + is_control_tab = (is_control or is_control_shift) and is_tab + is_control_page = is_control and is_page + is_control_escape = (is_control or is_control_shift) and is_escape + + if log.query(log.DEBUG): + debug_plugin_message(log.format("is_control_tab=%s, is_control_page=%s, is_control_escape=%s", is_control_tab, is_control_page, is_control_escape)) + + return (is_control_tab, is_control_page, is_control_escape) + +def is_next_key(event): + if log.query(log.INFO): + debug_plugin_message(log.format("key=%s", Gdk.keyval_name(event.keyval))) + + result = event.keyval in NEXT_KEY_SET + + if log.query(log.DEBUG): + debug_plugin_message(log.format("result=%s", result)) + + return result + diff --git a/controlyourtabs/locale/gedit-control-your-tabs.pot b/controlyourtabs/locale/gedit-control-your-tabs.pot index 8328feb..8d977b6 100644 --- a/controlyourtabs/locale/gedit-control-your-tabs.pot +++ b/controlyourtabs/locale/gedit-control-your-tabs.pot @@ -1,33 +1,33 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: gedit-control-your-tabs 0.3.1\n" -"POT-Creation-Date: 2017-10-17 03:56+0800\n" +"Project-Id-Version: gedit-control-your-tabs 0.3.2\n" +"POT-Creation-Date: 2018-03-13 21:23+0800\n" "PO-Revision-Date: 2014-12-03 21:27+0800\n" "Last-Translator: Jeffery To \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.0.1\n" +"X-Generator: Poedit 2.0.4\n" "X-Poedit-KeywordsList: _;gettext;gettext_noop\n" "X-Poedit-Basepath: ..\n" "X-Poedit-SearchPath-0: .\n" "X-Poedit-SearchPathExcluded-0: schemas\n" "X-Poedit-SearchPathExcluded-1: utils\n" -#: __init__.py:143 +#: __init__.py:95 msgid "Documents" msgstr "" -#: __init__.py:241 -msgid "Use tabbar order for Ctrl+Tab / Ctrl+Shift+Tab" +#: __init__.py:972 +msgid "Use tab row order for Ctrl+Tab / Ctrl+Shift+Tab" msgstr "" -#: __init__.py:247 +#: __init__.py:988 msgid "Sorry, no preferences are available for this version of gedit." msgstr "" -#: __init__.py:503 +#: tabinfo.py:74 tabinfo_pre312.py:71 msgid "Read-Only" msgstr "" diff --git a/controlyourtabs/log.py b/controlyourtabs/log.py new file mode 100644 index 0000000..633046c --- /dev/null +++ b/controlyourtabs/log.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +# +# log.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018 Jeffery To +# https://github.com/jefferyto/gedit-control-your-tabs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +from gi.repository import GLib +from .utils import debug_str + + +# for convenience, in decreasing order of severity +ERROR = GLib.LogLevelFlags.LEVEL_ERROR +CRITICAL = GLib.LogLevelFlags.LEVEL_CRITICAL +WARNING = GLib.LogLevelFlags.LEVEL_WARNING +MESSAGE = GLib.LogLevelFlags.LEVEL_MESSAGE +INFO = GLib.LogLevelFlags.LEVEL_INFO +DEBUG = GLib.LogLevelFlags.LEVEL_DEBUG + +LEVELS_TO_NAMES = { + ERROR: "error", + CRITICAL: "critical", + WARNING: "warning", + MESSAGE: "message", + INFO: "info", + DEBUG: "debug" +} + +NAMES_TO_LEVELS = {} + +for (level, name) in LEVELS_TO_NAMES.items(): + NAMES_TO_LEVELS[name] = level + +# messages equal or higher in severity will be printed +output_level = MESSAGE + +name = os.getenv('GEDIT_CONTROL_YOUR_TABS_DEBUG_LEVEL', '').lower() +if name in NAMES_TO_LEVELS: + output_level = NAMES_TO_LEVELS[name] + +# set by query(), used by name() +last_queried_level = None + + +def is_error(log_level): + return bool(log_level & ERROR) + +def is_critical(log_level): + return bool(log_level & CRITICAL) + +def is_warning(log_level): + return bool(log_level & WARNING) + +def is_message(log_level): + return bool(log_level & MESSAGE) + +def is_info(log_level): + return bool(log_level & INFO) + +def is_debug(log_level): + return bool(log_level & DEBUG) + +def highest(log_level): + if log_level < ERROR or is_error(log_level): + highest = ERROR + elif is_critical(log_level): + highest = CRITICAL + elif is_warning(log_level): + highest = WARNING + elif is_message(log_level): + highest = MESSAGE + elif is_info(log_level): + highest = INFO + else: + highest = DEBUG + + return highest + +def query(log_level): + global last_queried_level + last_queried_level = log_level + + return highest(log_level) <= output_level + +def name(log_level=None): + if log_level is None: + log_level = last_queried_level + + return LEVELS_TO_NAMES[highest(log_level)] if log_level is not None else 'unknown' + +def format(message, *args): + msg = message % tuple(debug_str(arg) for arg in args) + return "[%s] %s" % (name(), msg) + diff --git a/controlyourtabs/schemas/com.thingsthemselves.gedit.plugins.controlyourtabs.gschema.xml b/controlyourtabs/schemas/com.thingsthemselves.gedit.plugins.controlyourtabs.gschema.xml index cc4f2ae..1e2eb1d 100644 --- a/controlyourtabs/schemas/com.thingsthemselves.gedit.plugins.controlyourtabs.gschema.xml +++ b/controlyourtabs/schemas/com.thingsthemselves.gedit.plugins.controlyourtabs.gschema.xml @@ -1,10 +1,10 @@ - + false - Use tabbar order - Use tabbar order when switching tabs with Ctrl+Tab / Ctrl+Shift+Tab. + Use tab row order + Use tab row order when switching tabs with Ctrl+Tab / Ctrl+Shift+Tab. diff --git a/controlyourtabs/schemas/gschemas.compiled b/controlyourtabs/schemas/gschemas.compiled index 1c62ca1..097915c 100644 Binary files a/controlyourtabs/schemas/gschemas.compiled and b/controlyourtabs/schemas/gschemas.compiled differ diff --git a/controlyourtabs/tabinfo.py b/controlyourtabs/tabinfo.py new file mode 100644 index 0000000..a3f913b --- /dev/null +++ b/controlyourtabs/tabinfo.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# tabinfo.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018 Jeffery To +# https://github.com/jefferyto/gedit-control-your-tabs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os.path +from gi.repository import Gtk, GtkSource, Gedit +from xml.sax.saxutils import escape +from . import log + +BASE_PATH = os.path.dirname(os.path.realpath(__file__)) +LOCALE_PATH = os.path.join(BASE_PATH, 'locale') + +try: + import gettext + gettext.bindtextdomain('gedit-control-your-tabs', LOCALE_PATH) + _ = lambda s: gettext.dgettext('gedit-control-your-tabs', s) +except: + _ = lambda s: s + +try: + debug_plugin_message = Gedit.debug_plugin_message +except: # before gedit 3.4 + debug_plugin_message = lambda fmt, *fmt_args: None + + +# based on switch statement in _gedit_tab_get_icon() in gedit-tab.c +TAB_STATE_TO_NAMED_ICON = { + Gedit.TabState.STATE_PRINTING: 'printer-printing-symbolic', + Gedit.TabState.STATE_PRINT_PREVIEWING: 'printer-symbolic', + Gedit.TabState.STATE_SHOWING_PRINT_PREVIEW: 'printer-symbolic', + Gedit.TabState.STATE_LOADING_ERROR: 'dialog-error-symbolic', + Gedit.TabState.STATE_REVERTING_ERROR: 'dialog-error-symbolic', + Gedit.TabState.STATE_SAVING_ERROR: 'dialog-error-symbolic', + Gedit.TabState.STATE_GENERIC_ERROR: 'dialog-error-symbolic', + Gedit.TabState.STATE_EXTERNALLY_MODIFIED_NOTIFICATION: 'dialog-warning-symbolic' +} + +# based on doc_get_name() and document_row_sync_tab_name_and_icon() in gedit-documents-panel.c +def get_tab_name(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + doc = tab.get_document() + name = doc.get_short_name_for_display() + docname = Gedit.utils_str_middle_truncate(name, 60) # based on MAX_DOC_NAME_LENGTH in gedit-documents-panel.c + + tab_format = "%s" if doc.get_modified() else "%s" + tab_name = tab_format % escape(docname) + + try: + file = doc.get_file() + is_readonly = GtkSource.File.is_readonly(file) + except AttributeError: + is_readonly = doc.get_readonly() # deprecated since gedit 3.18 + + if is_readonly: + tab_name += " [%s]" % escape(_("Read-Only")) + + if log.query(log.DEBUG): + debug_plugin_message(log.format("tab_name=%s", tab_name)) + + return tab_name + +# based on _gedit_tab_get_icon() in gedit-tab.c +def get_tab_icon(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + state = tab.get_state() + + if state not in TAB_STATE_TO_NAMED_ICON: + return None + + theme = Gtk.IconTheme.get_for_screen(tab.get_screen()) + icon_name = TAB_STATE_TO_NAMED_ICON[state] + icon_size = get_tab_icon_size(tab) + + return Gtk.IconTheme.load_icon(theme, icon_name, icon_size, 0) + +def get_tab_icon_size(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup(Gtk.IconSize.MENU) + + return icon_size_height + diff --git a/controlyourtabs/tabinfo_pre312.py b/controlyourtabs/tabinfo_pre312.py new file mode 100644 index 0000000..88b5e72 --- /dev/null +++ b/controlyourtabs/tabinfo_pre312.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# +# tabinfo_pre312.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018 Jeffery To +# https://github.com/jefferyto/gedit-control-your-tabs +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os.path +from gi.repository import GObject, Gtk, GdkPixbuf, Gio, Gedit +from xml.sax.saxutils import escape +from . import log + +BASE_PATH = os.path.dirname(os.path.realpath(__file__)) +LOCALE_PATH = os.path.join(BASE_PATH, 'locale') + +try: + import gettext + gettext.bindtextdomain('gedit-control-your-tabs', LOCALE_PATH) + _ = lambda s: gettext.dgettext('gedit-control-your-tabs', s) +except: + _ = lambda s: s + +try: + debug_plugin_message = Gedit.debug_plugin_message +except: # before gedit 3.4 + debug_plugin_message = lambda fmt, *fmt_args: None + + +# based on switch statement in _gedit_tab_get_icon() in gedit-tab.c +TAB_STATE_TO_STOCK_ICON = { + Gedit.TabState.STATE_LOADING: Gtk.STOCK_OPEN, + Gedit.TabState.STATE_REVERTING: Gtk.STOCK_REVERT_TO_SAVED, + Gedit.TabState.STATE_SAVING: Gtk.STOCK_SAVE, + Gedit.TabState.STATE_PRINTING: Gtk.STOCK_PRINT, + Gedit.TabState.STATE_PRINT_PREVIEWING: Gtk.STOCK_PRINT_PREVIEW, + Gedit.TabState.STATE_SHOWING_PRINT_PREVIEW: Gtk.STOCK_PRINT_PREVIEW, + Gedit.TabState.STATE_LOADING_ERROR: Gtk.STOCK_DIALOG_ERROR, + Gedit.TabState.STATE_REVERTING_ERROR: Gtk.STOCK_DIALOG_ERROR, + Gedit.TabState.STATE_SAVING_ERROR: Gtk.STOCK_DIALOG_ERROR, + Gedit.TabState.STATE_GENERIC_ERROR: Gtk.STOCK_DIALOG_ERROR, + Gedit.TabState.STATE_EXTERNALLY_MODIFIED_NOTIFICATION: Gtk.STOCK_DIALOG_WARNING +} + +# based on tab_get_name() in gedit-documents-panel.c +def get_tab_name(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + doc = tab.get_document() + name = doc.get_short_name_for_display() + docname = Gedit.utils_str_middle_truncate(name, 60) # based on MAX_DOC_NAME_LENGTH in gedit-documents-panel.c + + tab_format = "%s" if doc.get_modified() else "%s" + tab_name = tab_format % escape(docname) + + if doc.get_readonly(): + tab_name += " [%s]" % escape(_("Read-Only")) + + if log.query(log.DEBUG): + debug_plugin_message(log.format("tab_name=%s", tab_name)) + + return tab_name + +# based on _gedit_tab_get_icon() in gedit-tab.c +def get_tab_icon(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + state = tab.get_state() + theme = Gtk.IconTheme.get_for_screen(tab.get_screen()) + icon_size = get_tab_icon_size(tab) + pixbuf = None + + if state in TAB_STATE_TO_STOCK_ICON: + stock = TAB_STATE_TO_STOCK_ICON[state] + + if log.query(log.DEBUG): + debug_plugin_message(log.format("getting stock icon %s", stock)) + + try: + pixbuf = get_stock_icon(theme, stock, icon_size) + except GObject.GError: + if log.query(log.WARNING): + debug_plugin_message(log.format("could not get stock icon %s", stock)) + + pixbuf = None + + if not pixbuf: + location = tab.get_document().get_location() + + if log.query(log.DEBUG): + debug_plugin_message(log.format("getting icon for location %s", location)) + + pixbuf = get_icon(theme, location, icon_size) + + return pixbuf + +def get_tab_icon_size(tab): + if log.query(log.INFO): + debug_plugin_message(log.format("%s", tab)) + + is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup_for_settings(tab.get_settings(), Gtk.IconSize.MENU) + + return icon_size_height + +# based on get_stock_icon() in gedit-tab.c +def get_stock_icon(theme, stock, size): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, size=%s", theme, stock, size)) + + pixbuf = theme.load_icon(stock, size, 0) + + return resize_icon(pixbuf, size) + +# based on get_icon() in gedit-tab.c +def get_icon(theme, location, size): + if log.query(log.INFO): + debug_plugin_message(log.format("%s, %s, size=%s", theme, location, size)) + + pixbuf = None + + if location: + if log.query(log.DEBUG): + debug_plugin_message(log.format("querying info for location %s", location)) + + # FIXME: Doing a sync stat is bad, this should be fixed + try: + info = location.query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, Gio.FileQueryInfoFlags.NONE, None) + except GObject.GError: + if log.query(log.WARNING): + debug_plugin_message(log.format("could not query info for location %s", location)) + + info = None + + icon = info.get_icon() if info else None + icon_info = theme.lookup_by_gicon(icon, size, 0) if icon else None + pixbuf = icon_info.load_icon() if icon_info else None + + if pixbuf: + if log.query(log.DEBUG): + debug_plugin_message(log.format("have pixbuf")) + + pixbuf = resize_icon(pixbuf, size) + + else: + if log.query(log.DEBUG): + debug_plugin_message(log.format("no pixbuf, getting stock icon %s", Gtk.STOCK_FILE)) + + pixbuf = get_stock_icon(theme, Gtk.STOCK_FILE, size) + + return pixbuf + +# based on resize_icon() in gedit-tab.c +def resize_icon(pixbuf, size): + if log.query(log.INFO): + debug_plugin_message(log.format("size=%s", size)) + + width = pixbuf.get_width() + height = pixbuf.get_height() + + # if the icon is larger than the nominal size, scale down + if max(width, height) > size: + if width > height: + height = height * size / width + width = size + else: + width = width * size / height + height = size + + pixbuf = pixbuf.scale_simple(width, height, GdkPixbuf.InterpType.BILINEAR) + + return pixbuf + diff --git a/controlyourtabs/utils/.editorconfig b/controlyourtabs/utils/.editorconfig new file mode 100644 index 0000000..092187d --- /dev/null +++ b/controlyourtabs/utils/.editorconfig @@ -0,0 +1,12 @@ +# editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/controlyourtabs/utils/.gitattributes b/controlyourtabs/utils/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/controlyourtabs/utils/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/controlyourtabs/utils/Changelog b/controlyourtabs/utils/Changelog deleted file mode 100644 index 9f63c14..0000000 --- a/controlyourtabs/utils/Changelog +++ /dev/null @@ -1,5 +0,0 @@ -2013-10-31 Jeffery To - - 0.1.0: - - * Initial release diff --git a/controlyourtabs/utils/Changelog.md b/controlyourtabs/utils/Changelog.md new file mode 100644 index 0000000..9784238 --- /dev/null +++ b/controlyourtabs/utils/Changelog.md @@ -0,0 +1,8 @@ +# Changelog + +## 0.2.0 (2017-10-13) +* Added create_bindings / release_bindings +* Added to_name, debug_str + +## 0.1.0 (2013-10-31) +* Initial release diff --git a/controlyourtabs/utils/README.md b/controlyourtabs/utils/README.md index 3d9b017..5740d5d 100644 --- a/controlyourtabs/utils/README.md +++ b/controlyourtabs/utils/README.md @@ -1,14 +1,14 @@ -# python-gtk-utils # +# python-gtk-utils A collection of utilities ready to be `git subtree`-ed into a Python GTK+ project -0.1.0 +0.2.0 All bug reports, feature requests and miscellaneous comments are welcome at the [project issue tracker][]. -## Installation ## +## Installation Use `git subtree` to pull this sub-project into your project: @@ -29,13 +29,13 @@ Pull for updates: git subtree pull --prefix=path/to/code/utils --squash python-gtk-utils master ``` -## Documentation ## +## Documentation ...would be a good idea ;-) -## License ## +## License -Copyright © 2013 Jeffery To +Copyright © 2013, 2017 Jeffery To Available under GNU General Public License version 3 diff --git a/controlyourtabs/utils/__init__.py b/controlyourtabs/utils/__init__.py index 878d857..b2f9a08 100644 --- a/controlyourtabs/utils/__init__.py +++ b/controlyourtabs/utils/__init__.py @@ -3,7 +3,7 @@ # __init__.py # This file is part of python-gtk-utils # -# Copyright (C) 2013 Jeffery To +# Copyright (C) 2013, 2017 Jeffery To # https://github.com/jefferyto/python-gtk-utils # # This program is free software: you can redistribute it and/or modify @@ -19,12 +19,26 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from gi.repository import GObject + + +# from future.utils + +def _iteritems(obj, **kwargs): + func = getattr(obj, 'iteritems', None) + if not func: + func = obj.items + return func(**kwargs) + + +# signal handlers + def _get_handler_ids_name(ns): return ns.__class__.__name__ + 'HandlerIds' def _get_handler_ids(ns, target): name = _get_handler_ids_name(ns) - return getattr(target, name) if hasattr(target, name) else [] + return getattr(target, name, []) def _set_handler_ids(ns, target, ids): name = _get_handler_ids_name(ns) @@ -42,7 +56,8 @@ def connect_handlers(ns, target, signals, prefix_or_fn, *args): if hasattr(prefix_or_fn, '__call__'): fn = prefix_or_fn else: - fn = getattr(ns, 'on_%s_%s' % (prefix_or_fn, signal.replace('-', '_').replace('::', '_'))) + fn = getattr(ns, 'on_%s_%s' % (prefix_or_fn, to_name(signal))) + handler_ids.append(target.connect(signal, fn, *args)) _set_handler_ids(ns, target, handler_ids) @@ -60,3 +75,74 @@ def block_handlers(ns, target): def unblock_handlers(ns, target): for handler_id in _get_handler_ids(ns, target): target.handler_unblock(handler_id) + + +# bindings + +def _get_bindings_name(ns): + return ns.__class__.__name__ + 'Bindings' + +def _get_bindings(ns, source, target): + name = _get_bindings_name(ns) + binding_map = getattr(source, name, {}) + return binding_map[target] if target in binding_map else [] + +def _set_bindings(ns, source, target, bindings): + name = _get_bindings_name(ns) + binding_map = getattr(source, name, {}) + binding_map[target] = bindings + setattr(source, name, binding_map) + +def _del_bindings(ns, source, target): + name = _get_bindings_name(ns) + binding_map = getattr(source, name, {}) + if target in binding_map: + del binding_map[target] + if not binding_map and hasattr(source, name): + delattr(source, name) + +def create_bindings(ns, source, target, properties, *args): + bindings = _get_bindings(ns, source, target) + + if isinstance(properties, dict): + for (source_property, target_property) in _iteritems(properties): + binding = source.bind_property( + source_property, + target, target_property, + *args + ) + bindings.append(binding) + + else: + for prop in properties: + binding = source.bind_property( + prop, + target, prop, + flags, + transform_to, transform_from, user_data + ) + bindings.append(binding) + + _set_bindings(ns, source, target, bindings) + +def release_bindings(ns, source, target): + for binding in _get_bindings(ns, source, target): + GObject.Binding.unbind(binding) + + _del_bindings(ns, source, target) + + +# misc + +def to_name(value): + return str(value).replace('-', '_').replace('::', '_') + +def debug_str(value): + if isinstance(value, GObject.Object): + # hash(value) is the memory address of the underlying gobject + result = value.__gtype__.name + ': ' + hex(hash(value)) + else: + result = value + + return result +