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 @@ + + + + + + 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