From 01bada3e7e0374cb3fcfb0ea63dc6cf911221b56 Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Sat, 11 Feb 2017 12:00:16 -0800 Subject: [PATCH 1/2] Implement font zooming in the editor Give controls for zoom in, zoom out, and reset zoom in the menu and via hotkey. Also add a new placeholder in the view menu to allow positioning the menuitems properly. Still want to keep the highlighting menu item at the bottom. This is implemented by using a builtin libpeas python plugin. Add support for loading these plugins at startup. They cannot be enabled or disabled through the UI. They can still be disabled in gsettings but will be automatically reloaded when xed is restarted. Closes: https://github.com/linuxmint/xed/issues/45 --- configure.ac | 4 + debian/xed.install | 3 +- plugins/Makefile.am | 2 + plugins/textsize/Makefile.am | 16 ++ plugins/textsize/textsize.plugin.desktop.in | 11 + plugins/textsize/textsize/Makefile.am | 11 + plugins/textsize/textsize/__init__.py | 233 ++++++++++++++++++++ plugins/textsize/textsize/documenthelper.py | 189 ++++++++++++++++ plugins/textsize/textsize/signals.py | 69 ++++++ xed/resources/ui/xed-ui.xml | 3 + xed/xed-plugins-engine.c | 29 ++- 11 files changed, 557 insertions(+), 13 deletions(-) create mode 100644 plugins/textsize/Makefile.am create mode 100644 plugins/textsize/textsize.plugin.desktop.in create mode 100644 plugins/textsize/textsize/Makefile.am create mode 100644 plugins/textsize/textsize/__init__.py create mode 100644 plugins/textsize/textsize/documenthelper.py create mode 100644 plugins/textsize/textsize/signals.py diff --git a/configure.ac b/configure.ac index 29e9783..8aa24b9 100644 --- a/configure.ac +++ b/configure.ac @@ -173,6 +173,8 @@ else enable_introspection=no fi +AM_PATH_PYTHON([3.2.3]) + dnl ================================================================ dnl GSettings related settings dnl ================================================================ @@ -236,6 +238,8 @@ plugins/sort/Makefile plugins/spell/Makefile plugins/spell/org.x.editor.plugins.spell.gschema.xml plugins/taglist/Makefile +plugins/textsize/Makefile +plugins/textsize/textsize/Makefile plugins/time/Makefile plugins/time/org.x.editor.plugins.time.gschema.xml plugins/trailsave/Makefile diff --git a/debian/xed.install b/debian/xed.install index 664190c..5871df7 100644 --- a/debian/xed.install +++ b/debian/xed.install @@ -1,8 +1,7 @@ usr/bin/xed usr/lib/*/xed/girepository-1.0/ usr/lib/xed/xed-bugreport.sh -usr/lib/*/xed/plugins/*.plugin -usr/lib/*/xed/plugins/*.so +usr/lib/*/xed/plugins/ usr/lib/*/xed/*.so usr/share/applications/xed.desktop usr/share/dbus-1/ diff --git a/plugins/Makefile.am b/plugins/Makefile.am index a162a09..2525b12 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -5,6 +5,7 @@ DIST_SUBDIRS = \ sort \ spell \ taglist \ + textsize \ time \ trailsave @@ -15,6 +16,7 @@ SUBDIRS = \ sort \ spell \ taglist \ + textsize \ time \ trailsave diff --git a/plugins/textsize/Makefile.am b/plugins/textsize/Makefile.am new file mode 100644 index 0000000..7088890 --- /dev/null +++ b/plugins/textsize/Makefile.am @@ -0,0 +1,16 @@ +# Textsize Plugin +SUBDIRS = textsize +plugindir = $(XED_PLUGINS_LIBS_DIR) + +plugin_in_files = textsize.plugin.desktop.in +%.plugin: %.plugin.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache + +plugin_DATA = $(plugin_in_files:.plugin.desktop.in=.plugin) + +EXTRA_DIST = \ + $(plugin_in_files) + +CLEANFILES = $(plugin_DATA) +DISTCLEANFILES = $(plugin_DATA) + +-include $(top_srcdir)/git.mk diff --git a/plugins/textsize/textsize.plugin.desktop.in b/plugins/textsize/textsize.plugin.desktop.in new file mode 100644 index 0000000..75ee9f0 --- /dev/null +++ b/plugins/textsize/textsize.plugin.desktop.in @@ -0,0 +1,11 @@ +[Plugin] +Loader=python3 +Module=textsize +IAge=3 +_Name=Text Size +_Description=Allow controlling the zoom levels of the text +Icon=gnome-mime-text-x-python +Authors=Steve Frécinaux ;Linux Mint team +Copyright=Copyright © 2017 by the authors +Website=https://github.com/linuxmint +Builtin=true \ No newline at end of file diff --git a/plugins/textsize/textsize/Makefile.am b/plugins/textsize/textsize/Makefile.am new file mode 100644 index 0000000..2999d65 --- /dev/null +++ b/plugins/textsize/textsize/Makefile.am @@ -0,0 +1,11 @@ +plugindir = $(XED_PLUGINS_LIBS_DIR)/textsize + +plugin_PYTHON = \ + __init__.py \ + signals.py \ + documenthelper.py + +CLEANFILES = +DISTCLEANFILES = + +-include $(top_srcdir)/git.mk diff --git a/plugins/textsize/textsize/__init__.py b/plugins/textsize/textsize/__init__.py new file mode 100644 index 0000000..d9b36bf --- /dev/null +++ b/plugins/textsize/textsize/__init__.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- +# +# __init__.py - Text size plugin +# +# Copyright (C) 2008 - Konstantin Mikhaylov +# Copyright (C) 2009 - Wouter Bolsterlee +# Copyright (C) 2010 - Ignacio Casal Quinteiro +# Copyright (C) 2010 - Jesse van den Kieboom +# Copyright (C) 2017 - Linux Mint team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + +from gi.repository import GObject, Gio, Gtk, Gdk, Xed +from .documenthelper import DocumentHelper + +MENU_PATH = "/MenuBar/ViewMenu/ViewOps_1" + +class TextSizePlugin(GObject.Object, Xed.WindowActivatable): + __gtype_name__ = "TextSizePlugin" + + window = GObject.property(type=Xed.Window) + + def __init__(self): + GObject.Object.__init__(self) + + def do_activate(self): + self._views = {} + + # Insert menu items + self._insert_menu() + + # Insert document helpers + for view in self.window.get_views(): + self.add_document_helper(view) + + self.window.connect('tab-added', self.on_tab_added) + self.window.connect('tab-removed', self.on_tab_removed) + + self._accel_group = Gtk.AccelGroup() + self.window.add_accel_group(self._accel_group) + + self._proxy_callback_map = { + 'LargerTextAction': self.on_larger_text_accel, + 'SmallerTextAction': self.on_smaller_text_accel, + 'NormalSizeAction': self.on_normal_size_accel + } + + self._proxy_mapping = {} + self._init_proxy_accels() + self._accel_map_handler_id = Gtk.AccelMap.get().connect('changed', self.on_accel_map_changed) + + def _install_proxy(self, action): + if not isinstance(action, Gtk.Action): + action = self._action_group.get_action(str(action)) + + if not action: + return + + entry = Gtk.AccelMap.lookup_entry(action.get_accel_path()) + + if not entry: + return + + mapping = { + Gdk.KEY_equal: Gdk.KEY_KP_Equal, + Gdk.KEY_KP_Equal: Gdk.KEY_equal, + Gdk.KEY_minus: Gdk.KEY_KP_Subtract, + Gdk.KEY_KP_Subtract: Gdk.KEY_minus, + Gdk.KEY_0: Gdk.KEY_KP_0, + Gdk.KEY_KP_0: Gdk.KEY_0 + } + + if entry[0] in mapping: + key = mapping[entry[0]] + mod = entry[1] + + callback = self._proxy_callback_map[action.get_name()] + + self._accel_group.connect_group(key, mod, Gtk.ACCEL_LOCKED, callback) + self._proxy_mapping[action] = (key, mod) + + def _init_proxy_accels(self): + self._install_proxy('LargerTextAction') + self._install_proxy('SmallerTextAction') + self._install_proxy('NormalSizeAction') + + def do_deactivate(self): + # Remove any installed menu items + self._remove_menu() + + for view in self.window.get_views(): + self.remove_document_helper(view) + + self.window.remove_accel_group(self._accel_group) + + Gtk.AccelMap.get().disconnect(self._accel_map_handler_id) + + self._accel_group = None + self._action_group = None + + def _insert_menu(self): + # Get the GtkUIManager + manager = self.window.get_ui_manager() + + # Create a new action group + self._action_group = Gtk.ActionGroup("XedTextSizePluginActions") + self._action_group.add_actions([("LargerTextAction", None, _("_Larger Text"), + "equal", None, + self.on_larger_text_activate), + ("SmallerTextAction", None, _("S_maller Text"), + "minus", None, + self.on_smaller_text_activate), + ("NormalSizeAction", None, _("_Normal size"), + "0", None, + self.on_normal_size_activate)]) + + # Insert the action group + manager.insert_action_group(self._action_group) + + self._ui_id = manager.new_merge_id(); + + manager.add_ui(self._ui_id, + MENU_PATH, + "LargerTextAction", + "LargerTextAction", + Gtk.UIManagerItemType.MENUITEM, + False) + + manager.add_ui(self._ui_id, + MENU_PATH, + "SmallerTextAction", + "SmallerTextAction", + Gtk.UIManagerItemType.MENUITEM, + False) + + manager.add_ui(self._ui_id, + MENU_PATH, + "NormalSizeAction", + "NormalSizeAction", + Gtk.UIManagerItemType.MENUITEM, + False) + + def _remove_menu(self): + # Get the GtkUIManager + manager = self.window.get_ui_manager() + + # Remove the ui + manager.remove_ui(self._ui_id) + + # Remove the action group + manager.remove_action_group(self._action_group) + + # Make sure the manager updates + manager.ensure_update() + + def do_update_state(self): + self._action_group.set_sensitive(self.window.get_active_document() != None) + + def get_helper(self, view): + if not hasattr(view, "textsize_document_helper"): + return None + return view.textsize_document_helper + + def add_document_helper(self, view): + if self.get_helper(view) != None: + return + + DocumentHelper(view) + + def remove_document_helper(self, view): + helper = self.get_helper(view) + + if helper != None: + helper.stop() + + def call_helper(self, cb): + view = self.window.get_active_view() + + if view: + cb(self.get_helper(view)) + + # Menu activate handlers + def on_larger_text_activate(self, action, user_data=None): + self.call_helper(lambda helper: helper.larger_text()) + + def on_smaller_text_activate(self, action, user_data=None): + self.call_helper(lambda helper: helper.smaller_text()) + + def on_normal_size_activate(self, action, user_data=None): + self.call_helper(lambda helper: helper.normal_size()) + + def on_larger_text_accel(self, group, accel, key, mod): + self.call_helper(lambda helper: helper.larger_text()) + + def on_smaller_text_accel(self, group, accel, key, mod): + self.call_helper(lambda helper: helper.smaller_text()) + + def on_normal_size_accel(self, group, accel, key, mod): + self.call_helper(lambda helper: helper.normal_size()) + + def on_tab_added(self, window, tab): + self.add_document_helper(tab.get_view()) + + def on_tab_removed(self, window, tab): + self.remove_document_helper(tab.get_view()) + + def _remap_proxy(self, action): + # Remove previous proxy + + if action in self._proxy_mapping: + item = self._proxy_mapping[action] + self._accel_group.disconnect_key(item[0], item[1]) + + self._install_proxy(action) + + def on_accel_map_changed(self, accelmap, path, key, mod): + for action in self._action_group.list_actions(): + if action.get_accel_path() == path: + self._remap_proxy(action) + return diff --git a/plugins/textsize/textsize/documenthelper.py b/plugins/textsize/textsize/documenthelper.py new file mode 100644 index 0000000..0fb580d --- /dev/null +++ b/plugins/textsize/textsize/documenthelper.py @@ -0,0 +1,189 @@ +# -*- coding: utf-8 -*- +# +# documenthelper.py - Document helper +# +# Copyright (C) 2010 - Jesse van den Kieboom +# Copyright (C) 2017 - Linux Mint team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, +# Boston, MA 02110-1301, USA. + + +from .signals import Signals +from gi.repository import Gtk, Gdk, Pango + +class DocumentHelper(Signals): + def __init__(self, view): + Signals.__init__(self) + + self._view = view + + self.connect_signal(self._view, 'scroll-event', self.on_scroll_event) + self.connect_signal(self._view, 'button-press-event', self.on_button_press_event) + + self._view.textsize_document_helper = self + + self._default_font = None + self._last_font = None + self._font_tags = {} + + def stop(self): + if self._default_font: + self._view.override_font(self._default_font) + + self.remove_font_tags() + self.disconnect_signals(self._view) + + self._view.textsize_document_helper = None + + def remove_font_tags(self): + buf = self._view.get_buffer() + table = buf.get_tag_table() + + # Remove all the font tags + for size in self._font_tags: + tag = self._font_tags[size] + table.remove(tag) + + self._font_tags = {} + + def update_default_font(self): + context = self._view.get_style_context() + description = context.get_font(context.get_state()).copy() + + if not self._last_font or description.hash() != self._last_font.hash(): + self._default_font = description + + def get_font_tags(self, start, end): + tags = set() + + # Check all the know font tags + for size in self._font_tags: + tag = self._font_tags[size] + + if start.has_tag(tag): + tags.add(tag) + else: + cp = start.copy() + + if cp.forward_to_tag_toggle(tag) and cp.compare(end) < 0: + tags.add(tag) + + return list(tags) + + def set_font_size(self, amount): + self.update_default_font() + + context = self._view.get_style_context() + description = context.get_font(context.get_state()).copy() + + buf = self._view.get_buffer() + bounds = buf.get_selection_bounds() + size = description.get_size() / Pango.SCALE + + if not bounds: + description.set_size(max(1, (size + amount)) * Pango.SCALE) + + self._view.override_font(description) + self._last_font = description + else: + start = bounds[0] + end = bounds[1] + + tags = self.get_font_tags(start, end) + + if not tags: + # Simply use the overall font size as the base + newsize = size + amount + elif len(tags) == 1: + newsize = tags[0].props.font_desc.get_size() / Pango.SCALE + amount + else: + newsize = 0 + + for tag in tags: + newsize += tag.props.font_desc.get_size() / Pango.SCALE + + newsize = round(newsize / len(tags)) + + newsize = int(max(1, newsize)) + + if not newsize in self._font_tags: + newtag = buf.create_tag(None) + + desc = description + desc.set_size(newsize * Pango.SCALE) + + newtag.props.font_desc = desc + self._font_tags[newsize] = newtag + else: + newtag = self._font_tags[newsize] + + # Remove all the previous mix of tags + for tag in tags: + buf.remove_tag(tag, start, end) + + buf.apply_tag(newtag, start, end) + + def larger_text(self): + self.set_font_size(1) + + def smaller_text(self): + self.set_font_size(-1) + + def normal_size(self): + self.update_default_font() + + buf = self._view.get_buffer() + bounds = buf.get_selection_bounds() + + if not bounds: + self.remove_font_tags() + + self._view.override_font(self._default_font) + self._last_font = self._default_font + else: + tags = self.get_font_tags(bounds[0], bounds[1]) + + for tag in tags: + buf.remove_tag(tag, bounds[0], bounds[1]) + + def on_scroll_event(self, view, event): + state = event.state & Gtk.accelerator_get_default_mod_mask() + + if state != Gdk.ModifierType.CONTROL_MASK: + return False + + if event.direction == Gdk.ScrollDirection.UP: + self.larger_text() + return True + elif event.direction == Gdk.ScrollDirection.DOWN: + self.smaller_text() + return True + elif event.direction == Gdk.ScrollDirection.SMOOTH: + if event.delta_y > 0: + self.smaller_text() + elif event.delta_y < 0: + self.larger_text() + + return False + + def on_button_press_event(self, view, event): + state = event.state & Gtk.accelerator_get_default_mod_mask() + + if state == Gdk.ModifierType.CONTROL_MASK and event.button == 2: + self.normal_size() + return True + else: + return False diff --git a/plugins/textsize/textsize/signals.py b/plugins/textsize/textsize/signals.py new file mode 100644 index 0000000..2996a12 --- /dev/null +++ b/plugins/textsize/textsize/signals.py @@ -0,0 +1,69 @@ +class Signals(object): + def __init__(self): + self._signals = {} + + def _connect(self, obj, name, handler, connector): + ret = self._signals.setdefault(obj, {}) + + hid = connector(name, handler) + ret.setdefault(name, []).append(hid) + + return hid + + def connect_signal(self, obj, name, handler): + return self._connect(obj, name, handler, obj.connect) + + def connect_signal_after(self, obj, name, handler): + return self._connect(obj, name, handler, obj.connect_after) + + def disconnect_signals(self, obj): + if obj not in self._signals: + return False + + for name in self._signals[obj]: + for hid in self._signals[obj][name]: + obj.disconnect(hid) + + del self._signals[obj] + return True + + def block_signal(self, obj, name): + if obj not in self._signals: + return False + + if name not in self._signals[obj]: + return False + + for hid in self._signals[obj][name]: + obj.handler_block(hid) + + return True + + def unblock_signal(self, obj, name): + if obj not in self._signals: + return False + + if name not in self._signals[obj]: + return False + + for hid in self._signals[obj][name]: + obj.handler_unblock(hid) + + return True + + def disconnect_signal(self, obj, name): + if obj not in self._signals: + return False + + if name not in self._signals[obj]: + return False + + for hid in self._signals[obj][name]: + obj.disconnect(hid) + + del self._signals[obj][name] + + if len(self._signals[obj]) == 0: + del self._signals[obj] + + return True \ No newline at end of file diff --git a/xed/resources/ui/xed-ui.xml b/xed/resources/ui/xed-ui.xml index 6b5fe27..4da5c7a 100644 --- a/xed/resources/ui/xed-ui.xml +++ b/xed/resources/ui/xed-ui.xml @@ -56,6 +56,9 @@ + + + diff --git a/xed/xed-plugins-engine.c b/xed/xed-plugins-engine.c index 6b96972..c293c1e 100644 --- a/xed/xed-plugins-engine.c +++ b/xed/xed-plugins-engine.c @@ -58,6 +58,7 @@ xed_plugins_engine_init (XedPluginsEngine *engine) { gchar *typelib_dir; GError *error = NULL; + const GList *all_plugins, *l; xed_debug (DEBUG_PLUGINS); @@ -93,17 +94,6 @@ xed_plugins_engine_init (XedPluginsEngine *engine) error = NULL; } - // private_path = g_build_filename (LIBDIR, "girepository-1.0", NULL); - - // if (!g_irepository_require_private (g_irepository_get_default (), private_path, "Xed", "1.0", 0, &error)) - // { - // g_warning ("Could not load Xed repository: %s", error->message); - // g_error_free (error); - // error = NULL; - // } - - // g_free (private_path); - peas_engine_add_search_path (PEAS_ENGINE (engine), xed_dirs_get_user_plugins_dir (), xed_dirs_get_user_plugins_dir ()); @@ -117,6 +107,23 @@ xed_plugins_engine_init (XedPluginsEngine *engine) engine, "loaded-plugins", G_SETTINGS_BIND_DEFAULT); + + /* Load our builtin plugins */ + all_plugins = peas_engine_get_plugin_list (PEAS_ENGINE (engine)); + + for (l = all_plugins; l != NULL; l = l->next) + { + if (peas_plugin_info_is_builtin (l->data)) + { + gboolean loaded; + + loaded = peas_engine_load_plugin (PEAS_ENGINE (engine), l->data); + if (!loaded) + { + g_warning ("Failed to load builtin plugin: %s", peas_plugin_info_get_name (l->data)); + } + } + } } static void From ad8fa295f2f8a88190b1ef65a359195ec386616a Mon Sep 17 00:00:00 2001 From: JosephMcc Date: Sun, 12 Feb 2017 15:42:32 -0800 Subject: [PATCH 2/2] Sort: Turn sort into a simpler and undoable action Convert to a builtin plugin and remove the use of the dialog. Sort can now be triggered from the edit menu or by the F10 hot key. --- plugins/sort/Makefile.am | 5 +- plugins/sort/sort.plugin.desktop.in | 1 + plugins/sort/sort.ui | 274 ------------------------ plugins/sort/xed-sort-plugin.c | 312 +++++++--------------------- 4 files changed, 79 insertions(+), 513 deletions(-) delete mode 100644 plugins/sort/sort.ui diff --git a/plugins/sort/Makefile.am b/plugins/sort/Makefile.am index 13e65f8..075c56e 100644 --- a/plugins/sort/Makefile.am +++ b/plugins/sort/Makefile.am @@ -16,16 +16,13 @@ libsort_la_SOURCES = \ libsort_la_LDFLAGS = $(PLUGIN_LIBTOOL_FLAGS) libsort_la_LIBADD = $(XED_LIBS) -uidir = $(XED_PLUGINS_DATA_DIR)/sort -ui_DATA = sort.ui - plugin_in_files = sort.plugin.desktop.in %.plugin: %.plugin.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache plugin_DATA = $(plugin_in_files:.plugin.desktop.in=.plugin) -EXTRA_DIST = $(ui_DATA) $(plugin_in_files) +EXTRA_DIST = $(plugin_in_files) CLEANFILES = $(plugin_DATA) DISTCLEANFILES = $(plugin_DATA) diff --git a/plugins/sort/sort.plugin.desktop.in b/plugins/sort/sort.plugin.desktop.in index 4b56701..b8b47e6 100644 --- a/plugins/sort/sort.plugin.desktop.in +++ b/plugins/sort/sort.plugin.desktop.in @@ -7,3 +7,4 @@ Icon=gtk-sort-ascending Authors=Carlo Borreo ;Lee Mallabone ;Paolo Maggi ;Jorge Alberto Torres H. Copyright=Copyright © 2001 Carlo Borreo\nCopyright © 2002-2003 Lee Mallabone, Paolo Maggi\nCopyright © 2004-2005 Paolo Maggi Website=http://www.mate-desktop.org +Builtin=true diff --git a/plugins/sort/sort.ui b/plugins/sort/sort.ui deleted file mode 100644 index a74b713..0000000 --- a/plugins/sort/sort.ui +++ /dev/null @@ -1,274 +0,0 @@ - - - - - 100 - 1 - 10 - 1 - 0 - 1 - - - gtk-sort-ascending - 4 - - - Sort - GTK_WINDOW_TOPLEVEL - GTK_WIN_POS_NONE - False - False - True - True - False - False - GDK_WINDOW_TYPE_HINT_DIALOG - GDK_GRAVITY_NORTH_WEST - True - - - True - False - 0 - - - True - GTK_BUTTONBOX_END - - - True - True - True - gtk-cancel - True - GTK_RELIEF_NORMAL - True - - - - - True - True - True - GTK_RELIEF_NORMAL - True - sort_image - _Sort - True - - - - - True - True - True - gtk-help - True - GTK_RELIEF_NORMAL - True - - - - - 0 - False - True - GTK_PACK_END - - - - - 10 - True - False - 18 - - - True - False - 12 - - - True - True - _Reverse order - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - True - True - R_emove duplicates - True - GTK_RELIEF_NORMAL - True - False - False - True - - - 0 - False - False - - - - - True - True - _Ignore case - True - GTK_RELIEF_NORMAL - True - True - False - True - - - 0 - False - False - - - - - True - False - 6 - - - True - S_tart at column: - True - False - GTK_JUSTIFY_LEFT - False - False - 0.5 - 0.5 - 0 - 0 - col_num_spinbutton - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - False - False - - - - - True - True - 1 - 0 - True - GTK_UPDATE_ALWAYS - False - False - adjustment1 - - - 0 - False - True - - - - - 0 - True - True - - - - - 0 - True - True - - - - - True - False - 6 - - - True - gtk-dialog-warning - 4 - 0.5 - 0.5 - 0 - 0 - - - 0 - False - True - - - - - True - You cannot undo a sort operation - False - True - GTK_JUSTIFY_LEFT - True - False - 0 - 0.5 - 0 - 0 - PANGO_ELLIPSIZE_NONE - -1 - False - 0 - - - 0 - True - True - - - - - 0 - True - True - - - - - 0 - True - True - - - - - - button1 - button2 - button3 - - - diff --git a/plugins/sort/xed-sort-plugin.c b/plugins/sort/xed-sort-plugin.c index 80f33b0..6de63ea 100644 --- a/plugins/sort/xed-sort-plugin.c +++ b/plugins/sort/xed-sort-plugin.c @@ -28,6 +28,7 @@ #include "xed-sort-plugin.h" #include +#include #include #include @@ -56,24 +57,9 @@ struct _XedSortPluginPrivate GtkActionGroup *ui_action_group; guint ui_id; - GtkWidget *dialog; - GtkWidget *col_num_spinbutton; - GtkWidget *reverse_order_checkbutton; - GtkWidget *ignore_case_checkbutton; - GtkWidget *remove_dups_checkbutton; - GtkTextIter start, end; /* selection */ }; -typedef struct -{ - gint starting_column; - - guint ignore_case : 1; - guint reverse_order : 1; - guint remove_duplicates : 1; -} SortInfo; - enum { PROP_0, @@ -82,43 +68,22 @@ enum static void sort_cb (GtkAction *action, XedSortPlugin *plugin); -static void sort_real (XedSortPlugin *plugin); + +static void buffer_sort_lines (GtkSourceBuffer *buffer, + GtkTextIter *start, + GtkTextIter *end); static const GtkActionEntry action_entries[] = { { "Sort", - "view-sort-ascending-symbolic", - N_("S_ort..."), NULL, + N_("S_ort lines"), + "F10", N_("Sort the current document or selection"), G_CALLBACK (sort_cb) } }; -static void -sort_dialog_response_handler (GtkDialog *dlg, - gint res_id, - XedSortPlugin *plugin) -{ - xed_debug (DEBUG_PLUGINS); - - switch (res_id) - { - case GTK_RESPONSE_OK: - sort_real (plugin); - gtk_widget_destroy (GTK_WIDGET (dlg)); - break; - - case GTK_RESPONSE_HELP: - xed_app_show_help (XED_APP (g_application_get_default ()), GTK_WINDOW (dlg), NULL, "xed-sort-plugin"); - break; - - case GTK_RESPONSE_CANCEL: - gtk_widget_destroy (GTK_WIDGET (dlg)); - break; - } -} - /* NOTE: we store the current selection in the dialog since focusing * the text field (like the combo box) looses the documnent selection. * Storing the selection ONLY works because the dialog is modal */ @@ -141,158 +106,25 @@ get_current_selection (XedSortPlugin *plugin) } } -static void -create_sort_dialog (XedSortPlugin *plugin) -{ - XedSortPluginPrivate *priv; - GtkWidget *error_widget; - gboolean ret; - gchar *data_dir; - gchar *ui_file; - - xed_debug (DEBUG_PLUGINS); - - priv = plugin->priv; - - data_dir = peas_extension_base_get_data_dir (PEAS_EXTENSION_BASE (plugin)); - ui_file = g_build_filename (data_dir, "sort.ui", NULL); - ret = xed_utils_get_ui_objects (ui_file, - NULL, - &error_widget, - "sort_dialog", &priv->dialog, - "reverse_order_checkbutton", &priv->reverse_order_checkbutton, - "col_num_spinbutton", &priv->col_num_spinbutton, - "ignore_case_checkbutton", &priv->ignore_case_checkbutton, - "remove_dups_checkbutton", &priv->remove_dups_checkbutton, - NULL); - g_free (data_dir); - g_free (ui_file); - - if (!ret) - { - const gchar *err_message; - - err_message = gtk_label_get_label (GTK_LABEL (error_widget)); - xed_warning (GTK_WINDOW (priv->window), "%s", err_message); - - gtk_widget_destroy (error_widget); - - return; - } - - gtk_dialog_set_default_response (GTK_DIALOG (priv->dialog), GTK_RESPONSE_OK); - - g_signal_connect (priv->dialog, "destroy", G_CALLBACK (gtk_widget_destroyed), &priv->dialog); - g_signal_connect (priv->dialog, "response", G_CALLBACK (sort_dialog_response_handler), plugin); - - get_current_selection (plugin); -} - static void sort_cb (GtkAction *action, XedSortPlugin *plugin) { XedSortPluginPrivate *priv; - GtkWindowGroup *wg; + XedDocument *doc; xed_debug (DEBUG_PLUGINS); priv = plugin->priv; - create_sort_dialog (plugin); + doc = xed_window_get_active_document (priv->window); + g_return_if_fail (doc != NULL); - wg = xed_window_get_group (priv->window); - gtk_window_group_add_window (wg, GTK_WINDOW (priv->dialog)); + get_current_selection (plugin); - gtk_window_set_transient_for (GTK_WINDOW (priv->dialog), GTK_WINDOW (priv->window)); - gtk_window_set_modal (GTK_WINDOW (priv->dialog), TRUE); - - gtk_widget_show (GTK_WIDGET (priv->dialog)); -} - -/* Compares two strings for the sorting algorithm. Uses the UTF-8 processing - * functions in GLib to be as correct as possible.*/ -static gint -compare_algorithm (gconstpointer s1, - gconstpointer s2, - gpointer data) -{ - gint length1, length2; - gint ret; - gchar *string1, *string2; - gchar *substring1, *substring2; - gchar *key1, *key2; - SortInfo *sort_info; - - xed_debug (DEBUG_PLUGINS); - - sort_info = (SortInfo *) data; - g_return_val_if_fail (sort_info != NULL, -1); - - if (!sort_info->ignore_case) - { - string1 = *((gchar **) s1); - string2 = *((gchar **) s2); - } - else - { - string1 = g_utf8_casefold (*((gchar **) s1), -1); - string2 = g_utf8_casefold (*((gchar **) s2), -1); - } - - length1 = g_utf8_strlen (string1, -1); - length2 = g_utf8_strlen (string2, -1); - - if ((length1 < sort_info->starting_column) && - (length2 < sort_info->starting_column)) - { - ret = 0; - } - else if (length1 < sort_info->starting_column) - { - ret = -1; - } - else if (length2 < sort_info->starting_column) - { - ret = 1; - } - else if (sort_info->starting_column < 1) - { - key1 = g_utf8_collate_key (string1, -1); - key2 = g_utf8_collate_key (string2, -1); - ret = strcmp (key1, key2); - - g_free (key1); - g_free (key2); - } - else - { - /* A character column offset is required, so figure out - * the correct offset into the UTF-8 string. */ - substring1 = g_utf8_offset_to_pointer (string1, sort_info->starting_column); - substring2 = g_utf8_offset_to_pointer (string2, sort_info->starting_column); - - key1 = g_utf8_collate_key (substring1, -1); - key2 = g_utf8_collate_key (substring2, -1); - ret = strcmp (key1, key2); - - g_free (key1); - g_free (key2); - } - - /* Do the necessary cleanup. */ - if (sort_info->ignore_case) - { - g_free (string1); - g_free (string2); - } - - if (sort_info->reverse_order) - { - ret = -1 * ret; - } - - return ret; + buffer_sort_lines (GTK_SOURCE_BUFFER (doc), + &priv->start, + &priv->end); } static gchar * @@ -300,7 +132,6 @@ get_line_slice (GtkTextBuffer *buf, gint line) { GtkTextIter start, end; - char *ret; gtk_text_buffer_get_iter_at_line (buf, &start, line); end = start; @@ -310,97 +141,108 @@ get_line_slice (GtkTextBuffer *buf, gtk_text_iter_forward_to_line_end (&end); } - ret= gtk_text_buffer_get_slice (buf, &start, &end, TRUE); + return gtk_text_buffer_get_slice (buf, &start, &end, TRUE); +} - g_assert (ret != NULL); +typedef struct { + gchar *line; /* the original text to re-insert */ + gchar *key; /* the key to use for the comparison */ +} SortLine; - return ret; +static gint +compare_line (gconstpointer aptr, + gconstpointer bptr) +{ + const SortLine *a = aptr; + const SortLine *b = bptr; + + return g_strcmp0 (a->key, b->key); } static void -sort_real (XedSortPlugin *plugin) +buffer_sort_lines (GtkSourceBuffer *buffer, + GtkTextIter *start, + GtkTextIter *end) { - XedSortPluginPrivate *priv; - XedDocument *doc; - GtkTextIter start, end; - gint start_line, end_line; - gint i; - gchar *last_row = NULL; + GtkTextBuffer *text_buffer; + gint start_line; + gint end_line; gint num_lines; - gchar **lines; - SortInfo *sort_info; + SortLine *lines; + gchar *last_line = NULL; + gint i; - xed_debug (DEBUG_PLUGINS); + g_return_if_fail (GTK_SOURCE_IS_BUFFER (buffer)); + g_return_if_fail (start != NULL); + g_return_if_fail (end != NULL); - priv = plugin->priv; + text_buffer = GTK_TEXT_BUFFER (buffer); - doc = xed_window_get_active_document (priv->window); - g_return_if_fail (doc != NULL); + gtk_text_iter_order (start, end); - sort_info = g_slice_new (SortInfo); - sort_info->ignore_case = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->ignore_case_checkbutton)); - sort_info->reverse_order = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->reverse_order_checkbutton)); - sort_info->remove_duplicates = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (priv->remove_dups_checkbutton)); - sort_info->starting_column = gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (priv->col_num_spinbutton)) - 1; + start_line = gtk_text_iter_get_line (start); + end_line = gtk_text_iter_get_line (end); - start = priv->start; - end = priv->end; - start_line = gtk_text_iter_get_line (&start); - end_line = gtk_text_iter_get_line (&end); + /* Required for gtk_text_buffer_delete() */ + if (!gtk_text_iter_starts_line (start)) + { + gtk_text_iter_set_line_offset (start, 0); + } /* if we are at line start our last line is the previus one. * Otherwise the last line is the current one but we try to * move the iter after the line terminator */ - if (gtk_text_iter_get_line_offset (&end) == 0) + if (gtk_text_iter_starts_line (end)) { end_line = MAX (start_line, end_line - 1); } else { - gtk_text_iter_forward_line (&end); + gtk_text_iter_forward_line (end); + } + + if (start_line == end_line) + { + return; } num_lines = end_line - start_line + 1; - lines = g_new0 (gchar *, num_lines + 1); - - xed_debug_message (DEBUG_PLUGINS, "Building list..."); + lines = g_new0 (SortLine, num_lines); for (i = 0; i < num_lines; i++) { - lines[i] = get_line_slice (GTK_TEXT_BUFFER (doc), start_line + i); + gchar *line; + + lines[i].line = get_line_slice (text_buffer, start_line + i); + line = g_utf8_casefold (lines[i].line, -1); + lines[i].key = g_utf8_collate_key (line, -1); + + g_free (line); } - lines[num_lines] = NULL; + qsort (lines, num_lines, sizeof (SortLine), compare_line); - xed_debug_message (DEBUG_PLUGINS, "Sort list..."); + gtk_text_buffer_begin_user_action (text_buffer); - g_qsort_with_data (lines, num_lines, sizeof (gpointer), compare_algorithm, sort_info); - - xed_debug_message (DEBUG_PLUGINS, "Rebuilding document..."); - - gtk_source_buffer_begin_not_undoable_action (GTK_SOURCE_BUFFER (doc)); - - gtk_text_buffer_delete (GTK_TEXT_BUFFER (doc), &start, &end); + gtk_text_buffer_delete (text_buffer, start, end); for (i = 0; i < num_lines; i++) { - if (sort_info->remove_duplicates && last_row != NULL && (strcmp (last_row, lines[i]) == 0)) - { - continue; - } + gtk_text_buffer_insert (text_buffer, start, lines[i].line, -1); + gtk_text_buffer_insert (text_buffer, start, "\n", -1); - gtk_text_buffer_insert (GTK_TEXT_BUFFER (doc), &start, lines[i], -1); - gtk_text_buffer_insert (GTK_TEXT_BUFFER (doc), &start, "\n", -1); - - last_row = lines[i]; + last_line = lines[i].line; } - gtk_source_buffer_end_not_undoable_action (GTK_SOURCE_BUFFER (doc)); + gtk_text_buffer_end_user_action (text_buffer); - g_strfreev (lines); - g_slice_free (SortInfo, sort_info); + for (i = 0; i < num_lines; i++) + { + g_free (lines[i].line); + g_free (lines[i].key); + } - xed_debug_message (DEBUG_PLUGINS, "Done."); + g_free (lines); } static void