Skip to content
Snippets Groups Projects
gi_composites.py 8.84 KiB
Newer Older
Nico's avatar
Nico committed
#
# Copyright 2015 Dustin Spicuzza <dustin@virtualroadside.com>
#
# 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']

Nico's avatar
Nico committed
class GtkTemplateWarning(UserWarning):
    pass

Nico's avatar
Nico committed
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__)

Nico's avatar
Nico committed
    if template_inst is None:  # This should never happen
Nico's avatar
Nico committed
        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
Nico's avatar
Nico committed
                # cls.bind_template_callback_full(name, o)
Nico's avatar
Nico committed
        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'''

    # TODO: could disallow using a metaclass.. but this is good enough
    # .. if you disagree, feel free to fix it and issue a PR :)
    if self.__class__ is not cls:
Nico's avatar
Nico committed
        raise TypeError("Inheritance from classes with @GtkTemplate decorators"
                        " is not allowed at this time")
Nico's avatar
Nico committed

    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)


# TODO: Make it easier for IDE to introspect this
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

            TODO: Alternatively, could wait until first class instantiation
                  before registering templates? Would need a metaclass...
        '''
        _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:
Nico's avatar
Nico committed
            template_bytes = \
             Gio.resources_lookup_data(self.ui, Gio.ResourceLookupFlags.NONE)
Nico's avatar
Nico committed
        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


GtkTemplate = _GtkTemplate