diff --git a/Changelog.md b/Changelog.md index ec9b22a..83884b4 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,6 +1,13 @@ # Changelog ## [v0.4.2-dev][Unreleased] - Unreleased +* Added support for Pluma and xed +* Added support for moving tabs with Ctrl-Shift-Page Up and + Ctrl-Shift-Page Down ([#13]) +* Added support for switching tabs with Tab/Page Up/Page Down keys in + numeric keypad +* Settings schema can be stored in a shared schemas location instead of + the plugin "schemas" directory ## [v0.4.1] - 2024-06-07 * Fixed error when loaded in gedit 47 @@ -92,5 +99,6 @@ [#4]: https://github.com/jefferyto/gedit-control-your-tabs/pull/4 [#8]: https://github.com/jefferyto/gedit-control-your-tabs/pull/8 +[#13]: https://github.com/jefferyto/gedit-control-your-tabs/issues/13 [#15]: https://github.com/jefferyto/gedit-control-your-tabs/pull/15 [#17]: https://github.com/jefferyto/gedit-control-your-tabs/issues/17 diff --git a/README.md b/README.md index 4e6cc29..b6a0ad0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# Control Your Tabs, a plugin for gedit +# Control Your Tabs, a plugin for gedit, Pluma, and xed -Switch between document tabs using Ctrl+Tab / Ctrl+Shift+Tab and -Ctrl+PageUp / Ctrl+PageDown +Switch between document tabs using Ctrl+Tab and other common keyboard +shortcuts v0.4.2-dev @@ -15,9 +15,14 @@ releases. ## Requirements -This plugin requires gedit 3.12 or newer. The last version compatible -with gedit 2 is [v0.1.2], and the last version compatible with gedit -3.0-3.10 is [v0.3.5]. +This plugin requires one of these text editors: + +* gedit 3.12 or newer +* Pluma 1.26.0 or newer +* xed 1.4.0 or newer + +The last version compatible with gedit 2 is [v0.1.2], and the last +version compatible with gedit 3.0–3.10 is [v0.3.5]. [v0.1.2]: https://github.com/jefferyto/gedit-control-your-tabs/releases/tag/v0.1.2 [v0.3.5]: https://github.com/jefferyto/gedit-control-your-tabs/releases/tag/v0.3.5 @@ -26,10 +31,12 @@ with gedit 2 is [v0.1.2], and the last version compatible with gedit 1. Download the [latest release] and extract. 2. Copy the `controlyourtabs` folder and the `controlyourtabs.plugin` - file into `~/.local/share/gedit/plugins` (create if it does not - exist). -3. Restart gedit, then activate the plugin in the **Plugins** tab in - gedit's **Preferences** window. + file into one of these paths (create if it does not exist): + * gedit: `~/.local/share/gedit/plugins` + * Pluma: `~/.local/share/pluma/plugins` + * xed: `~/.local/share/xed/plugins` +3. Restart the text editor, then activate the plugin in the **Plugins** + tab of the text editor's **Preferences** window. [latest release]: https://github.com/jefferyto/gedit-control-your-tabs/releases/latest @@ -42,21 +49,27 @@ with gedit 2 is [v0.1.2], and the last version compatible with gedit ## Usage -* Ctrl+Tab / - Ctrl+Shift+Tab - Switch tabs in - most recently used order. -* Ctrl+Page Up / - Ctrl+Page Down - Switch tabs in tab row order. +This plugin adds the following keyboard shortcuts: + +| Action | Shortcut | +| :------------------------------------ | :-------------------------------------------------------- | +| Switch to next most recently used tab | Ctrl + Tab | +| Switch to tab on the left | Ctrl + Page Up | +| Switch to tab on the right | Ctrl + Page Down | +| Move current tab left | Ctrl + Shift + Page Up | +| Move current tab right | Ctrl + Shift + Page Down | Hold down Ctrl to continue tab switching. Press -Esc while switching to cancel and return to the initial tab. +Esc while holding Ctrl to cancel and return to the +initial tab. ## Preferences -* `Use tab row order for Ctrl+Tab / Ctrl+Shift+Tab` - Change - Ctrl+Tab / - Ctrl+Shift+Tab to switch tabs in - tab row order instead of most recently used order. +* `Ctrl+Tab and Ctrl+Shift+Tab switch to tabs on the left and right` + + Change Ctrl + Tab and Ctrl + + Shift + Tab to switch to tabs on the left and + right instead of in most recently used order. ## Contributing diff --git a/controlyourtabs/__init__.py b/controlyourtabs/__init__.py index 155ace1..f1f59f5 100644 --- a/controlyourtabs/__init__.py +++ b/controlyourtabs/__init__.py @@ -19,1006 +19,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import gi -gi.require_version('Gtk', '3.0') -gi.require_version('Gedit', '3.0') - -import math -import os.path -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 - -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 - - -class ControlYourTabsWindowActivatable(GObject.Object, Gedit.WindowActivatable): - - __gtype_name__ = 'ControlYourTabsWindowActivatable' - - window = GObject.property(type=Gedit.Window) # before pygobject 3.2, lowercase 'p' - - MAX_TAB_WINDOW_ROWS = 9 - - MAX_TAB_WINDOW_HEIGHT_PERCENTAGE = 0.5 - - - def __init__(self): - GObject.Object.__init__(self) - - def do_activate(self): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s", self.window)) - - window = self.window - tab_models = {} - - tabwin = Gtk.Window.new(Gtk.WindowType.POPUP) - tabwin.set_transient_for(window) - tabwin.set_destroy_with_parent(True) - tabwin.set_accept_focus(False) - tabwin.set_decorated(False) - tabwin.set_resizable(False) - tabwin.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) - tabwin.set_type_hint(Gdk.WindowTypeHint.UTILITY) - tabwin.set_skip_taskbar_hint(False) - tabwin.set_skip_pager_hint(False) - - sw = Gtk.ScrolledWindow.new(None, None) - sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - sw.show() - - tabwin.add(sw) - - view = Gtk.TreeView.new() - view.set_enable_search(False) - view.set_headers_visible(False) - view.show() - - sw.add(view) - - col = Gtk.TreeViewColumn.new() - col.set_title(_("Documents")) - col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) - - 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) - col.pack_start(space_cell, False) - - col.add_attribute(icon_cell, 'pixbuf', 0) - col.add_attribute(name_cell, 'markup', 1) - - view.append_column(col) - - sel = view.get_selection() - sel.set_mode(Gtk.SelectionMode.SINGLE) - - # hack to ensure tabwin is correctly positioned/sized on first show - view.realize() - - 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._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 = get_settings() - self._tabinfo = tabinfo - - tab = window.get_active_tab() - - if tab: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("found active tab %s, setting up now", tab)) - - self.setup(window, tab, tab_models) - - if self._multi: - self.active_tab_changed(tab, tab_models[tab.get_parent()]) - - else: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("waiting for new tab")) - - connect_handlers(self, window, ['tab-added'], 'setup', tab_models) - - def do_deactivate(self): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s", self.window)) - - multi = self._multi - tab_models = self._tab_models - - for notebook in list(tab_models.keys()): - self.untrack_notebook(notebook, tab_models) - - if multi: - disconnect_handlers(self, multi) - - disconnect_handlers(self, self.window) - - self.cancel_tabwin_resize() - self.end_switching() - - self._tabwin.destroy() - - self._is_switching = None - self._is_tabwin_visible = None - self._is_control_held = None - self._initial_tab = None - self._multi = None - self._tab_models = None - self._tabwin = None - self._view = None - self._sw = None - self._icon_cell = None - self._space_cell = None - self._tabwin_resize_id = None - self._settings = None - self._tabinfo = None - - def do_update_state(self): - pass - - - # plugin setup - - def on_setup_tab_added(self, window, tab, tab_models): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", window, tab)) - - disconnect_handlers(self, window) - - self.setup(window, tab, tab_models) - - def setup(self, window, tab, tab_models): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", window, tab)) - - icon_size = self._tabinfo.get_tab_icon_size(tab) - - self._icon_cell.set_fixed_size(icon_size, icon_size) - self._space_cell.set_fixed_size(icon_size, icon_size) - - multi = window.get_template_child(Gedit.Window, 'multi_notebook') - - 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 - ) - - self._multi = multi - - for document in window.get_documents(): - notebook = Gedit.Tab.get_from_document(document).get_parent() - self.track_notebook(notebook, tab_models) - - - # tracking notebooks / tabs - - def track_notebook(self, notebook, tab_models): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", self.window, notebook)) - - if notebook in tab_models: - if log.query(log.WARNING): - Gedit.debug_plugin_message(log.format("already tracking notebook")) - - return - - tab_model = ControlYourTabsTabModel(self._tabinfo) - - 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' - ) - - tab_models[notebook] = tab_model - - for tab in notebook.get_children(): - self.track_tab(tab, tab_model) - - def untrack_notebook(self, notebook, tab_models): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", self.window, notebook)) - - if notebook not in tab_models: - if log.query(log.WARNING): - Gedit.debug_plugin_message(log.format("not tracking notebook")) - - return - - tab_model = tab_models[notebook] - - for tab in notebook.get_children(): - self.untrack_tab(tab, tab_model) - - if self.is_active_view_model(tab_model): - self.set_active_view_model(None) - - disconnect_handlers(self, tab_model) - - del tab_models[notebook] - - def track_tab(self, tab, tab_model): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", self.window, tab)) - - if tab in tab_model: - if log.query(log.WARNING): - Gedit.debug_plugin_message(log.format("already tracking tab")) - - return - - tab_model.append(tab) - - connect_handlers( - self, tab, - [ - 'notify::name', - 'notify::state' - ], - self.on_tab_notify_name_state, - tab_model - ) - - def untrack_tab(self, tab, tab_model): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", self.window, tab)) - - if tab == self._initial_tab: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("tab is initial tab, clearing")) - - self._initial_tab = None - - if tab not in tab_model: - if log.query(log.WARNING): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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=None): - # tab parameter removed in gedit 47 - if not tab_models: - tab_models = tab - tab = window.get_active_tab() - - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, %s", window, tab)) - - if 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): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("no control keys held down")) - - self.end_switching() - - else: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("one or more control keys held down")) - - def on_window_focus_out_event(self, window, event, tab_models): - if log.query(log.INFO): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("tab model not active")) - - 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): - Gedit.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): - Gedit.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) - - else: - selection.unselect_all() - - - # tab switching - - def key_press_event(self, event): - if log.query(log.INFO): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("normal key while switching and tabwin not visible")) - - self.end_switching() - block_event = False - - else: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("normal key while %s", "switching" if self._is_switching else "not switching")) - - block_event = self._is_switching - - return block_event - - def switch_tab(self, use_mru_order, to_next_tab, time): - if log.query(log.INFO): - Gedit.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)) - - window = self.window - current_tab = window.get_active_tab() - - if not current_tab: - if log.query(log.DEBUG): - Gedit.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): - Gedit.debug_plugin_message(log.format("only 1 tab")) - - return - - current_index = tabs.index(current_tab) - step = 1 if to_next_tab else -1 - next_index = (current_index + step) % num_tabs - - next_tab = tabs[next_index] - - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("switching from %s to %s", current_tab, next_tab)) - - if not self._is_switching: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("saving %s as initial tab", current_tab)) - - self._initial_tab = current_tab - - self._is_switching = True - - window.set_active_tab(next_tab) - - if use_mru_order: - tabwin = self._tabwin - - if not self._is_tabwin_visible: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("showing tabwin")) - - tabwin.show_all() - - else: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("presenting tabwin")) - - tabwin.present_with_time(time) - - self._is_tabwin_visible = True - - def end_switching(self, do_revert=False): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, do_revert=%s", self.window, do_revert)) - - if not self._is_switching: - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("not switching")) - - return - - window = self.window - initial_tab = self._initial_tab - - 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): - Gedit.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 log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s", self.window)) - - if self._tabwin_resize_id: - if log.query(log.INFO): - Gedit.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): - Gedit.debug_plugin_message(log.format("%s", self.window)) - - if not self._tabwin_resize_id: - if log.query(log.DEBUG): - Gedit.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): - Gedit.debug_plugin_message(log.format("%s", self.window)) - - 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 = 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) - - max_height = min(max_rows_height, max_win_height) - - # we can't reliably tell if overlay scrolling is being used - # since gtk_scrolled_window_get_overlay_scrolling() can still return True if GTK_OVERLAY_SCROLLING=0 is set - # and even if we can tell if overlay scrolling is disabled, - # we cannot tell if the scrolled window has reserved enough space for the scrollbar - # fedora < 25: reserved - # fedora >= 25: not reserved - # ubuntu 17.04: reserved - # so let's ignore overlay scrolling for now :-( - - vscrollbar_policy = Gtk.PolicyType.AUTOMATIC if view_height > max_height else Gtk.PolicyType.NEVER - sw.set_policy(Gtk.PolicyType.NEVER, vscrollbar_policy) - - sw_min_size, sw_nat_size = sw.get_preferred_size() - - tabwin_width = max(sw_min_size.width, sw_nat_size.width) - tabwin_height = min(view_height, max_height) - - if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("view height = %s", view_height)) - Gedit.debug_plugin_message(log.format("max rows height = %s", max_rows_height)) - Gedit.debug_plugin_message(log.format("max win height = %s", max_win_height)) - Gedit.debug_plugin_message(log.format("tabwin height = %s", tabwin_height)) - Gedit.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 - - -class ControlYourTabsTabModel(GObject.Object): - - __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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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 - Gedit.debug_plugin_message(log.format("%s, %s", self, model)) - - self.emit('rows-reordered') - - def do_row_inserted(self, path): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, path=%s", self, path)) - - def do_row_deleted(self, path): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, path=%s", self, path)) - - def do_row_changed(self, path): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, path=%s", self, path)) - - def do_rows_reordered(self): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s", self)) - - def do_selected_path_changed(self, path): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s, path=%s", self, path)) - - @_model_modifier - def insert(self, position, tab): - if log.query(log.INFO): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("%s, %s", self, tab)) - - self.insert(0, tab) - - def remove(self, tab): - if log.query(log.INFO): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("%s, %s", self, tab)) - - self._selected = tab - - def unselect(self): - if log.query(log.INFO): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("")) - - settings = get_settings() - - if settings: - if log.query(log.DEBUG): - Gedit.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): - Gedit.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 - - -def get_settings(): - if log.query(log.INFO): - Gedit.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: - if log.query(log.WARNING): - Gedit.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): - Gedit.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): - Gedit.debug_plugin_message(log.format("could not lookup schema")) - - return None - - return Gio.Settings.new_full( - schema, - None, - '/com/thingsthemselves/gedit/plugins/controlyourtabs/' - ) +from .configurable import ControlYourTabsConfigurable +from .windowactivatable import ControlYourTabsWindowActivatable diff --git a/controlyourtabs/configurable.py b/controlyourtabs/configurable.py new file mode 100644 index 0000000..a545ffb --- /dev/null +++ b/controlyourtabs/configurable.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +# +# configurable.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018, 2020, 2023-2024 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 gi +gi.require_version('GObject', '2.0') +gi.require_version('Gio', '2.0') +gi.require_version('Gtk', '3.0') +gi.require_version('PeasGtk', '1.0') + +from gi.repository import GObject, Gio, Gtk, PeasGtk +from .plugin import _ +from .settings import get_settings +from . import editor, log + + +class ControlYourTabsConfigurable(GObject.Object, PeasGtk.Configurable): + + __gtype_name__ = 'ControlYourTabsConfigurable' + + + def do_create_configure_widget(self): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("")) + + settings = get_settings() + + if settings: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Loaded settings")) + + widget = Gtk.CheckButton.new_with_label( + _("Ctrl+Tab and Ctrl+Shift+Tab switch to tabs on the left and right") + ) + + settings.bind( + 'use-tabbar-order', + widget, 'active', + Gio.SettingsBindFlags.DEFAULT + ) + + widget._settings = settings + + else: + if log.query(log.WARNING): + editor.debug_plugin_message(log.format("Could not load settings")) + + widget = Gtk.Label.new( + _("Unable to load preferences") + ) + + box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) + box.set_border_width(5) + box.add(widget) + + return box + diff --git a/controlyourtabs/editor.py b/controlyourtabs/editor.py new file mode 100644 index 0000000..1d0636f --- /dev/null +++ b/controlyourtabs/editor.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +# editor.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018, 2020, 2023-2024 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 gi +import inspect +import os + + +# based on get_trace_info() in Gedit.py +def get_trace_info(num_back_frames=0): + frame = inspect.currentframe().f_back + try: + for i in range(num_back_frames): + back_frame = frame.f_back + if back_frame is None: + break + frame = back_frame + + filename = frame.f_code.co_filename + + # http://code.activestate.com/recipes/145297-grabbing-the-current-line-number-easily/ + lineno = frame.f_lineno + + func_name = frame.f_code.co_name + try: + # http://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object + cls_name = frame.f_locals["self"].__class__.__name__ + except: + pass + else: + func_name = "%s.%s" % (cls_name, func_name) + + return (filename, lineno, func_name) + finally: + frame = None + +# based on debug_plugin_message() in Gedit.py and gedit-debug.c +def _debug_plugin_message(format, *format_args): + filename, lineno, func_name = get_trace_info(1) + message = format % format_args + print("%s:%d (%s) %s" % (filename, lineno, func_name, message), flush=True) + +try: + gi.require_version('Gedit', '3.0') +except ValueError: + Editor = None +else: + from gi.repository import Gedit as Editor + name = 'gedit' + use_new_tab_name_style = True + use_symbolic_icons = True + use_document_icons = False + use_editor_workaround = False + +if not Editor: + try: + gi.require_version('Xed', '1.0') + except ValueError: + Editor = None + else: + from gi.repository import Xed as Editor + name = 'xed' + use_new_tab_name_style = False + use_symbolic_icons = True + use_document_icons = True + use_editor_workaround = True + +if not Editor: + try: + # needs to be last because Pluma is a non-private namespace + gi.require_version('Pluma', '1.0') + except ValueError: + Editor = None + else: + from gi.repository import Pluma as Editor + name = 'Pluma' + use_new_tab_name_style = False + use_symbolic_icons = False + use_document_icons = True + use_editor_workaround = False + +try: + debug_plugin_message = Editor.debug_plugin_message +except AttributeError: + debug_plugin_message = lambda *args: None + + is_debug = os.getenv('%s_DEBUG' % name.upper()) + is_debug_plugins = os.getenv('%s_DEBUG_PLUGINS' % name.upper()) + + if is_debug or is_debug_plugins: + debug_plugin_message = _debug_plugin_message + diff --git a/controlyourtabs/keyinfo.py b/controlyourtabs/keyinfo.py index 23dcadc..65f08a9 100644 --- a/controlyourtabs/keyinfo.py +++ b/controlyourtabs/keyinfo.py @@ -19,8 +19,13 @@ # 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 +import gi +gi.require_version('Gdk', '3.0') +gi.require_version('Gtk', '3.0') + +from collections import namedtuple +from gi.repository import Gdk, Gtk +from . import editor, log CONTROL_MASK = Gdk.ModifierType.CONTROL_MASK @@ -29,13 +34,37 @@ 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 +KEY_SETS = { + 'tab': set([Gdk.KEY_ISO_Left_Tab, Gdk.KEY_Tab, Gdk.KEY_KP_Tab]), # what is shift numpad tab? + 'page_up': set([Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up]), + 'page_down': set([Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down]), + 'escape': set([Gdk.KEY_Escape]) +} + +MODIFIER_KEY_SET = set( + [ + Gdk.KEY_Shift_L, Gdk.KEY_Shift_R, + Gdk.KEY_Control_L, Gdk.KEY_Control_R, + Gdk.KEY_Meta_L, Gdk.KEY_Meta_R, + Gdk.KEY_Alt_L, Gdk.KEY_Alt_R, + Gdk.KEY_Super_L, Gdk.KEY_Super_R, + Gdk.KEY_Hyper_L, Gdk.KEY_Hyper_R, + Gdk.KEY_Caps_Lock, Gdk.KEY_Shift_Lock, Gdk.KEY_Num_Lock, Gdk.KEY_Scroll_Lock, + Gdk.KEY_ISO_Lock, Gdk.KEY_ISO_Level2_Latch, + Gdk.KEY_ISO_Level3_Shift, Gdk.KEY_ISO_Level3_Latch, Gdk.KEY_ISO_Level3_Lock, + Gdk.KEY_ISO_Level5_Shift, Gdk.KEY_ISO_Level5_Latch, Gdk.KEY_ISO_Level5_Lock, + Gdk.KEY_Mode_switch + ] +) + +ControlKeys = namedtuple( + 'ControlKeys', + [ + *[key for key in KEY_SETS.keys()], + *['shift_' + key for key in KEY_SETS.keys()], + *[key + '_key' for key in KEY_SETS.keys()] + ] +) def default_control_held(): @@ -44,8 +73,8 @@ def default_control_held(): def update_control_held(event, prev_statuses, new_status): keyval = event.keyval - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("key=%s, %s, new_status=%s", Gdk.keyval_name(keyval), prev_statuses, new_status)) + if log.query(log.DEBUG): + editor.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 @@ -53,7 +82,7 @@ def update_control_held(event, prev_statuses, new_status): ] if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("new_statuses=%s", new_statuses)) + editor.debug_plugin_message(log.format("new_statuses=%s", new_statuses)) return new_statuses @@ -61,33 +90,34 @@ def is_control_keys(event): keyval = event.keyval state = event.state & Gtk.accelerator_get_default_mod_mask() - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("key=%s, state=%s", Gdk.keyval_name(keyval), state)) + if log.query(log.DEBUG): + editor.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_control_key = is_control or is_control_shift - is_tab = keyval in TAB_KEY_SET - is_page = keyval in PAGE_KEY_SET - is_escape = keyval == ESCAPE_KEY + is_key = {key: keyval in set for (key, set) in KEY_SETS.items()} - 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 + result = ControlKeys(**{ + **{key: value and is_control for (key, value) in is_key.items()}, + **{'shift_' + key: value and is_control_shift for (key, value) in is_key.items()}, + **{key + '_key': value and is_control_key for (key, value) in is_key.items()} + }) if log.query(log.DEBUG): - Gedit.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)) + editor.debug_plugin_message(log.format("result=%s", result)) - return (is_control_tab, is_control_page, is_control_escape) + return result -def is_next_key(event): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("key=%s", Gdk.keyval_name(event.keyval))) +def is_modifier_key(event): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("key=%s", Gdk.keyval_name(event.keyval))) - result = event.keyval in NEXT_KEY_SET + result = event.keyval in MODIFIER_KEY_SET if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("result=%s", result)) + editor.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 74d29fb..b452013 100644 --- a/controlyourtabs/locale/gedit-control-your-tabs.pot +++ b/controlyourtabs/locale/gedit-control-your-tabs.pot @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: gedit-control-your-tabs 0.4.2-dev\n" -"POT-Creation-Date: 2024-06-08 03:17+0800\n" +"POT-Creation-Date: 2024-10-24 08:17+0800\n" "PO-Revision-Date: 2014-12-03 21:27+0800\n" "Last-Translator: Jeffery To \n" "Language-Team: \n" @@ -16,18 +16,18 @@ msgstr "" "X-Poedit-SearchPathExcluded-0: schemas\n" "X-Poedit-SearchPathExcluded-1: utils\n" -#: __init__.py:90 -msgid "Documents" +#: configurable.py:50 +msgid "Ctrl+Tab and Ctrl+Shift+Tab switch to tabs on the left and right" msgstr "" -#: __init__.py:957 -msgid "Use tab row order for Ctrl+Tab / Ctrl+Shift+Tab" +#: configurable.py:66 +msgid "Unable to load preferences" msgstr "" -#: __init__.py:973 -msgid "Sorry, no preferences are available for this version of gedit." +#: tabinfo.py:86 +msgid "Read-Only" msgstr "" -#: tabinfo.py:83 -msgid "Read-Only" +#: windowactivatable.py:83 +msgid "Documents" msgstr "" diff --git a/controlyourtabs/log.py b/controlyourtabs/log.py index 3717ef7..0450268 100644 --- a/controlyourtabs/log.py +++ b/controlyourtabs/log.py @@ -19,9 +19,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import gi +gi.require_version('GLib', '2.0') + import os from gi.repository import GLib from .utils import debug_str +from . import editor # for convenience, in decreasing order of severity @@ -49,9 +53,14 @@ # 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] +gedit_env_name = os.getenv('GEDIT_CONTROL_YOUR_TABS_DEBUG_LEVEL', '') +editor_env_name = os.getenv( + '%s_CONTROL_YOUR_TABS_DEBUG_LEVEL' % editor.name.upper(), + gedit_env_name +) +env_name = editor_env_name.lower() +if env_name in NAMES_TO_LEVELS: + output_level = NAMES_TO_LEVELS[env_name] # set by query(), used by name() last_queried_level = None @@ -101,7 +110,10 @@ 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' + if log_level is None: + return "unknown" + + return LEVELS_TO_NAMES[highest(log_level)] def format(message, *args): msg = message % tuple(debug_str(arg) for arg in args) diff --git a/controlyourtabs/plugin.py b/controlyourtabs/plugin.py new file mode 100644 index 0000000..75de9d7 --- /dev/null +++ b/controlyourtabs/plugin.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# plugin.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018, 2020, 2023-2024 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 gi +gi.require_version('Peas', '1.0') + +import os.path +from gi.repository import Peas + + +data_dir = Peas.Engine.get_default().get_plugin_info('controlyourtabs').get_data_dir() + +try: + import gettext + gettext.bindtextdomain( + 'gedit-control-your-tabs', + os.path.join(data_dir, 'locale') + ) + _ = lambda s: gettext.dgettext('gedit-control-your-tabs', s) +except: + _ = lambda s: s + diff --git a/controlyourtabs/schemas/com.thingsthemselves.gedit.plugins.controlyourtabs.gschema.xml b/controlyourtabs/schemas/com.thingsthemselves.gedit.plugins.controlyourtabs.gschema.xml index 1e2eb1d..bfca954 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 tab row order - Use tab row order when switching tabs with Ctrl+Tab / Ctrl+Shift+Tab. + Ctrl+Tab and Ctrl+Shift+Tab switch to tabs on the left and right. diff --git a/controlyourtabs/schemas/com.thingsthemselves.pluma.plugins.controlyourtabs.gschema.xml b/controlyourtabs/schemas/com.thingsthemselves.pluma.plugins.controlyourtabs.gschema.xml new file mode 100644 index 0000000..63a7a46 --- /dev/null +++ b/controlyourtabs/schemas/com.thingsthemselves.pluma.plugins.controlyourtabs.gschema.xml @@ -0,0 +1,10 @@ + + + + + false + Use tab row order + Ctrl+Tab and Ctrl+Shift+Tab switch to tabs on the left and right. + + + diff --git a/controlyourtabs/schemas/com.thingsthemselves.xed.plugins.controlyourtabs.gschema.xml b/controlyourtabs/schemas/com.thingsthemselves.xed.plugins.controlyourtabs.gschema.xml new file mode 100644 index 0000000..39562ec --- /dev/null +++ b/controlyourtabs/schemas/com.thingsthemselves.xed.plugins.controlyourtabs.gschema.xml @@ -0,0 +1,10 @@ + + + + + false + Use tab row order + Ctrl+Tab and Ctrl+Shift+Tab switch to tabs on the left and right. + + + diff --git a/controlyourtabs/schemas/gschemas.compiled b/controlyourtabs/schemas/gschemas.compiled index 097915c..9801742 100644 Binary files a/controlyourtabs/schemas/gschemas.compiled and b/controlyourtabs/schemas/gschemas.compiled differ diff --git a/controlyourtabs/settings.py b/controlyourtabs/settings.py new file mode 100644 index 0000000..533c7a7 --- /dev/null +++ b/controlyourtabs/settings.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# settings.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018, 2020, 2023-2024 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 gi +gi.require_version('Gio', '2.0') + +import os.path +from gi.repository import Gio +from .plugin import data_dir as plugin_data_dir +from . import editor, log + + +def get_settings(): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("")) + + schemas_directory = os.path.join(plugin_data_dir, 'schemas') + default_schema_source = Gio.SettingsSchemaSource.get_default() + schema_id = 'com.thingsthemselves.%s.plugins.controlyourtabs' % editor.name.lower() + + try: + schema_source = Gio.SettingsSchemaSource.new_from_directory( + schemas_directory, + default_schema_source, + False + ) + + except: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Could not load schema source from %s", schemas_directory)) + + schema_source = None + + if not schema_source: + schema_source = default_schema_source + + schema = schema_source.lookup(schema_id, True) if schema_source else None + return Gio.Settings.new_full(schema, None, None) if schema else None + diff --git a/controlyourtabs/tabinfo.py b/controlyourtabs/tabinfo.py index 2165ee8..d46e9e9 100644 --- a/controlyourtabs/tabinfo.py +++ b/controlyourtabs/tabinfo.py @@ -19,59 +19,61 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import gi +gi.require_version('GObject', '2.0') +gi.require_version('Gio', '2.0') +gi.require_version('Gtk', '3.0') +# GtkSource can be version 3 or 4 or 300 + import os.path -from gi.repository import Gtk, GtkSource, Gedit +from gi.repository import GObject, Gio, Gtk, GtkSource 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 +from .plugin import _ +from . import editor, log # based on switch statement in _gedit_tab_get_icon() in gedit-tab.c -TAB_STATE_TO_NAMED_ICON = {} -try: - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.PRINTING] = 'printer-printing-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.SHOWING_PRINT_PREVIEW] = 'printer-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.LOADING_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.REVERTING_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.SAVING_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.GENERIC_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.EXTERNALLY_MODIFIED_NOTIFICATION] = 'dialog-warning-symbolic' -except AttributeError: - # constant names before gedit 47 - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_PRINTING] = 'printer-printing-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_SHOWING_PRINT_PREVIEW] = 'printer-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_LOADING_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_REVERTING_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_SAVING_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_GENERIC_ERROR] = 'dialog-error-symbolic' - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_EXTERNALLY_MODIFIED_NOTIFICATION] = 'dialog-warning-symbolic' - -try: - # Gedit.TabState.STATE_PRINT_PREVIEWING removed in gedit 3.36 - TAB_STATE_TO_NAMED_ICON[Gedit.TabState.STATE_PRINT_PREVIEWING] = 'printer-symbolic' -except AttributeError: - pass +STATE_ICONS = { + 'PRINTING': 'document-print', + 'PRINT_PREVIEWING': 'document-print-preview', # removed in gedit 3.36 + 'SHOWING_PRINT_PREVIEW': 'document-print-preview', + 'LOADING_ERROR': 'dialog-error', + 'REVERTING_ERROR': 'dialog-error', + 'SAVING_ERROR': 'dialog-error', + 'GENERIC_ERROR': 'dialog-error', + 'EXTERNALLY_MODIFIED_NOTIFICATION': 'dialog-warning' +} + +TAB_STATE_ICONS = {} +for state_name, icon_name in STATE_ICONS.items(): + state = None + if hasattr(editor.Editor.TabState, state_name): + state = getattr(editor.Editor.TabState, state_name) + elif hasattr(editor.Editor.TabState, 'STATE_' + state_name): # before gedit 47 + state = getattr(editor.Editor.TabState, 'STATE_' + state_name) + + if editor.use_symbolic_icons: + icon_name += '-symbolic' + + if state: + TAB_STATE_ICONS[state] = icon_name # 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): - Gedit.debug_plugin_message(log.format("%s", tab)) + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", tab)) doc = tab.get_document() + is_modified = doc.get_modified() name = tab.get_property('name') - if doc.get_modified() and name[0] == "*": + if is_modified and name[0] == "*": name = name[1:] - tab_format = "%s" if doc.get_modified() else "%s" - tab_name = tab_format % escape(name) + if is_modified: + name_format = '%s' if editor.use_new_tab_name_style else '%s' + else: + name_format = '%s' + tab_name = name_format % escape(name) try: file = doc.get_file() @@ -80,34 +82,89 @@ def get_tab_name(tab): is_readonly = doc.get_readonly() # deprecated since gedit 3.18 if is_readonly: - tab_name += " [%s]" % escape(_("Read-Only")) + readonly_format = '%s' if editor.use_new_tab_name_style else '%s' + readonly_text = readonly_format % escape(_("Read-Only")) + tab_name += ' [%s]' % readonly_text if log.query(log.DEBUG): - Gedit.debug_plugin_message(log.format("tab_name=%s", tab_name)) + editor.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): - Gedit.debug_plugin_message(log.format("%s", tab)) + if log.query(log.DEBUG): + editor.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() + pixbuf = None - if state not in TAB_STATE_TO_NAMED_ICON: - return None + if state in TAB_STATE_ICONS: + icon_name = TAB_STATE_ICONS[state] - theme = Gtk.IconTheme.get_for_screen(tab.get_screen()) - icon_name = TAB_STATE_TO_NAMED_ICON[state] - icon_size = get_tab_icon_size(tab) + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Getting icon for state %s (%s)", state, icon_name)) - return Gtk.IconTheme.load_icon(theme, icon_name, icon_size, 0) + pixbuf = Gtk.IconTheme.load_icon(theme, icon_name, icon_size, 0) -def get_tab_icon_size(tab): - if log.query(log.INFO): - Gedit.debug_plugin_message(log.format("%s", tab)) + elif editor.use_document_icons: + doc = tab.get_document() + try: + file = doc.get_file() + location = file.get_location() + except AttributeError: + location = doc.get_location() + + if log.query(log.DEBUG): + editor.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(): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("")) is_valid_size, icon_size_width, icon_size_height = Gtk.icon_size_lookup(Gtk.IconSize.MENU) return icon_size_height +# based on get_icon() in gedit-tab.c +def get_icon(theme, location, size): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s, size=%s", theme, location, size)) + + pixbuf = None + + if location: + if log.query(log.DEBUG): + editor.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): + editor.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 not pixbuf: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("No pixbuf, getting generic text document icon")) + + pixbuf = Gtk.IconTheme.load_icon(theme, 'text-x-generic', size, 0) + + return pixbuf + diff --git a/controlyourtabs/tabmodel.py b/controlyourtabs/tabmodel.py new file mode 100644 index 0000000..f91c925 --- /dev/null +++ b/controlyourtabs/tabmodel.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# +# tabmodel.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018, 2020, 2023-2024 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 gi +gi.require_version('GObject', '2.0') +gi.require_version('GdkPixbuf', '2.0') +gi.require_version('Gtk', '3.0') + +from functools import wraps +from gi.repository import GObject, GdkPixbuf, Gtk +from .utils import connect_handlers +from . import editor, log, tabinfo + + +class ControlYourTabsTabModel(GObject.Object): + + __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): + GObject.Object.__init__(self) + + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", self)) + + self._model = Gtk.ListStore.new((GdkPixbuf.Pixbuf, str, editor.Editor.Tab)) + self._references = {} + self._selected = None + + 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.DEBUG): + editor.debug_plugin_message(log.format("%s, key=%s", self, key)) + + tab = self._model[key][2] + + if self._selected is 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.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + # 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 + editor.debug_plugin_message(log.format("%s, %s", self, model)) + + self.emit('rows-reordered') + + def do_row_inserted(self, path): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, path=%s", self, path)) + + def do_row_deleted(self, path): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, path=%s", self, path)) + + def do_row_changed(self, path): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, path=%s", self, path)) + + def do_rows_reordered(self): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", self)) + + def do_selected_path_changed(self, path): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, path=%s", self, path)) + + @_model_modifier + def insert(self, position, tab): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, position=%s, %s", self, position, tab)) + + tab_iter = self._model.insert( + position, + ( + tabinfo.get_tab_icon(tab), + 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.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self, tab)) + + self.insert(0, tab) + + def remove(self, tab): + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("%s, %s, %s", self, tab, sibling)) + + self.move(tab, sibling, move_before=True) + + def move_after(self, tab, sibling=None): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s, %s", self, tab, sibling)) + + self.move(tab, sibling, move_before=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.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self, tab)) + + self._selected = tab + + def unselect(self): + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self, tab)) + + path = self.get_path(tab) + + self._model[path][0] = tabinfo.get_tab_icon(tab) + self._model[path][1] = tabinfo.get_tab_name(tab) + diff --git a/controlyourtabs/windowactivatable.py b/controlyourtabs/windowactivatable.py new file mode 100644 index 0000000..31ed63f --- /dev/null +++ b/controlyourtabs/windowactivatable.py @@ -0,0 +1,857 @@ +# -*- coding: utf-8 -*- +# +# windowactivatable.py +# This file is part of Control Your Tabs, a plugin for gedit +# +# Copyright (C) 2010-2013, 2017-2018, 2020, 2023-2024 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 gi +gi.require_version('GLib', '2.0') +gi.require_version('GObject', '2.0') +gi.require_version('Gdk', '3.0') +gi.require_version('Gtk', '3.0') + +import math +from gi.repository import GLib, GObject, Gdk, Gtk +from .plugin import _ +from .settings import get_settings +from .tabmodel import ControlYourTabsTabModel +from .utils import connect_handlers, disconnect_handlers +from . import editor, keyinfo, log, tabinfo + + +class ControlYourTabsWindowActivatable(GObject.Object, editor.Editor.WindowActivatable): + + __gtype_name__ = 'ControlYourTabsWindowActivatable' + + window = GObject.property(type=editor.Editor.Window) # before pygobject 3.2, lowercase 'p' + + MAX_TAB_WINDOW_ROWS = 9 + + MAX_TAB_WINDOW_HEIGHT_PERCENTAGE = 0.5 + + + def __init__(self): + GObject.Object.__init__(self) + + def do_activate(self): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", self.window)) + + window = self.window + tab_models = {} + + tabwin = Gtk.Window.new(Gtk.WindowType.POPUP) + tabwin.set_transient_for(window) + tabwin.set_destroy_with_parent(True) + tabwin.set_accept_focus(False) + tabwin.set_decorated(False) + tabwin.set_resizable(False) + tabwin.set_position(Gtk.WindowPosition.CENTER_ON_PARENT) + tabwin.set_type_hint(Gdk.WindowTypeHint.UTILITY) + tabwin.set_skip_taskbar_hint(False) + tabwin.set_skip_pager_hint(False) + + sw = Gtk.ScrolledWindow.new(None, None) + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.show() + + tabwin.add(sw) + + view = Gtk.TreeView.new() + view.set_enable_search(False) + view.set_headers_visible(False) + view.show() + + sw.add(view) + + col = Gtk.TreeViewColumn.new() + col.set_title(_("Documents")) + col.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE) + + 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) + col.pack_start(space_cell, False) + + col.add_attribute(icon_cell, 'pixbuf', 0) + col.add_attribute(name_cell, 'markup', 1) + + view.append_column(col) + + sel = view.get_selection() + sel.set_mode(Gtk.SelectionMode.SINGLE) + + # hack to ensure tabwin is correctly positioned/sized on first show + view.realize() + + self._is_switching = False + self._is_tabwin_visible = False + self._is_control_held = keyinfo.default_control_held() + self._pre_key_press_control_keys = None + self._initial_tab = None + self._multi = None + 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 = get_settings() + + tab = window.get_active_tab() + + if tab: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Found active tab %s, setting up now", tab)) + + self.setup(window, tab, tab_models) + + else: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Waiting for new tab")) + + connect_handlers( + self, window, + ['tab-added'], + 'setup', + tab_models + ) + + def do_deactivate(self): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", self.window)) + + multi = self._multi + tab_models = self._tab_models + + for notebook in list(tab_models.keys()): + self.untrack_notebook(notebook, tab_models) + + if multi: + disconnect_handlers(self, multi) + + disconnect_handlers(self, self.window) + + self.cancel_tabwin_resize() + self.end_switching() + + self._tabwin.destroy() + + self._is_switching = None + self._is_tabwin_visible = None + self._is_control_held = None + self._pre_key_press_control_keys = None + self._initial_tab = None + self._multi = None + self._tab_models = None + self._tabwin = None + self._view = None + self._sw = None + self._icon_cell = None + self._space_cell = None + self._tabwin_resize_id = None + self._settings = None + + def do_update_state(self): + pass + + + # plugin setup + + def on_setup_tab_added(self, window, tab, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", window, tab)) + + disconnect_handlers(self, window) + + self.setup(window, tab, tab_models) + + def setup(self, window, tab, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", window, tab)) + + icon_size = tabinfo.get_tab_icon_size() + + self._icon_cell.set_fixed_size(icon_size, icon_size) + self._space_cell.set_fixed_size(icon_size, icon_size) + + multi = window.get_template_child(editor.Editor.Window, 'multi_notebook') + + if multi: + connect_handlers( + self, multi, + [ + 'notebook-added', + 'notebook-removed', + 'tab-added', + 'tab-removed' + ], + 'multi_notebook', + tab_models + ) + else: + connect_handlers( + self, window, + [ + 'tab-added', + 'tab-removed' + ], + 'window', + tab.get_parent(), tab_models + ) + + connect_handlers( + self, window, + [ + 'active-tab-changed', + 'key-press-event', + 'key-release-event', + 'focus-out-event', + 'configure-event' + ], + 'window', + tab_models + ) + + if editor.use_editor_workaround: + connect_handlers( + self, window, + [ + 'event', + 'event-after' + ], + 'window' + ) + + self._multi = multi + + for document in window.get_documents(): + notebook = editor.Editor.Tab.get_from_document(document).get_parent() + self.track_notebook(notebook, tab_models, is_setup=True) + + self.active_tab_changed(tab, tab_models[tab.get_parent()]) + + + # tracking notebooks / tabs + + def track_notebook(self, notebook, tab_models, is_setup=False): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self.window, notebook)) + + if notebook in tab_models: + if log.query(log.DEBUG if is_setup else log.WARNING): + editor.debug_plugin_message(log.format("Already tracking %s", notebook)) + + return + + tab_model = ControlYourTabsTabModel() + + 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' + ) + + tab_models[notebook] = tab_model + + for tab in notebook.get_children(): + self.track_tab(tab, tab_model) + + def untrack_notebook(self, notebook, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self.window, notebook)) + + if notebook not in tab_models: + if log.query(log.WARNING): + editor.debug_plugin_message(log.format("Not tracking %s", notebook)) + + return + + tab_model = tab_models[notebook] + + for tab in notebook.get_children(): + self.untrack_tab(tab, tab_model) + + if self.is_active_view_model(tab_model): + self.set_active_view_model(None) + + disconnect_handlers(self, tab_model) + + del tab_models[notebook] + + def track_tab(self, tab, tab_model): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self.window, tab)) + + if tab in tab_model: + if log.query(log.WARNING): + editor.debug_plugin_message(log.format("Already tracking %s", tab)) + + return + + tab_model.append(tab) + + connect_handlers( + self, tab, + [ + 'notify::name', + 'notify::state' + ], + self.on_tab_notify_name_state, + tab_model + ) + + def untrack_tab(self, tab, tab_model): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", self.window, tab)) + + if tab is self._initial_tab: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Tab is initial tab, clearing")) + + self._initial_tab = None + + if tab not in tab_model: + if log.query(log.WARNING): + editor.debug_plugin_message(log.format("Not tracking %s", tab)) + + return + + disconnect_handlers(self, tab) + + tab_model.remove(tab) + + def active_tab_changed(self, tab, tab_model): + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("%s, %s, %s", self.window, notebook, tab)) + + self.untrack_tab(tab, tab_models[notebook]) + + def on_window_tab_added(self, window, tab, notebook, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s, %s", window, notebook, tab)) + + self.track_tab(tab, tab_models[notebook]) + + def on_window_tab_removed(self, window, tab, notebook, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s, %s", window, notebook, tab)) + + self.untrack_tab(tab, tab_models[notebook]) + + def on_window_active_tab_changed(self, window, tab, tab_models=None): + # tab parameter removed in gedit 47 + if not tab_models: + tab_models = tab + tab = window.get_active_tab() + + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, %s", window, tab)) + + if tab: + tab_model = tab_models[tab.get_parent()] + + # in pluma/xed, when a tab is added to an empty notebook, + # active-tab-changed is emitted before tab-added + if tab not in tab_model: + self.track_tab(tab, tab_model) + + self.active_tab_changed(tab, tab_model) + + def on_window_key_press_event(self, window, event, tab_models): + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("No control keys held down")) + + self.end_switching() + + else: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("One or more control keys held down")) + + def on_window_focus_out_event(self, window, event, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", window)) + + self.end_switching() + + def on_window_configure_event(self, window, event, tab_models): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", window)) + + self.schedule_tabwin_resize() + + def on_window_event(self, window, event): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", window)) + + if event.type is Gdk.EventType.KEY_PRESS: + self.pre_key_press_event(event) + + def on_window_event_after(self, window, event): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", window)) + + if event.type is Gdk.EventType.KEY_PRESS: + self._pre_key_press_control_keys = None + + def on_tab_notify_name_state(self, tab, pspec, tab_model): + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.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): + editor.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.DEBUG): + editor.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): + editor.debug_plugin_message(log.format("Tab model not active")) + + 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.DEBUG): + editor.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.DEBUG): + editor.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) + + else: + selection.unselect_all() + + + # tab switching/moving + + def pre_key_press_event(self, event): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, key=%s", self.window, Gdk.keyval_name(event.keyval))) + + is_control = keyinfo.is_control_keys(event) + + if is_control.tab_key: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Applying editor workaround for Ctrl-Tab")) + + event.state &= ~keyinfo.CONTROL_MASK + self._pre_key_press_control_keys = is_control + + elif self._is_switching and is_control.escape_key: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Applying editor workaround for Ctrl-Esc")) + + event.keyval = Gdk.KEY_VoidSymbol + self._pre_key_press_control_keys = is_control + + def key_press_event(self, event): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, key=%s", self.window, Gdk.keyval_name(event.keyval))) + + settings = self._settings + block_event = False + + if self._pre_key_press_control_keys: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Completing editor workaround")) + + is_control = self._pre_key_press_control_keys + + if is_control.tab_key: + event.state |= keyinfo.CONTROL_MASK + + elif self._is_switching and is_control.escape_key: + event.keyval = Gdk.KEY_Escape + + self._pre_key_press_control_keys = None + + else: + is_control = keyinfo.is_control_keys(event) + + if is_control.tab_key and settings and settings['use-tabbar-order']: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Coercing Ctrl-Tab into Ctrl-PgUp/PgDn because of settings")) + + is_control = is_control._replace( + tab=False, shift_tab=False, tab_key=False, + page_up=is_control.shift_tab, + page_up_key=is_control.shift_tab, + page_down=is_control.tab, + page_down_key=is_control.tab + ) + + if is_control.tab_key or is_control.page_up or is_control.page_down: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Ctrl-Tab or Ctrl-PgUp/PgDn, switch tab")) + + self.switch_tab( + use_mru_order=is_control.tab_key, + to_next_tab=is_control.tab or is_control.page_down, + time=event.time + ) + block_event = True + + elif is_control.shift_page_up or is_control.shift_page_down: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Ctrl-Shift-PgUp/PgDn, move tab")) + + self.end_switching() + self.move_tab(to_right=is_control.shift_page_down) + block_event = True + + elif self._is_switching: + if is_control.escape_key: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Ctrl-Esc while switching, cancel tab switching")) + + self.end_switching(do_revert=True) + block_event = True + + elif keyinfo.is_modifier_key(event): + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Modifier key while switching, no action")) + + elif not self._is_tabwin_visible: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Normal key while switching and tabwin not visible, end tab switching")) + + self.end_switching() + + else: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Normal key while switching, block key press")) + + block_event = True + + else: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Normal key, no action")) + + return block_event + + def switch_tab(self, use_mru_order, to_next_tab, time): + if log.query(log.DEBUG): + editor.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)) + + window = self.window + current_tab = window.get_active_tab() + + if not current_tab: + if log.query(log.INFO): + editor.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.INFO): + editor.debug_plugin_message(log.format("Only 1 tab")) + + return + + current_index = tabs.index(current_tab) + step = 1 if to_next_tab else -1 + next_index = (current_index + step) % num_tabs + + next_tab = tabs[next_index] + + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Switching from %s to %s", current_tab, next_tab)) + + if not self._is_switching: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Saving %s as initial tab", current_tab)) + + self._initial_tab = current_tab + + self._is_switching = True + + window.set_active_tab(next_tab) + + if use_mru_order: + tabwin = self._tabwin + + if not self._is_tabwin_visible: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Showing tabwin")) + + tabwin.show_all() + + else: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Presenting tabwin")) + + tabwin.present_with_time(time) + + self._is_tabwin_visible = True + + def end_switching(self, do_revert=False): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, do_revert=%s", self.window, do_revert)) + + if not self._is_switching: + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("Not switching")) + + return + + window = self.window + initial_tab = self._initial_tab + + 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.INFO): + editor.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()]) + + def move_tab(self, to_right): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s, to_right=%s", self.window, to_right)) + + window = self.window + current_tab = window.get_active_tab() + + if not current_tab: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("No tabs")) + + return + + notebook = current_tab.get_parent() + tabs = notebook.get_children() + num_tabs = len(tabs) + + if num_tabs < 2: + if log.query(log.INFO): + editor.debug_plugin_message(log.format("Only 1 tab")) + + return + + current_index = tabs.index(current_tab) + step = 1 if to_right else -1 + next_index = (current_index + step) % num_tabs + + try: + notebook.reorder_tab(current_tab, next_index) + except AttributeError: + notebook.reorder_child(current_tab, next_index) + + + # tab window resizing + + def schedule_tabwin_resize(self): + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("%s", self.window)) + + if self._tabwin_resize_id: + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("%s", self.window)) + + if not self._tabwin_resize_id: + if log.query(log.DEBUG): + editor.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.DEBUG): + editor.debug_plugin_message(log.format("%s", self.window)) + + 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 = 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) + + max_height = min(max_rows_height, max_win_height) + + # we can't reliably tell if overlay scrolling is being used + # since gtk_scrolled_window_get_overlay_scrolling() can still return True if GTK_OVERLAY_SCROLLING=0 is set + # and even if we can tell if overlay scrolling is disabled, + # we cannot tell if the scrolled window has reserved enough space for the scrollbar + # fedora < 25: reserved + # fedora >= 25: not reserved + # ubuntu 17.04: reserved + # so let's ignore overlay scrolling for now :-( + + vscrollbar_policy = Gtk.PolicyType.AUTOMATIC if view_height > max_height else Gtk.PolicyType.NEVER + sw.set_policy(Gtk.PolicyType.NEVER, vscrollbar_policy) + + sw_min_size, sw_nat_size = sw.get_preferred_size() + + tabwin_width = max(sw_min_size.width, sw_nat_size.width) + tabwin_height = min(view_height, max_height) + + if log.query(log.DEBUG): + editor.debug_plugin_message(log.format("view height = %s", view_height)) + editor.debug_plugin_message(log.format("max rows height = %s", max_rows_height)) + editor.debug_plugin_message(log.format("max win height = %s", max_win_height)) + editor.debug_plugin_message(log.format("tabwin height = %s", tabwin_height)) + editor.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 +