mirror of
https://github.com/libratbag/piper.git
synced 2026-04-23 07:48:36 -04:00
Window: use GtkTemplate to instantiate from a GtkBuilder template
In C/GTK+, templates allow one to define the UI in .ui files, from which implementations can be instantiated. This significantly reduces boilerplate code that creates widgets, sets their desired properties and connects their signals[1, 2]. For several reasons, this does not work in Python[3]. gi_composites.py[4] is an implementation in Python that does make this work. It's been used by several projects for a few months now and none report issues. See the project's README for more information[5]. Taking these facts into mind, let's experiment for a bit and see how it can serve Piper. [1]: https://wiki.gnome.org/HowDoI/CustomWidgets#Templates [2]: https://blogs.gnome.org/tvb/2013/04/09/announcing-composite-widget-templates/ [3]: https://bugzilla.gnome.org/show_bug.cgi?id=701843 [4]: https://github.com/virtuald/pygi-composite-templates/ [5]: https://github.com/virtuald/pygi-composite-templates/blob/master/README.rst
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/org/freedesktop/Piper">
|
||||
<file preprocess="xml-stripblanks">aboutDialog.ui</file>
|
||||
<file>404.svg</file>
|
||||
<!-- Using this alias, GtkApplication will automatically pick it up for us. -->
|
||||
<file alias="gtk/menus.ui" preprocess="xml-stripblanks">menus.ui</file>
|
||||
</gresource>
|
||||
<gresource prefix="/org/freedesktop/Piper">
|
||||
<file>404.svg</file>
|
||||
|
||||
<file preprocess="xml-stripblanks">aboutDialog.ui</file>
|
||||
<file preprocess="xml-stripblanks">window.ui</file>
|
||||
<!-- Using this alias, GtkApplication will automatically pick it up for us. -->
|
||||
<file alias="gtk/menus.ui" preprocess="xml-stripblanks">menus.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
33
data/window.ui
Normal file
33
data/window.ui
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.20.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<template class="ApplicationWindow" parent="GtkApplicationWindow">
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="transition_duration">400</property>
|
||||
<property name="transition_type">slide-left-right</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="titlebar">
|
||||
<object class="GtkHeaderBar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child type="title">
|
||||
<object class="GtkStackSwitcher" id="stackswitcher">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stack">stack</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
16
piper.in
16
piper.in
@@ -1,10 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import gettext
|
||||
import gi
|
||||
import locale
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
|
||||
gi.require_version('Gio', '2.0')
|
||||
@@ -12,9 +9,8 @@ gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gio, Gtk
|
||||
|
||||
@devel@
|
||||
|
||||
# Must come after development code injection.
|
||||
from piper.application import Application
|
||||
resource = Gio.resource_load(os.path.join('@pkgdatadir@', 'piper.gresource'))
|
||||
Gio.Resource._register(resource)
|
||||
|
||||
|
||||
def install_excepthook():
|
||||
@@ -30,6 +26,11 @@ def install_excepthook():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import gettext
|
||||
import locale
|
||||
import signal
|
||||
from piper.application import Application
|
||||
|
||||
install_excepthook()
|
||||
|
||||
locale.bindtextdomain('piper', '@localedir@')
|
||||
@@ -37,9 +38,6 @@ if __name__ == "__main__":
|
||||
gettext.bindtextdomain('piper', '@localedir@')
|
||||
gettext.textdomain('piper')
|
||||
|
||||
resource = Gio.resource_load(os.path.join('@pkgdatadir@', 'piper.gresource'))
|
||||
Gio.Resource._register(resource)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
exit_status = Application().run(sys.argv)
|
||||
sys.exit(exit_status)
|
||||
|
||||
269
piper/gi_composites.py
Normal file
269
piper/gi_composites.py
Normal file
@@ -0,0 +1,269 @@
|
||||
#
|
||||
# Copyright (C) 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']
|
||||
|
||||
|
||||
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'''
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
|
||||
# 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:
|
||||
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
|
||||
@@ -14,63 +14,32 @@
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
from gettext import gettext as _
|
||||
from .gi_composites import GtkTemplate
|
||||
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk
|
||||
|
||||
from .mousemap import MouseMap
|
||||
|
||||
|
||||
@GtkTemplate(ui="/org/freedesktop/Piper/window.ui")
|
||||
class Window(Gtk.ApplicationWindow):
|
||||
"""A Gtk.ApplicationWindow subclass to implement the main application
|
||||
window."""
|
||||
|
||||
__gtype_name__ = "ApplicationWindow"
|
||||
|
||||
stack = GtkTemplate.Child()
|
||||
|
||||
def __init__(self, ratbag, *args, **kwargs):
|
||||
"""Instantiates a new Window.
|
||||
|
||||
@param ratbag The ratbag instance to connect to, as ratbagd.Ratbag
|
||||
"""
|
||||
Gtk.ApplicationWindow.__init__(self, *args, **kwargs)
|
||||
self.init_template()
|
||||
|
||||
self._ratbag = ratbag
|
||||
|
||||
stack = Gtk.Stack()
|
||||
self.add(stack)
|
||||
stack.props.homogeneous = True
|
||||
stack.props.transition_duration = 500
|
||||
stack.props.transition_type = Gtk.StackTransitionType.SLIDE_LEFT_RIGHT
|
||||
|
||||
device = self._fetch_ratbag_device()
|
||||
stack.add_titled(self._setup_buttons_page(device), "buttons", _("Buttons"))
|
||||
self.set_titlebar(self._setup_headerbar(stack))
|
||||
|
||||
def _setup_headerbar(self, stack):
|
||||
headerbar = Gtk.HeaderBar()
|
||||
|
||||
sizeGroup = Gtk.SizeGroup.new(Gtk.SizeGroupMode.HORIZONTAL)
|
||||
self._quit = Gtk.Button.new_with_mnemonic(_("_Quit"))
|
||||
self._quit.connect("clicked", lambda button, data: data.destroy(), self)
|
||||
sizeGroup.add_widget(self._quit)
|
||||
headerbar.pack_start(self._quit)
|
||||
|
||||
switcher = Gtk.StackSwitcher()
|
||||
switcher.set_stack(stack)
|
||||
headerbar.set_custom_title(switcher)
|
||||
|
||||
return headerbar
|
||||
|
||||
def _setup_buttons_page(self, device):
|
||||
mousemap = MouseMap("#Buttons", device, spacing=20, border_width=20)
|
||||
sizegroup = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
|
||||
profile = device.active_profile
|
||||
for button in profile.buttons:
|
||||
mapbutton = Gtk.Button("Button {}".format(button.index))
|
||||
mousemap.add(mapbutton, "#button{}".format(button.index))
|
||||
sizegroup.add_widget(mapbutton)
|
||||
return mousemap
|
||||
self._device = self._fetch_ratbag_device()
|
||||
|
||||
def _fetch_ratbag_device(self):
|
||||
"""Get the first ratbag device available. If there are multiple
|
||||
|
||||
Reference in New Issue
Block a user