Skip to content

Commit

Permalink
Retrocompatible python3 implementation of Gtk.Template (Dustin Spicuzza)
Browse files Browse the repository at this point in the history
  • Loading branch information
maoschanz committed Jun 27, 2021
1 parent e89e8db commit 5609ed6
Show file tree
Hide file tree
Showing 5 changed files with 298 additions and 25 deletions.
266 changes: 266 additions & 0 deletions src/gi_composites.py
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


10 changes: 6 additions & 4 deletions src/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ configure_file(

drawing_sources = [
'__init__.py',
'gi_composites.py',
'main.py',

'window.py',
Expand Down
20 changes: 11 additions & 9 deletions src/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

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
_gsettings = 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, \
Expand Down
26 changes: 14 additions & 12 deletions src/window.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 5609ed6

Please sign in to comment.