diff --git a/data/piper.gresource.xml b/data/piper.gresource.xml
index 777e88c..5a2a24f 100644
--- a/data/piper.gresource.xml
+++ b/data/piper.gresource.xml
@@ -1,9 +1,11 @@
-
- aboutDialog.ui
- 404.svg
-
- menus.ui
-
+
+ 404.svg
+
+ aboutDialog.ui
+ window.ui
+
+ menus.ui
+
diff --git a/data/window.ui b/data/window.ui
new file mode 100644
index 0000000..e6f8b08
--- /dev/null
+++ b/data/window.ui
@@ -0,0 +1,33 @@
+
+
+
+
+
+ False
+
+
+
+
+
+
+
+
diff --git a/piper.in b/piper.in
index 7edfb6b..b214fa2 100755
--- a/piper.in
+++ b/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)
diff --git a/piper/gi_composites.py b/piper/gi_composites.py
new file mode 100644
index 0000000..ef442eb
--- /dev/null
+++ b/piper/gi_composites.py
@@ -0,0 +1,269 @@
+#
+# Copyright (C) 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'''
+
+ # 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
diff --git a/piper/window.py b/piper/window.py
index e27bc3c..863b933 100644
--- a/piper/window.py
+++ b/piper/window.py
@@ -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