-
-
Notifications
You must be signed in to change notification settings - Fork 101
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Retrocompatible python3 implementation of Gtk.Template (Dustin Spicuzza)
- Loading branch information
Showing
5 changed files
with
298 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
# | ||
# Copyright 2015 Dustin Spicuzza <[email protected]> | ||
# | ||
# 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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -33,6 +33,7 @@ configure_file( | |
|
||
drawing_sources = [ | ||
'__init__.py', | ||
'gi_composites.py', | ||
'main.py', | ||
|
||
'window.py', | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters