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
This commit is contained in:
JosephMcc 2017-02-11 12:00:16 -08:00
parent 98c983f144
commit 01bada3e7e
11 changed files with 557 additions and 13 deletions

View File

@ -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

3
debian/xed.install vendored
View File

@ -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/

View File

@ -5,6 +5,7 @@ DIST_SUBDIRS = \
sort \
spell \
taglist \
textsize \
time \
trailsave
@ -15,6 +16,7 @@ SUBDIRS = \
sort \
spell \
taglist \
textsize \
time \
trailsave

View File

@ -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

View File

@ -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 <steve@istique.net>;Linux Mint team
Copyright=Copyright © 2017 by the authors
Website=https://github.com/linuxmint
Builtin=true

View File

@ -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

View File

@ -0,0 +1,233 @@
# -*- coding: utf-8 -*-
#
# __init__.py - Text size plugin
#
# Copyright (C) 2008 - Konstantin Mikhaylov <jtraub.devel@gmail.com>
# Copyright (C) 2009 - Wouter Bolsterlee <wbolster@gnome.org>
# Copyright (C) 2010 - Ignacio Casal Quinteiro <icq@gnome.org>
# Copyright (C) 2010 - Jesse van den Kieboom <jessevdk@gnome.org>
# Copyright (C) 2017 - Linux Mint team <https://github.com/linuxmint>
#
# 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"),
"<Ctrl>equal", None,
self.on_larger_text_activate),
("SmallerTextAction", None, _("S_maller Text"),
"<Ctrl>minus", None,
self.on_smaller_text_activate),
("NormalSizeAction", None, _("_Normal size"),
"<Ctrl>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

View File

@ -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

View File

@ -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

View File

@ -56,6 +56,9 @@
<menuitem name="ViewFullscreenMenu" action="ViewFullscreen"/>
<separator/>
<menuitem name="ViewWordWrapMenu" action="ViewWordWrap"/>
<separator/>
<placeholder name="ViewOps_1" />
<separator/>
<menu name="ViewHighlightModeMenu" action="ViewHighlightMode">
<placeholder name="LanguagesMenuPlaceholder">
</placeholder>

View File

@ -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