diff --git a/src/gi_composites.py b/src/gi_composites.py new file mode 100644 index 00000000..a683e332 --- /dev/null +++ b/src/gi_composites.py @@ -0,0 +1,265 @@ +# +# 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 498a172e..dec957dd 100644 --- a/src/image.py +++ b/src/image.py @@ -16,6 +16,7 @@ # along with this program. If not, see . from gi.repository import Gtk, Gdk, Gio, GdkPixbuf, GLib, Pango +from .gi_composites import GtkTemplate import cairo from .utilities import utilities_save_pixbuf_at @@ -28,18 +29,19 @@ class DrawingMotionBehavior(): ################################################################################ -@Gtk.Template(resource_path='/com/github/maoschanz/drawing/ui/image.ui') +@GtkTemplate(ui='/com/github/maoschanz/drawing/ui/image.ui') class DrawingImage(Gtk.Box): __gtype_name__ = 'DrawingImage' - 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() CLOSING_PRECISION = 10 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 f6f7f2bf..4ef17db4 100644 --- a/src/meson.build +++ b/src/meson.build @@ -27,6 +27,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 9825842a..8424fcc3 100644 --- a/src/preferences.py +++ b/src/preferences.py @@ -16,30 +16,32 @@ # along with this program. If not, see . from gi.repository import Gtk, Gio, GLib, Gdk +from .gi_composites import GtkTemplate from .utilities import utilities_add_px_to_spinbutton SETTINGS_SCHEMA = 'com.github.maoschanz.drawing' -@Gtk.Template(resource_path='/com/github/maoschanz/drawing/ui/preferences.ui') +@GtkTemplate(ui='/com/github/maoschanz/drawing/ui/preferences.ui') class DrawingPrefsWindow(Gtk.Window): __gtype_name__ = 'DrawingPrefsWindow' - content_area = Gtk.Template.Child() - stack_switcher = Gtk.Template.Child() + content_area = GtkTemplate.Child() + stack_switcher = 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() _settings = Gio.Settings.new('com.github.maoschanz.drawing') def __init__(self, is_beta, wants_csd, **kwargs): super().__init__(**kwargs) + self.init_template() if wants_csd: header_bar = Gtk.HeaderBar(visible=True, title=_("Preferences"), \ show_close_button=True) diff --git a/src/window.py b/src/window.py index 08b47be0..86b6a19f 100644 --- a/src/window.py +++ b/src/window.py @@ -18,6 +18,7 @@ import os from gi.repository import Gtk, Gdk, Gio, GdkPixbuf, GLib +from .gi_composites import GtkTemplate from .tool_arc import ToolArc from .tool_circle import ToolCircle @@ -50,41 +51,42 @@ UI_PATH = '/com/github/maoschanz/drawing/ui/' -@Gtk.Template(resource_path=UI_PATH+'window.ui') +@GtkTemplate(ui=UI_PATH+'window.ui') class DrawingWindow(Gtk.ApplicationWindow): __gtype_name__ = 'DrawingWindow' _settings = Gio.Settings.new('com.github.maoschanz.drawing') # Window empty widgets - tools_panel = Gtk.Template.Child() - toolbar_box = Gtk.Template.Child() - info_bar = Gtk.Template.Child() - info_label = Gtk.Template.Child() - notebook = Gtk.Template.Child() - bottom_panel_box = Gtk.Template.Child() - tools_scrollable_box = Gtk.Template.Child() - tools_nonscrollable_box = Gtk.Template.Child() + tools_panel = GtkTemplate.Child() + toolbar_box = GtkTemplate.Child() + info_bar = GtkTemplate.Child() + info_label = GtkTemplate.Child() + notebook = GtkTemplate.Child() + bottom_panel_box = GtkTemplate.Child() + tools_scrollable_box = GtkTemplate.Child() + tools_nonscrollable_box = GtkTemplate.Child() # Default bottom panel - bottom_panel = Gtk.Template.Child() - color_box = Gtk.Template.Child() - color_menu_btn_l = Gtk.Template.Child() - color_menu_btn_r = Gtk.Template.Child() - l_btn_image = Gtk.Template.Child() - r_btn_image = Gtk.Template.Child() - thickness_spinbtn = Gtk.Template.Child() - options_btn = Gtk.Template.Child() - options_label = Gtk.Template.Child() - options_long_box = Gtk.Template.Child() - options_short_box = Gtk.Template.Child() - minimap_btn = Gtk.Template.Child() - minimap_icon = Gtk.Template.Child() - minimap_label = Gtk.Template.Child() - minimap_arrow = Gtk.Template.Child() + bottom_panel = GtkTemplate.Child() + color_box = GtkTemplate.Child() + color_menu_btn_l = GtkTemplate.Child() + color_menu_btn_r = GtkTemplate.Child() + l_btn_image = GtkTemplate.Child() + r_btn_image = GtkTemplate.Child() + thickness_spinbtn = GtkTemplate.Child() + options_btn = GtkTemplate.Child() + options_label = GtkTemplate.Child() + options_long_box = GtkTemplate.Child() + options_short_box = GtkTemplate.Child() + minimap_btn = GtkTemplate.Child() + minimap_icon = GtkTemplate.Child() + minimap_label = GtkTemplate.Child() + minimap_arrow = GtkTemplate.Child() def __init__(self, **kwargs): super().__init__(**kwargs) + self.init_template() self.app = kwargs['application'] self.header_bar = None