summaryrefslogtreecommitdiffstats
path: root/share/extensions/inkex/gui/app.py
blob: 6b2a0ab1ae18991cb84e2a4998331d1527b7fe20 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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