diff --git a/src/gi_composites.py b/src/gi_composites.py new file mode 100644 index 00000000..60584a84 --- /dev/null +++ b/src/gi_composites.py @@ -0,0 +1,266 @@ +# +# Copyright 2015 Dustin Spicuzza +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 +# USA + +from os.path import abspath, join + +import inspect +import warnings + +from gi.repository import Gio +from gi.repository import GLib +from gi.repository import GObject +from gi.repository import Gtk + +__all__ = ['GtkTemplate'] + +class GtkTemplateWarning(UserWarning): + pass + +def _connect_func(builder, obj, signal_name, handler_name, + connect_object, flags, cls): + '''Handles GtkBuilder signal connect events''' + + if connect_object is None: + extra = () + else: + extra = (connect_object,) + + # The handler name refers to an attribute on the template instance, + # so ask GtkBuilder for the template instance + template_inst = builder.get_object(cls.__gtype_name__) + + if template_inst is None: # This should never happen + errmsg = "Internal error: cannot find template instance! obj: %s; " \ + "signal: %s; handler: %s; connect_obj: %s; class: %s" % \ + (obj, signal_name, handler_name, connect_object, cls) + warnings.warn(errmsg, GtkTemplateWarning) + return + + handler = getattr(template_inst, handler_name) + + if flags == GObject.ConnectFlags.AFTER: + obj.connect_after(signal_name, handler, *extra) + else: + obj.connect(signal_name, handler, *extra) + + template_inst.__connected_template_signals__.add(handler_name) + + +def _register_template(cls, template_bytes): + '''Registers the template for the widget and hooks init_template''' + + # This implementation won't work if there are nested templates, but + # we can't do that anyways due to PyGObject limitations so it's ok + + if not hasattr(cls, 'set_template'): + raise TypeError("Requires PyGObject 3.13.2 or greater") + + cls.set_template(template_bytes) + + bound_methods = set() + bound_widgets = set() + + # Walk the class, find marked callbacks and child attributes + for name in dir(cls): + + o = getattr(cls, name, None) + + if inspect.ismethod(o): + if hasattr(o, '_gtk_callback'): + bound_methods.add(name) + # Don't need to call this, as connect_func always gets called + #cls.bind_template_callback_full(name, o) + elif isinstance(o, _Child): + cls.bind_template_child_full(name, True, 0) + bound_widgets.add(name) + + # Have to setup a special connect function to connect at template init + # because the methods are not bound yet + cls.set_connect_func(_connect_func, cls) + + cls.__gtemplate_methods__ = bound_methods + cls.__gtemplate_widgets__ = bound_widgets + + base_init_template = cls.init_template + cls.init_template = lambda s: _init_template(s, cls, base_init_template) + + +def _init_template(self, cls, base_init_template): + '''This would be better as an override for Gtk.Widget''' + + if self.__class__ is not cls: + raise TypeError("Inheritance from classes with @GtkTemplate decorators " + "is not allowed at this time") + + connected_signals = set() + self.__connected_template_signals__ = connected_signals + + base_init_template(self) + + for name in self.__gtemplate_widgets__: + widget = self.get_template_child(cls, name) + self.__dict__[name] = widget + + if widget is None: + # Bug: if you bind a template child, and one of them was + # not present, then the whole template is broken (and + # it's not currently possible for us to know which + # one is broken either -- but the stderr should show + # something useful with a Gtk-CRITICAL message) + raise AttributeError("A missing child widget was set using " + "GtkTemplate.Child and the entire " + "template is now broken (widgets: %s)" % + ', '.join(self.__gtemplate_widgets__)) + + for name in self.__gtemplate_methods__.difference(connected_signals): + errmsg = ("Signal '%s' was declared with @GtkTemplate.Callback " + + "but was not present in template") % name + warnings.warn(errmsg, GtkTemplateWarning) + +class _Child(object): + ''' + Assign this to an attribute in your class definition and it will + be replaced with a widget defined in the UI file when init_template + is called + ''' + + __slots__ = [] + + @staticmethod + def widgets(count): + ''' + Allows declaring multiple widgets with less typing:: + + button \ + label1 \ + label2 = GtkTemplate.Child.widgets(3) + ''' + return [_Child() for _ in range(count)] + + +class _GtkTemplate(object): + ''' + Use this class decorator to signify that a class is a composite + widget which will receive widgets and connect to signals as + defined in a UI template. You must call init_template to + cause the widgets/signals to be initialized from the template:: + + @GtkTemplate(ui='foo.ui') + class Foo(Gtk.Box): + + def __init__(self): + super(Foo, self).__init__() + self.init_template() + + The 'ui' parameter can either be a file path or a GResource resource + path:: + + @GtkTemplate(ui='/org/example/foo.ui') + class Foo(Gtk.Box): + pass + + To connect a signal to a method on your instance, do:: + + @GtkTemplate.Callback + def on_thing_happened(self, widget): + pass + + To create a child attribute that is retrieved from your template, + add this to your class definition:: + + @GtkTemplate(ui='foo.ui') + class Foo(Gtk.Box): + + widget = GtkTemplate.Child() + + + Note: This is implemented as a class decorator, but if it were + included with PyGI I suspect it might be better to do this + in the GObject metaclass (or similar) so that init_template + can be called automatically instead of forcing the user to do it. + + .. note:: Due to limitations in PyGObject, you may not inherit from + python objects that use the GtkTemplate decorator. + ''' + + __ui_path__ = None + + @staticmethod + def Callback(f): + ''' + Decorator that designates a method to be attached to a signal from + the template + ''' + f._gtk_callback = True + return f + + + Child = _Child + + @staticmethod + def set_ui_path(*path): + ''' + If using file paths instead of resources, call this *before* + loading anything that uses GtkTemplate, or it will fail to load + your template file + + :param path: one or more path elements, will be joined together + to create the final path + ''' + _GtkTemplate.__ui_path__ = abspath(join(*path)) + + + def __init__(self, ui): + self.ui = ui + + def __call__(self, cls): + + if not issubclass(cls, Gtk.Widget): + raise TypeError("Can only use @GtkTemplate on Widgets") + + # Nested templates don't work + if hasattr(cls, '__gtemplate_methods__'): + raise TypeError("Cannot nest template classes") + + # Load the template either from a resource path or a file + # - Prefer the resource path first + + try: + template_bytes = Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE) + except GLib.GError: + ui = self.ui + if isinstance(ui, (list, tuple)): + ui = join(ui) + + if _GtkTemplate.__ui_path__ is not None: + ui = join(_GtkTemplate.__ui_path__, ui) + + with open(ui, 'rb') as fp: + template_bytes = GLib.Bytes.new(fp.read()) + + _register_template(cls, template_bytes) + return cls + + +# Future shim support if this makes it into PyGI? +#if hasattr(Gtk, 'GtkTemplate'): +# GtkTemplate = lambda c: c +#else: +GtkTemplate = _GtkTemplate + + diff --git a/src/image.py b/src/image.py index dda4564c..ac196f8b 100644 --- a/src/image.py +++ b/src/image.py @@ -17,6 +17,7 @@ import cairo, random, math from gi.repository import Gtk, Gdk, Gio, GdkPixbuf, Pango, GLib +from .gi_composites import GtkTemplate from .history_manager import DrHistoryManager from .selection_manager import DrSelectionManager from .properties import DrPropertiesDialog @@ -38,18 +39,19 @@ def __init__(self, pb_name): ################################################################################ -@Gtk.Template(resource_path='/com/github/maoschanz/drawing/ui/image.ui') +@GtkTemplate(ui='/com/github/maoschanz/drawing/ui/image.ui') class DrImage(Gtk.Box): __gtype_name__ = 'DrImage' - _drawing_area = Gtk.Template.Child() - _h_scrollbar = Gtk.Template.Child() - _v_scrollbar = Gtk.Template.Child() + _drawing_area = GtkTemplate.Child() + _h_scrollbar = GtkTemplate.Child() + _v_scrollbar = GtkTemplate.Child() SCALE_FACTOR = 1.0 # XXX doesn't work well enough to be anything else def __init__(self, window, **kwargs): super().__init__(**kwargs) + self.init_template() self.window = window self.gfile = None diff --git a/src/meson.build b/src/meson.build index 3dfa3340..0309c9e1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -33,6 +33,7 @@ configure_file( drawing_sources = [ '__init__.py', + 'gi_composites.py', 'main.py', 'window.py', diff --git a/src/preferences.py b/src/preferences.py index 65216c8b..d7dc9f69 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -16,22 +16,23 @@ # along with this program. If not, see . from gi.repository import Gtk, Gio, Gdk +from .gi_composites import GtkTemplate from .utilities import utilities_add_unit_to_spinbtn -@Gtk.Template(resource_path='/com/github/maoschanz/drawing/ui/preferences.ui') +@GtkTemplate(ui='/com/github/maoschanz/drawing/ui/preferences.ui') class DrPrefsWindow(Gtk.Window): __gtype_name__ = 'DrPrefsWindow' - content_area = Gtk.Template.Child() - stack = Gtk.Template.Child() + content_area = GtkTemplate.Child() + stack = GtkTemplate.Child() - page_images = Gtk.Template.Child() - page_tools = Gtk.Template.Child() - page_advanced = Gtk.Template.Child() + page_images = GtkTemplate.Child() + page_tools = GtkTemplate.Child() + page_advanced = GtkTemplate.Child() - adj_width = Gtk.Template.Child() - adj_height = Gtk.Template.Child() - adj_preview = Gtk.Template.Child() + adj_width = GtkTemplate.Child() + adj_height = GtkTemplate.Child() + adj_preview = GtkTemplate.Child() _current_grid = None _grid_attach_cpt = 0 @@ -39,6 +40,7 @@ class DrPrefsWindow(Gtk.Window): def __init__(self, is_beta, wants_csd, **kwargs): super().__init__(**kwargs) + self.init_template() if wants_csd: header_bar = Gtk.HeaderBar( visible=True, \ diff --git a/src/window.py b/src/window.py index fa2b3c20..2f60450b 100644 --- a/src/window.py +++ b/src/window.py @@ -18,6 +18,7 @@ # Import libs import os, traceback from gi.repository import Gtk, Gdk, Gio, GdkPixbuf, GLib +from .gi_composites import GtkTemplate # Import tools from .tool_arc import ToolArc @@ -76,27 +77,28 @@ ################################################################################ -@Gtk.Template(resource_path=UI_PATH+'window.ui') +@GtkTemplate(ui=UI_PATH+'window.ui') class DrWindow(Gtk.ApplicationWindow): __gtype_name__ = 'DrWindow' gsettings = Gio.Settings.new('com.github.maoschanz.drawing') # Window empty widgets - tools_flowbox = Gtk.Template.Child() - toolbar_box = Gtk.Template.Child() - info_bar = Gtk.Template.Child() - info_label = Gtk.Template.Child() - info_action = Gtk.Template.Child() - notebook = Gtk.Template.Child() - bottom_panes_box = Gtk.Template.Child() - unfullscreen_btn = Gtk.Template.Child() - bottom_meta_box = Gtk.Template.Child() - tools_scrollable_box = Gtk.Template.Child() - tools_nonscrollable_box = Gtk.Template.Child() + tools_flowbox = GtkTemplate.Child() + toolbar_box = GtkTemplate.Child() + info_bar = GtkTemplate.Child() + info_label = GtkTemplate.Child() + info_action = GtkTemplate.Child() + notebook = GtkTemplate.Child() + bottom_panes_box = GtkTemplate.Child() + unfullscreen_btn = GtkTemplate.Child() + bottom_meta_box = GtkTemplate.Child() + tools_scrollable_box = GtkTemplate.Child() + tools_nonscrollable_box = GtkTemplate.Child() def __init__(self, **kwargs): super().__init__(**kwargs) + self.init_template() self.app = kwargs['application'] self.pointer_to_current_page = None # this ridiculous hack allows to