summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/gui/app.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--share/extensions/inkex/gui/app.py176
1 files changed, 176 insertions, 0 deletions
diff --git a/share/extensions/inkex/gui/app.py b/share/extensions/inkex/gui/app.py
new file mode 100644
index 0000000..6b2a0ab
--- /dev/null
+++ b/share/extensions/inkex/gui/app.py
@@ -0,0 +1,176 @@
+# coding=utf-8
+#
+# Copyright 2011-2022 Martin Owens <doctormo@geek-2.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>
+#
+"""
+Gtk Application base classes, providing a way to load a GtkBuilder
+with a specific glade/ui file conaining windows, and building
+a usable pythonic interface from them.
+"""
+import os
+import signal
+import logging
+
+from gi.repository import Gtk, GLib
+
+
+class GtkApp:
+ """
+ This wraps gtk builder and allows for some extra functionality with
+ windows, especially the management of gtk main loops.
+
+ Args:
+ start_loop (bool, optional): If set to true will start a new gtk main loop.
+ Defaults to False.
+ start_gui (bool, optional): Used as local propertes if unset and passed to
+ primary window when loaded. Defaults to True.
+ """
+
+ @property
+ def prefix(self):
+ """Folder prefix added to ui_dir"""
+ return self.kwargs.get("prefix", "")
+
+ @property
+ def windows(self):
+ """Returns a list of windows for this app"""
+ return self.kwargs.get("windows", [])
+
+ @property
+ def ui_dir(self):
+ """This is often the local directory"""
+ return self.kwargs.get("ui_dir", "./")
+
+ @property
+ def ui_file(self):
+ """If a single file is used for multiple windows"""
+ return self.kwargs.get("ui_file", None)
+
+ @property
+ def app_name(self):
+ """Set this variable in your class"""
+ try:
+ return self.kwargs["app_name"]
+ except KeyError:
+ raise NotImplementedError(
+ "App name is not set, pass in or set 'app_name' in class."
+ )
+
+ @property
+ def window(self):
+ """Return the primary window"""
+ return self._primary
+
+ def __init__(self, start_loop=False, start_gui=True, **kwargs):
+ """Creates a new GtkApp."""
+ self.kwargs = kwargs
+ self._loaded = {}
+ self._initial = {}
+ self._primary = None
+
+ self.main_loop = GLib.main_depth()
+
+ # Start with creating all the defined windows.
+ if start_gui:
+ self.init_gui()
+ # Start up a gtk main loop when requested
+ if start_loop:
+ self.run()
+
+ def run(self):
+ """Run the gtk mainloop with ctrl+C and keyboard interupt additions"""
+ if not Gtk.init_check()[0]: # pragma: no cover
+ raise RuntimeError(
+ "Gtk failed to start." " Make sure $DISPLAY variable is set.\n"
+ )
+ try:
+ # Add a signal to force quit on Ctrl+C (just like the old days)
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
+ Gtk.main()
+ except KeyboardInterrupt: # pragma: no cover
+ logging.info("User Interputed")
+ logging.debug("Exiting %s", self.app_name)
+
+ def get_ui_file(self, window):
+ """Load any given gtk builder file from a standard location."""
+ paths = [
+ os.path.join(self.ui_dir, self.prefix, f"{window}.ui"),
+ os.path.join(self.ui_dir, self.prefix, f"{self.ui_file}.ui"),
+ ]
+ for path in paths:
+ if os.path.isfile(path):
+ return path
+ raise FileNotFoundError(f"Gtk Builder file is missing: {paths}")
+
+ def init_gui(self):
+ """Initalise all of our windows and load their signals"""
+ if self.windows:
+ for cls in self.windows:
+ window = cls
+ logging.debug("Adding window %s to GtkApp", window.name)
+ self._initial[window.name] = window
+ for window in self._initial.values():
+ if window.primary:
+ if not self._primary:
+ self._primary = self.load_window(window.name)
+ if not self.windows or not self._primary:
+ raise KeyError(f"No primary window found for '{self.app_name}' app.")
+
+ def load_window(self, name, *args, **kwargs):
+ """Load a specific window from our group of windows"""
+ window = self.proto_window(name)
+ window.init(*args, **kwargs)
+ return window
+
+ def load_window_extract(self, name, **kwargs):
+ """Load a child window as a widget container"""
+ window = self.proto_window(name)
+ window.load_widgets(**kwargs)
+ return window.extract()
+
+ def proto_window(self, name):
+ """
+ Loads a glade window as a window without initialisation, used for
+ extracting widgets from windows without loading them as windows.
+ """
+ logging.debug("Loading '%s' from %s", name, self._initial)
+ if name in self._initial:
+ # Create a new instance of this window
+ window = self._initial[name](self)
+ # Save the window object linked against the gtk window instance
+ self._loaded[window.wid] = window
+ return window
+ raise KeyError(f"Can't load window '{name}', class not found.")
+
+ def remove_window(self, window):
+ """Remove the window from the list and exit if none remain"""
+ if window.wid in self._loaded:
+ self._loaded.pop(window.wid)
+ else:
+ logging.warning("Missing window '%s' on exit.", window.name)
+ logging.debug("Loaded windows: %s", self._loaded)
+ if not self._loaded:
+ self.exit()
+
+ def exit(self):
+ """Exit our gtk application and kill gtk main if we have to"""
+ if self.main_loop < GLib.main_depth():
+ # Quit Gtk loop if we started one.
+ tag = self._primary.name if self._primary else "program"
+ logging.debug("Quit '%s' Main Loop.", tag)
+ Gtk.main_quit()
+ # You have to return in order for the loop to exit
+ return 0