port GUI to GTK4
Stefan Schuermans

Stefan Schuermans commited on 2026-03-03 19:57:52
Showing 2 changed files, with 246 additions and 301 deletions.

... ...
@@ -1,7 +1,6 @@
1 1
 <?xml version="1.0" encoding="UTF-8"?>
2
-<!-- Generated with glade 3.22.1 -->
3 2
 <interface>
4
-  <requires lib="gtk+" version="3.20"/>
3
+  <requires lib="gtk" version="4.0"/>
5 4
   <object class="GtkTreeStore" id="DetailsTree">
6 5
     <columns>
7 6
       <!-- column-name proc_id -->
... ...
@@ -48,80 +47,36 @@
48 47
       <column type="gchararray"/>
49 48
     </columns>
50 49
   </object>
51
-  <object class="GtkWindow" id="Application">
52
-    <property name="visible">True</property>
53
-    <property name="can_focus">False</property>
54
-    <signal name="destroy" handler="onDestroy" swapped="no"/>
55
-    <child>
56
-      <placeholder/>
57
-    </child>
58
-    <child>
59
-      <object class="GtkOverlay">
60
-        <property name="visible">True</property>
61
-        <property name="can_focus">False</property>
50
+  <object class="GtkOverlay" id="MainOverlay">
62 51
     <child>
63 52
       <object class="GtkBox" id="TopVBox">
64
-            <property name="visible">True</property>
65
-            <property name="can_focus">False</property>
66 53
         <property name="orientation">vertical</property>
67 54
         <child>
68
-              <object class="GtkButtonBox" id="ButtonBox">
69
-                <property name="visible">True</property>
70
-                <property name="can_focus">False</property>
71
-                <property name="layout_style">start</property>
55
+          <object class="GtkBox" id="ButtonBox">
72 56
             <child>
73 57
               <object class="GtkToggleButton" id="TreeToggle">
74 58
                 <property name="label" translatable="yes">Tree</property>
75
-                    <property name="visible">True</property>
76
-                    <property name="can_focus">True</property>
77
-                    <property name="receives_default">True</property>
78 59
                 <property name="active">True</property>
79
-                    <signal name="toggled" handler="onTreeToggled" swapped="no"/>
80 60
               </object>
81
-                  <packing>
82
-                    <property name="expand">True</property>
83
-                    <property name="fill">True</property>
84
-                    <property name="position">0</property>
85
-                  </packing>
86
-                </child>
87
-                <child>
88
-                  <placeholder/>
89
-                </child>
90
-                <child>
91
-                  <placeholder/>
92 61
             </child>
93 62
           </object>
94
-              <packing>
95
-                <property name="expand">False</property>
96
-                <property name="fill">True</property>
97
-                <property name="position">0</property>
98
-              </packing>
99 63
         </child>
100 64
         <child>
101 65
           <object class="GtkBox" id="TopHBox">
102
-                <property name="visible">True</property>
103
-                <property name="can_focus">False</property>
66
+            <property name="hexpand">True</property>
67
+            <property name="vexpand">True</property>
104 68
             <child>
105
-                  <object class="GtkScrolledWindow" id="ProcessesScoll">
106
-                    <property name="width_request">256</property>
107
-                    <property name="height_request">256</property>
108
-                    <property name="visible">True</property>
109
-                    <property name="can_focus">True</property>
110
-                    <property name="shadow_type">in</property>
69
+              <object class="GtkScrolledWindow" id="ProcessesScroll">
70
+                <property name="hexpand">True</property>
71
+                <property name="vexpand">True</property>
72
+                <property name="min-content-width">256</property>
73
+                <property name="min-content-height">256</property>
111 74
                 <child>
112 75
                   <object class="GtkTreeView" id="ProcessesView">
113
-                        <property name="visible">True</property>
114
-                        <property name="can_focus">True</property>
115 76
                     <property name="model">ProcessesTree</property>
116
-                        <property name="rules_hint">True</property>
117
-                        <property name="search_column">0</property>
118
-                        <property name="fixed_height_mode">True</property>
119
-                        <property name="enable_tree_lines">True</property>
120
-                        <signal name="cursor-changed" handler="onProcessesCursorChanged" swapped="no"/>
121
-                        <signal name="row-activated" handler="onProcessesRowActivated" swapped="no"/>
122
-                        <child internal-child="selection">
123
-                          <object class="GtkTreeSelection"/>
124
-                        </child>
77
+                    <property name="search-column">0</property>
78
+                    <property name="fixed-height-mode">True</property>
79
+                    <property name="enable-tree-lines">True</property>
125 80
                     <child>
126 81
                       <object class="GtkTreeViewColumn" id="ProcessesCommandCol">
127 82
                         <property name="resizable">True</property>
... ...
@@ -129,8 +84,8 @@
129 84
                         <property name="title" translatable="yes">Command</property>
130 85
                         <property name="clickable">True</property>
131 86
                         <property name="reorderable">True</property>
132
-                            <property name="sort_indicator">True</property>
133
-                            <property name="sort_column_id">5</property>
87
+                        <property name="sort-indicator">True</property>
88
+                        <property name="sort-column-id">5</property>
134 89
                         <child>
135 90
                           <object class="GtkCellRendererText" id="ProcessesCommandText"/>
136 91
                           <attributes>
... ...
@@ -146,8 +101,8 @@
146 101
                         <property name="title" translatable="yes">Begin</property>
147 102
                         <property name="clickable">True</property>
148 103
                         <property name="reorderable">True</property>
149
-                            <property name="sort_indicator">True</property>
150
-                            <property name="sort_column_id">1</property>
104
+                        <property name="sort-indicator">True</property>
105
+                        <property name="sort-column-id">1</property>
151 106
                         <child>
152 107
                           <object class="GtkCellRendererText" id="ProcessesBeginText"/>
153 108
                           <attributes>
... ...
@@ -163,8 +118,8 @@
163 118
                         <property name="title" translatable="yes">End</property>
164 119
                         <property name="clickable">True</property>
165 120
                         <property name="reorderable">True</property>
166
-                            <property name="sort_indicator">True</property>
167
-                            <property name="sort_column_id">3</property>
121
+                        <property name="sort-indicator">True</property>
122
+                        <property name="sort-column-id">3</property>
168 123
                         <child>
169 124
                           <object class="GtkCellRendererText" id="ProcessesEndText"/>
170 125
                           <attributes>
... ...
@@ -180,8 +135,8 @@
180 135
                         <property name="title" translatable="yes">CPU Time</property>
181 136
                         <property name="clickable">True</property>
182 137
                         <property name="reorderable">True</property>
183
-                            <property name="sort_indicator">True</property>
184
-                            <property name="sort_column_id">6</property>
138
+                        <property name="sort-indicator">True</property>
139
+                        <property name="sort-column-id">6</property>
185 140
                         <child>
186 141
                           <object class="GtkCellRendererText" id="ProcessesCpuTimeText"/>
187 142
                           <attributes>
... ...
@@ -197,8 +152,8 @@
197 152
                         <property name="title" translatable="yes">Memory</property>
198 153
                         <property name="clickable">True</property>
199 154
                         <property name="reorderable">True</property>
200
-                            <property name="sort_indicator">True</property>
201
-                            <property name="sort_column_id">8</property>
155
+                        <property name="sort-indicator">True</property>
156
+                        <property name="sort-column-id">8</property>
202 157
                         <child>
203 158
                           <object class="GtkCellRendererText" id="ProcessesMemoryText"/>
204 159
                           <attributes>
... ...
@@ -214,8 +169,8 @@
214 169
                         <property name="title" translatable="yes">Page Faults</property>
215 170
                         <property name="clickable">True</property>
216 171
                         <property name="reorderable">True</property>
217
-                            <property name="sort_indicator">True</property>
218
-                            <property name="sort_column_id">10</property>
172
+                        <property name="sort-indicator">True</property>
173
+                        <property name="sort-column-id">10</property>
219 174
                         <child>
220 175
                           <object class="GtkCellRendererText" id="ProcessesPageFaultsText"/>
221 176
                           <attributes>
... ...
@@ -231,8 +186,8 @@
231 186
                         <property name="title" translatable="yes">File System Operations</property>
232 187
                         <property name="clickable">True</property>
233 188
                         <property name="reorderable">True</property>
234
-                            <property name="sort_indicator">True</property>
235
-                            <property name="sort_column_id">12</property>
189
+                        <property name="sort-indicator">True</property>
190
+                        <property name="sort-column-id">12</property>
236 191
                         <child>
237 192
                           <object class="GtkCellRendererText" id="ProcessesFileSysOpsText"/>
238 193
                           <attributes>
... ...
@@ -248,8 +203,8 @@
248 203
                         <property name="title" translatable="yes">Context Switches</property>
249 204
                         <property name="clickable">True</property>
250 205
                         <property name="reorderable">True</property>
251
-                            <property name="sort_indicator">True</property>
252
-                            <property name="sort_column_id">14</property>
206
+                        <property name="sort-indicator">True</property>
207
+                        <property name="sort-column-id">14</property>
253 208
                         <child>
254 209
                           <object class="GtkCellRendererText" id="ProcessesCtxSwText"/>
255 210
                           <attributes>
... ...
@@ -261,32 +216,19 @@
261 216
                   </object>
262 217
                 </child>
263 218
               </object>
264
-                  <packing>
265
-                    <property name="expand">True</property>
266
-                    <property name="fill">True</property>
267
-                    <property name="position">0</property>
268
-                  </packing>
269 219
             </child>
270 220
             <child>
271 221
               <object class="GtkScrolledWindow" id="DetailsScroll">
272
-                    <property name="width_request">256</property>
273
-                    <property name="height_request">256</property>
274
-                    <property name="visible">True</property>
275
-                    <property name="can_focus">True</property>
276
-                    <property name="shadow_type">in</property>
222
+                <property name="hexpand">True</property>
223
+                <property name="vexpand">True</property>
224
+                <property name="min-content-width">256</property>
225
+                <property name="min-content-height">256</property>
277 226
                 <child>
278 227
                   <object class="GtkTreeView" id="DetailsView">
279
-                        <property name="visible">True</property>
280
-                        <property name="can_focus">True</property>
281 228
                     <property name="model">DetailsTree</property>
282
-                        <property name="rules_hint">True</property>
283
-                        <property name="search_column">0</property>
284
-                        <property name="fixed_height_mode">True</property>
285
-                        <property name="enable_tree_lines">True</property>
286
-                        <signal name="row-activated" handler="onDetailsRowActivated" swapped="no"/>
287
-                        <child internal-child="selection">
288
-                          <object class="GtkTreeSelection"/>
289
-                        </child>
229
+                    <property name="search-column">0</property>
230
+                    <property name="fixed-height-mode">True</property>
231
+                    <property name="enable-tree-lines">True</property>
290 232
                     <child>
291 233
                       <object class="GtkTreeViewColumn" id="DetailsKeyCol">
292 234
                         <property name="resizable">True</property>
... ...
@@ -294,8 +236,8 @@
294 236
                         <property name="title" translatable="yes">Key</property>
295 237
                         <property name="clickable">True</property>
296 238
                         <property name="reorderable">True</property>
297
-                            <property name="sort_indicator">True</property>
298
-                            <property name="sort_column_id">1</property>
239
+                        <property name="sort-indicator">True</property>
240
+                        <property name="sort-column-id">1</property>
299 241
                         <child>
300 242
                           <object class="GtkCellRendererText" id="DetailsKeyText"/>
301 243
                           <attributes>
... ...
@@ -311,8 +253,8 @@
311 253
                         <property name="title" translatable="yes">Value</property>
312 254
                         <property name="clickable">True</property>
313 255
                         <property name="reorderable">True</property>
314
-                            <property name="sort_indicator">True</property>
315
-                            <property name="sort_column_id">2</property>
256
+                        <property name="sort-indicator">True</property>
257
+                        <property name="sort-column-id">2</property>
316 258
                         <child>
317 259
                           <object class="GtkCellRendererText" id="DetailsValueText"/>
318 260
                           <attributes>
... ...
@@ -324,76 +266,34 @@
324 266
                   </object>
325 267
                 </child>
326 268
               </object>
327
-                  <packing>
328
-                    <property name="expand">True</property>
329
-                    <property name="fill">True</property>
330
-                    <property name="position">1</property>
331
-                  </packing>
332 269
             </child>
333 270
           </object>
334
-              <packing>
335
-                <property name="expand">True</property>
336
-                <property name="fill">True</property>
337
-                <property name="position">1</property>
338
-              </packing>
339 271
         </child>
340 272
       </object>
341
-          <packing>
342
-            <property name="index">-1</property>
343
-          </packing>
344 273
     </child>
345 274
     <child type="overlay">
346 275
       <object class="GtkRevealer" id="NotificationRevealer">
347
-            <property name="visible">True</property>
348
-            <property name="can_focus">False</property>
349 276
         <property name="halign">center</property>
350 277
         <property name="valign">start</property>
351
-            <property name="transition_type">slide-up</property>
278
+        <property name="transition-type">slide-up</property>
352 279
         <child>
353 280
           <object class="GtkBox">
354
-                <property name="visible">True</property>
355
-                <property name="can_focus">False</property>
356 281
             <property name="halign">center</property>
357 282
             <property name="valign">start</property>
358 283
             <property name="spacing">6</property>
284
+            <style>
285
+              <class name="app-notification"/>
286
+            </style>
359 287
             <child>
360 288
               <object class="GtkLabel" id="NotificationMessage">
361
-                    <property name="visible">True</property>
362
-                    <property name="can_focus">False</property>
363
-                    <property name="label" translatable="yes">Sample notification message to be
364
-shown to the user</property>
289
+                <property name="label" translatable="yes">notification</property>
365 290
                 <property name="justify">center</property>
366 291
               </object>
367
-                  <packing>
368
-                    <property name="expand">False</property>
369
-                    <property name="fill">True</property>
370
-                    <property name="position">1</property>
371
-                  </packing>
372 292
             </child>
373 293
             <child>
374 294
               <object class="GtkButton" id="NotificationClose">
375
-                    <property name="visible">True</property>
376
-                    <property name="can_focus">True</property>
377
-                    <property name="receives_default">True</property>
378
-                    <property name="relief">none</property>
379
-                    <signal name="clicked" handler="onNotificationClose" swapped="no"/>
380
-                    <child>
381
-                      <object class="GtkImage">
382
-                        <property name="visible">True</property>
383
-                        <property name="can_focus">False</property>
384
-                        <property name="icon_name">window-close-symbolic</property>
385
-                      </object>
386
-                    </child>
387
-                  </object>
388
-                  <packing>
389
-                    <property name="expand">False</property>
390
-                    <property name="fill">True</property>
391
-                    <property name="position">2</property>
392
-                  </packing>
393
-                </child>
394
-                <style>
395
-                  <class name="app-notification"/>
396
-                </style>
295
+                <property name="has-frame">False</property>
296
+                <property name="icon-name">window-close-symbolic</property>
397 297
               </object>
398 298
             </child>
399 299
           </object>
... ...
@@ -14,17 +14,15 @@ import time
14 14
 import uproctrace.gui_glade
15 15
 import uproctrace.processes
16 16
 
17
-# pylint: disable=C0411
18 17
 import gi
19
-gi.require_version('Gdk', '3.0')
20
-gi.require_version('Gtk', '3.0')
21
-# pylint: disable=C0413
22
-from gi.repository import Gdk, Gtk, GLib, GObject
23 18
 
24
-GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
19
+gi.require_version("Gtk", "4.0")
20
+# pylint: disable=wrong-import-position
21
+# pylint: disable=too-many-positional-arguments
22
+from gi.repository import Gdk, Gtk, GLib
25 23
 
26 24
 # regular expression for an environment variable assignment
27
-RE_ENV_VAR = re.compile(r'^(?P<name>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>.*)$')
25
+RE_ENV_VAR = re.compile(r"^(?P<name>[A-Za-z_][A-Za-z0-9_]*)=(?P<value>.*)$")
28 26
 
29 27
 
30 28
 def add_none(val_a: int, val_b: int) -> int:
... ...
@@ -41,8 +39,8 @@ def cmdline2str(cmdline: list) -> str:
41 39
     Convert command line to string.
42 40
     """
43 41
     if cmdline is None:
44
-        return '???'
45
-    return ' '.join([cmdline_str_escape(s) for s in cmdline])
42
+        return "???"
43
+    return " ".join([cmdline_str_escape(s) for s in cmdline])
46 44
 
47 45
 
48 46
 def cmdline_str_escape(string: str) -> str:
... ...
@@ -56,9 +54,9 @@ def cmdline_str_escape(string: str) -> str:
56 54
         return shlex.quote(string)
57 55
     # variable assignment -> escape only value part
58 56
     # (also works if it only looks like a variable assignment)
59
-    name = match.group('name')
60
-    value = shlex.quote(match.group('value'))
61
-    return f'{name:s}={value:s}'
57
+    name = match.group("name")
58
+    value = shlex.quote(match.group("value"))
59
+    return f"{name:s}={value:s}"
62 60
 
63 61
 
64 62
 def duration2str(duration: float) -> str:
... ...
@@ -66,7 +64,7 @@ def duration2str(duration: float) -> str:
66 64
     Convert duration to string.
67 65
     """
68 66
     if duration is None:
69
-        return '???'
67
+        return "???"
70 68
     # split into day, hours, minutes, seconds
71 69
     dur_s = int(duration)
72 70
     dur_m = dur_s // 60
... ...
@@ -82,21 +80,21 @@ def duration2str(duration: float) -> str:
82 80
     dur_ms = dur_us // 1000
83 81
     dur_us = dur_us % 1000
84 82
     # assemble text
85
-    txt = ''
83
+    txt = ""
86 84
     if dur_d > 0:
87
-        txt += f'{dur_d:d} d '
85
+        txt += f"{dur_d:d} d "
88 86
     if dur_h > 0 or txt:
89
-        txt += f'{dur_h:d} h '
87
+        txt += f"{dur_h:d} h "
90 88
     if dur_m > 0 or txt:
91
-        txt += f'{dur_m:d} m '
89
+        txt += f"{dur_m:d} m "
92 90
     if dur_s > 0 or txt:
93
-        txt += f'{dur_s:d} s '
91
+        txt += f"{dur_s:d} s "
94 92
     if dur_ms > 0 or txt:
95
-        txt += f'{dur_ms:d} ms '
93
+        txt += f"{dur_ms:d} ms "
96 94
     if dur_us > 0 or txt:
97
-        txt += f'{dur_us:d} us '
98
-    txt += f'{dur_ns:d} ns '
99
-    txt += f'({duration:f} s)'
95
+        txt += f"{dur_us:d} us "
96
+    txt += f"{dur_ns:d} ns "
97
+    txt += f"({duration:f} s)"
100 98
     return txt
101 99
 
102 100
 
... ...
@@ -105,8 +103,8 @@ def int2str(val: int) -> str:
105 103
     Convert integer to string, support None.
106 104
     """
107 105
     if val is None:
108
-        return '???'
109
-    return f'{val:d}'
106
+        return "???"
107
+    return f"{val:d}"
110 108
 
111 109
 
112 110
 def kb2str(size_kb: int) -> str:
... ...
@@ -114,20 +112,20 @@ def kb2str(size_kb: int) -> str:
114 112
     Convert size in KiB to string.
115 113
     """
116 114
     if size_kb is None:
117
-        return '???'
115
+        return "???"
118 116
     # split into GiB, MiB, KiB
119 117
     mib = size_kb // 1024
120 118
     kib = size_kb % 1024
121 119
     gib = mib // 1024
122 120
     mib = mib % 1024
123 121
     # assemble text
124
-    txt = ''
122
+    txt = ""
125 123
     if gib > 0:
126
-        txt += f'{gib:d} GiB '
124
+        txt += f"{gib:d} GiB "
127 125
     if mib > 0 or txt:
128
-        txt += f'{mib:d} MiB '
129
-    txt += f'{kib:d} KiB '
130
-    txt += f'({size_kb:d} KiB)'
126
+        txt += f"{mib:d} MiB "
127
+    txt += f"{kib:d} KiB "
128
+    txt += f"({size_kb:d} KiB)"
131 129
     return txt
132 130
 
133 131
 
... ...
@@ -136,7 +134,7 @@ def str2str(str_or_none: str) -> str:
136 134
     Convert string (or None) to string.
137 135
     """
138 136
     if str_or_none is None:
139
-        return '???'
137
+        return "???"
140 138
     return str_or_none
141 139
 
142 140
 
... ...
@@ -145,19 +143,19 @@ def timestamp2str(timestamp: float) -> str:
145 143
     Convert a timestamp to a human-reable time string."
146 144
     """
147 145
     if timestamp is None:
148
-        return '???'
146
+        return "???"
149 147
     sec = int(timestamp)
150 148
     nsec = int((timestamp - sec) * 1e9)
151
-    time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(sec))
152
-    return time_str + f'.{nsec:09d}'
149
+    time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(sec))
150
+    return time_str + f".{nsec:09d}"
153 151
 
154 152
 
155
-class UptGui:
153
+class UptGui(Gtk.Application):
156 154
     """
157 155
     Graphical user interface of UProcTrace.
158 156
     """
159 157
 
160
-    # pylint: disable=R0902
158
+    # pylint: disable=too-many-instance-attributes
161 159
 
162 160
     DETAIL_PROC_ID = 0
163 161
     DETAIL_KEY = 1
... ...
@@ -183,65 +181,120 @@ class UptGui:
183 181
         """
184 182
         Construct the GUI.
185 183
         """
184
+        super().__init__()
185
+        self.proto_filename = proto_filename
186
+        self.builder = None
187
+        self.clipboard = None
188
+        self.show_processes_as_tree = None
189
+        self.wid_details_tree = None
190
+        self.wid_details_view = None
191
+        self.wid_processes_tree = None
192
+        self.wid_processes_view = None
193
+        self.wid_tree_toggle = None
194
+        self.notifier = None
195
+        self.notifier_msg = None
196
+        self.notifier_timeout = None
197
+        self.window = None
198
+        self.processes = None
199
+        self.connect("activate", self._on_activate)
200
+
201
+    def _on_activate(self, _app):
202
+        """
203
+        Application activated: build UI and show window.
204
+        """
186 205
         self.builder = Gtk.Builder()
187 206
         self.builder.add_from_string(uproctrace.gui_glade.DATA)
188
-        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
207
+        self.clipboard = Gdk.Display.get_default().get_clipboard()
189 208
         self.show_processes_as_tree = True
190
-        self.wid_details_tree = self.builder.get_object('DetailsTree')
191
-        self.wid_details_view = self.builder.get_object('DetailsView')
192
-        self.wid_processes_tree = self.builder.get_object('ProcessesTree')
193
-        self.wid_processes_view = self.builder.get_object('ProcessesView')
194
-        self.wid_tree_toggle = self.builder.get_object('TreeToggle')
209
+        self.wid_details_tree = self.builder.get_object("DetailsTree")
210
+        self.wid_details_view = self.builder.get_object("DetailsView")
211
+        self.wid_processes_tree = self.builder.get_object("ProcessesTree")
212
+        self.wid_processes_view = self.builder.get_object("ProcessesView")
213
+        self.wid_tree_toggle = self.builder.get_object("TreeToggle")
195 214
         self.notifier = self.builder.get_object("NotificationRevealer")
196 215
         self.notifier_msg = self.builder.get_object("NotificationMessage")
197 216
         self.notifier_timeout = None
198
-        handlers = {
199
-            'onDestroy': self.onDestroy,
200
-            'onDetailsRowActivated': self.onDetailsRowActivated,
201
-            'onNotificationClose': self.onNotificationClose,
202
-            'onProcessesCursorChanged': self.onProcessesCursorChanged,
203
-            'onProcessesRowActivated': self.onProcessesRowActivated,
204
-            'onTreeToggled': self.onTreeToggled,
205
-        }
206
-        self.builder.connect_signals(handlers)
217
+        # connect signals manually (GTK4 has no builder.connect_signals)
218
+        self.wid_details_view.connect("row-activated", self.onDetailsRowActivated)
219
+        self.wid_processes_view.connect("cursor-changed", self.onProcessesCursorChanged)
220
+        self.wid_processes_view.connect("row-activated", self.onProcessesRowActivated)
221
+        self.wid_tree_toggle.connect("toggled", self.onTreeToggled)
222
+        close_btn = self.builder.get_object("NotificationClose")
223
+        close_btn.connect("clicked", self.onNotificationClose)
224
+        # create application window
225
+        self.window = Gtk.ApplicationWindow(application=self)
226
+        overlay = self.builder.get_object("MainOverlay")
227
+        self.window.set_child(overlay)
228
+        # handle SIGINT
229
+        GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, self.quit)
207 230
         # open trace file
208
-        self.openTrace(proto_filename)
231
+        self.openTrace(self.proto_filename)
232
+        # show window
233
+        self.window.present()
209 234
 
210
-    def fillProcessesEntry(self, proc_iter,
211
-                           proc: uproctrace.processes.Process):
235
+    def fillProcessesEntry(self, proc_iter, proc: uproctrace.processes.Process):
212 236
         """
213 237
         Fill attributes of processes tree entry.
214 238
         proc_iter: process tree entry
215 239
         proc: process object
216 240
         """
217
-        self.wid_processes_tree.set_value(proc_iter, self.PROC_PROC_ID,
218
-                                          proc.proc_id)
219
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_BEGIN_TIMESTAMP,
241
+        self.wid_processes_tree.set_value(proc_iter, self.PROC_PROC_ID, proc.proc_id)
242
+        self.fillProcessesEntryAttr(
243
+            proc_iter,
244
+            self.PROC_BEGIN_TIMESTAMP,
220 245
             self.PROC_BEGIN_TIMESTAMP_TEXT,
221
-                                    timestamp2str, proc.begin_timestamp)
222
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_END_TIMESTAMP,
246
+            timestamp2str,
247
+            proc.begin_timestamp,
248
+        )
249
+        self.fillProcessesEntryAttr(
250
+            proc_iter,
251
+            self.PROC_END_TIMESTAMP,
223 252
             self.PROC_END_TIMESTAMP_TEXT,
224
-                                    timestamp2str, proc.end_timestamp)
225
-        self.wid_processes_tree.set_value(proc_iter, self.PROC_CMDLINE,
226
-                                          cmdline2str(proc.cmdline))
227
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_CPU_TIME,
228
-                                    self.PROC_CPU_TIME_TEXT, duration2str,
229
-                                    proc.cpu_time)
230
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_MAX_RSS_KB,
231
-                                    self.PROC_MAX_RSS_KB_TEXT, kb2str,
232
-                                    proc.max_rss_kb)
233
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_PAGE_FAULTS,
234
-                                    self.PROC_PAGE_FAULTS_TEXT, int2str,
235
-                                    add_none(proc.min_flt, proc.maj_flt))
236
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_FILE_SYS_OPS,
237
-                                    self.PROC_FILE_SYS_OPS_TEXT, int2str,
238
-                                    add_none(proc.in_block, proc.ou_block))
239
-        self.fillProcessesEntryAttr(proc_iter, self.PROC_CTX_SW,
240
-                                    self.PROC_CTX_SW_TEXT, int2str,
241
-                                    add_none(proc.n_v_csw, proc.n_iv_csw))
242
-
243
-    def fillProcessesEntryAttr(self, proc_iter, col: int, text_col: int,
244
-                               val2str_func, val):
253
+            timestamp2str,
254
+            proc.end_timestamp,
255
+        )
256
+        self.wid_processes_tree.set_value(
257
+            proc_iter, self.PROC_CMDLINE, cmdline2str(proc.cmdline)
258
+        )
259
+        self.fillProcessesEntryAttr(
260
+            proc_iter,
261
+            self.PROC_CPU_TIME,
262
+            self.PROC_CPU_TIME_TEXT,
263
+            duration2str,
264
+            proc.cpu_time,
265
+        )
266
+        self.fillProcessesEntryAttr(
267
+            proc_iter,
268
+            self.PROC_MAX_RSS_KB,
269
+            self.PROC_MAX_RSS_KB_TEXT,
270
+            kb2str,
271
+            proc.max_rss_kb,
272
+        )
273
+        self.fillProcessesEntryAttr(
274
+            proc_iter,
275
+            self.PROC_PAGE_FAULTS,
276
+            self.PROC_PAGE_FAULTS_TEXT,
277
+            int2str,
278
+            add_none(proc.min_flt, proc.maj_flt),
279
+        )
280
+        self.fillProcessesEntryAttr(
281
+            proc_iter,
282
+            self.PROC_FILE_SYS_OPS,
283
+            self.PROC_FILE_SYS_OPS_TEXT,
284
+            int2str,
285
+            add_none(proc.in_block, proc.ou_block),
286
+        )
287
+        self.fillProcessesEntryAttr(
288
+            proc_iter,
289
+            self.PROC_CTX_SW,
290
+            self.PROC_CTX_SW_TEXT,
291
+            int2str,
292
+            add_none(proc.n_v_csw, proc.n_iv_csw),
293
+        )
294
+
295
+    def fillProcessesEntryAttr(
296
+        self, proc_iter, col: int, text_col: int, val2str_func, val
297
+    ):
245 298
         """
246 299
         Fill attribute of processes tree entry.
247 300
         proc_iter: process tree entry
... ...
@@ -252,14 +305,7 @@ class UptGui:
252 305
         """
253 306
         # pylint: disable=R0913
254 307
         self.wid_processes_tree.set_value(proc_iter, col, val)
255
-        self.wid_processes_tree.set_value(proc_iter, text_col,
256
-                                          val2str_func(val))
257
-
258
-    def onDestroy(self, _widget):
259
-        """
260
-        Window will be destroyed.
261
-        """
262
-        Gtk.main_quit()
308
+        self.wid_processes_tree.set_value(proc_iter, text_col, val2str_func(val))
263 309
 
264 310
     def onDetailsRowActivated(self, _widget, _row, _col):
265 311
         """
... ...
@@ -273,22 +319,20 @@ class UptGui:
273 319
         if detail_iter is None:
274 320
             return
275 321
         # copy string of selected item or subtree to clipboard
276
-        string = self.wid_details_tree.get_value(detail_iter,
277
-                                                 self.DETAIL_VALUE)
322
+        string = self.wid_details_tree.get_value(detail_iter, self.DETAIL_VALUE)
278 323
         child_iter = self.wid_details_tree.iter_children(detail_iter)
279 324
         if child_iter is not None:
280 325
             # selected row has children, assemble command line from children
281 326
             strings = []
282 327
             while child_iter is not None:
283 328
                 strings.append(
284
-                    self.wid_details_tree.get_value(child_iter,
285
-                                                    self.DETAIL_VALUE))
329
+                    self.wid_details_tree.get_value(child_iter, self.DETAIL_VALUE)
330
+                )
286 331
                 child_iter = self.wid_details_tree.iter_next(child_iter)
287 332
             string = cmdline2str(strings)
288 333
         self.storeInClipboardAndNotify(string)
289 334
         # get proc_id of selected row, nothing else to do if none
290
-        proc_id = self.wid_details_tree.get_value(detail_iter,
291
-                                                  self.DETAIL_PROC_ID)
335
+        proc_id = self.wid_details_tree.get_value(detail_iter, self.DETAIL_PROC_ID)
292 336
         if proc_id < 0:
293 337
             return
294 338
         # select process
... ...
@@ -309,8 +353,7 @@ class UptGui:
309 353
         if proc_iter is None:
310 354
             self.showDetails(None)
311 355
             return
312
-        proc_id = self.wid_processes_tree.get_value(proc_iter,
313
-                                                    self.PROC_PROC_ID)
356
+        proc_id = self.wid_processes_tree.get_value(proc_iter, self.PROC_PROC_ID)
314 357
         # show details of selected process
315 358
         self.showDetails(proc_id)
316 359
 
... ...
@@ -326,8 +369,7 @@ class UptGui:
326 369
         if processes_iter is None:
327 370
             return
328 371
         # get process
329
-        proc_id = self.wid_processes_tree.get_value(processes_iter,
330
-                                                    self.PROC_PROC_ID)
372
+        proc_id = self.wid_processes_tree.get_value(processes_iter, self.PROC_PROC_ID)
331 373
         if proc_id is None or proc_id < 0:
332 374
             return
333 375
         proc = self.processes.getProcess(proc_id)
... ...
@@ -335,14 +377,14 @@ class UptGui:
335 377
             return
336 378
         # copy shell command line to repeat process call to clipboard
337 379
         # ( cd <workdir>; env -i <environment> <cmdline> )
338
-        string = '('
380
+        string = "("
339 381
         if proc.cwd:
340
-            string += ' cd ' + cmdline_str_escape(proc.cwd) + ';'
382
+            string += " cd " + cmdline_str_escape(proc.cwd) + ";"
341 383
         if proc.environ:
342
-            string += ' env -i ' + cmdline2str(sorted(proc.environ))
384
+            string += " env -i " + cmdline2str(sorted(proc.environ))
343 385
         if proc.cmdline:
344
-            string += ' ' + cmdline2str(proc.cmdline)
345
-        string += ' )'
386
+            string += " " + cmdline2str(proc.cmdline)
387
+        string += " )"
346 388
         self.storeInClipboardAndNotify(string)
347 389
 
348 390
     def onTreeToggled(self, _widget):
... ...
@@ -366,7 +408,7 @@ class UptGui:
366 408
         """
367 409
         self.notifier.set_reveal_child(False)
368 410
         if self.notifier_timeout is not None:
369
-            GObject.source_remove(self.notifier_timeout)
411
+            GLib.source_remove(self.notifier_timeout)
370 412
             self.notifier_timeout = None
371 413
 
372 414
     def showNotification(self, message: str, timeout_ms: int = None):
... ...
@@ -380,15 +422,13 @@ class UptGui:
380 422
         self.notifier.set_reveal_child(True)
381 423
 
382 424
         if timeout_ms is not None:
383
-            self.notifier_timeout = GObject.timeout_add(
384
-                timeout_ms, self.closeNotification)
425
+            self.notifier_timeout = GLib.timeout_add(timeout_ms, self.closeNotification)
385 426
 
386 427
     def storeInClipboard(self, string: str):
387 428
         """
388 429
         Stores a string in the clipboard
389 430
         """
390
-        self.clipboard.set_text(string, -1)
391
-        self.clipboard.store()
431
+        self.clipboard.set(string)
392 432
 
393 433
     def storeInClipboardAndNotify(self, string: str):
394 434
         """
... ...
@@ -404,7 +444,7 @@ class UptGui:
404 444
         Open a trace file.
405 445
         """
406 446
         # load new data
407
-        with open(proto_filename, 'rb') as proto_file:
447
+        with open(proto_filename, "rb") as proto_file:
408 448
             self.processes = uproctrace.processes.Processes(proto_file)
409 449
         # populate processes view
410 450
         self.populateProcesses()
... ...
@@ -425,7 +465,8 @@ class UptGui:
425 465
             proc = procs[0]
426 466
             del procs[0]
427 467
             proc_iter = self.wid_processes_tree.append(
428
-                parent_iter if self.show_processes_as_tree else None)
468
+                parent_iter if self.show_processes_as_tree else None
469
+            )
429 470
             self.fillProcessesEntry(proc_iter, proc)
430 471
             to_be_output.append((proc.children, proc_iter))
431 472
         # show all processes
... ...
@@ -454,7 +496,7 @@ class UptGui:
454 496
             if proc_store.get_value(proc_iter, self.PROC_PROC_ID) != proc_id:
455 497
                 return
456 498
             proc_sel.select_iter(proc_iter)
457
-            self.wid_processes_view.scroll_to_cell(proc_path)
499
+            self.wid_processes_view.scroll_to_cell(proc_path, None, False, 0, 0)
458 500
 
459 501
         self.wid_processes_tree.foreach(update, None)
460 502
 
... ...
@@ -480,11 +523,9 @@ class UptGui:
480 523
             Return iterator to added detail.
481 524
             """
482 525
             detail_iter = self.wid_details_tree.append(parent_iter)
483
-            self.wid_details_tree.set_value(detail_iter, self.DETAIL_PROC_ID,
484
-                                            -1)
526
+            self.wid_details_tree.set_value(detail_iter, self.DETAIL_PROC_ID, -1)
485 527
             self.wid_details_tree.set_value(detail_iter, self.DETAIL_KEY, key)
486
-            self.wid_details_tree.set_value(detail_iter, self.DETAIL_VALUE,
487
-                                            value)
528
+            self.wid_details_tree.set_value(detail_iter, self.DETAIL_VALUE, value)
488 529
             return detail_iter
489 530
 
490 531
         def add_list(key: str, values: list, parent_iter=None):
... ...
@@ -494,10 +535,10 @@ class UptGui:
494 535
             Return iterator to added top-level of detail subtree.
495 536
             """
496 537
             if values is None:
497
-                return add(key, '???', parent_iter)
498
-            list_iter = add(key, f'{len(values):d} entries', parent_iter)
538
+                return add(key, "???", parent_iter)
539
+            list_iter = add(key, f"{len(values):d} entries", parent_iter)
499 540
             for i, value in enumerate(values):
500
-                add(f'{key} {i:d}', value, list_iter)
541
+                add(f"{key} {i:d}", value, list_iter)
501 542
             return list_iter
502 543
 
503 544
         def add_list_sorted(key: str, values: list, parent_iter=None):
... ...
@@ -519,50 +560,61 @@ class UptGui:
519 560
             for sub_key, val in zip(sub_keys, values):
520 561
                 add(sub_key, int2str(val), sum_iter)
521 562
             self.wid_details_view.expand_row(
522
-                self.wid_details_tree.get_path(sum_iter), True)
563
+                self.wid_details_tree.get_path(sum_iter), True
564
+            )
523 565
             return sum_iter
524 566
 
525
-        add('begin time', timestamp2str(proc.begin_timestamp))
526
-        cmdline_iter = add_list('command line', proc.cmdline)
567
+        add("begin time", timestamp2str(proc.begin_timestamp))
568
+        cmdline_iter = add_list("command line", proc.cmdline)
527 569
         self.wid_details_view.expand_row(
528
-            self.wid_details_tree.get_path(cmdline_iter), True)
529
-        add_sum('context switches', ['involuntary', 'voluntary'],
530
-                [proc.n_iv_csw, proc.n_v_csw])
531
-        add('CPU time', duration2str(proc.cpu_time))
532
-        add('end time', timestamp2str(proc.end_timestamp))
533
-        add_list_sorted('environment', proc.environ)
534
-        add('executable', str2str(proc.exe))
535
-        add_sum('file system operations', ['input', 'output'],
536
-                [proc.in_block, proc.ou_block])
537
-        add('max. resident memory', kb2str(proc.max_rss_kb))
538
-        add_sum('page faults', ['major', 'minor'],
539
-                [proc.maj_flt, proc.min_flt])
540
-        add('pid', int2str(proc.pid))
541
-        add('ppid', int2str(proc.ppid))
542
-        add('system CPU time', duration2str(proc.sys_time))
543
-        add('user CPU time', duration2str(proc.user_time))
544
-        add('working directory', str2str(proc.cwd))
570
+            self.wid_details_tree.get_path(cmdline_iter), True
571
+        )
572
+        add_sum(
573
+            "context switches",
574
+            ["involuntary", "voluntary"],
575
+            [proc.n_iv_csw, proc.n_v_csw],
576
+        )
577
+        add("CPU time", duration2str(proc.cpu_time))
578
+        add("end time", timestamp2str(proc.end_timestamp))
579
+        add_list_sorted("environment", proc.environ)
580
+        add("executable", str2str(proc.exe))
581
+        add_sum(
582
+            "file system operations",
583
+            ["input", "output"],
584
+            [proc.in_block, proc.ou_block],
585
+        )
586
+        add("max. resident memory", kb2str(proc.max_rss_kb))
587
+        add_sum("page faults", ["major", "minor"], [proc.maj_flt, proc.min_flt])
588
+        add("pid", int2str(proc.pid))
589
+        add("ppid", int2str(proc.ppid))
590
+        add("system CPU time", duration2str(proc.sys_time))
591
+        add("user CPU time", duration2str(proc.user_time))
592
+        add("working directory", str2str(proc.cwd))
545 593
         # add parent
546 594
         parent_proc = proc.parent
547 595
         if parent_proc is None:
548
-            add('parent', '???')
596
+            add("parent", "???")
549 597
         else:
550
-            parent_iter = add('parent', cmdline2str(parent_proc.cmdline))
551
-            self.wid_details_tree.set_value(parent_iter, self.DETAIL_PROC_ID,
552
-                                            parent_proc.proc_id)
598
+            parent_iter = add("parent", cmdline2str(parent_proc.cmdline))
599
+            self.wid_details_tree.set_value(
600
+                parent_iter, self.DETAIL_PROC_ID, parent_proc.proc_id
601
+            )
553 602
         # add children
554 603
         child_procs = proc.children
555 604
         if child_procs is None:
556
-            add('children', '???')
605
+            add("children", "???")
557 606
         else:
558
-            list_iter = add('children', f'{len(child_procs):d} entries')
607
+            list_iter = add("children", f"{len(child_procs):d} entries")
559 608
             for i, child_proc in enumerate(child_procs):
560
-                child_iter = add(f'child {i:d}',
561
-                                 cmdline2str(child_proc.cmdline), list_iter)
609
+                child_iter = add(
610
+                    f"child {i:d}", cmdline2str(child_proc.cmdline), list_iter
611
+                )
562 612
                 self.wid_details_tree.set_value(
563
-                    child_iter, self.DETAIL_PROC_ID, child_proc.proc_id)
613
+                    child_iter, self.DETAIL_PROC_ID, child_proc.proc_id
614
+                )
564 615
             self.wid_details_view.expand_row(
565
-                self.wid_details_tree.get_path(list_iter), True)
616
+                self.wid_details_tree.get_path(list_iter), True
617
+            )
566 618
 
567 619
 
568 620
 def run(proto_filename):
... ...
@@ -570,9 +622,4 @@ def run(proto_filename):
570 622
     Run the graphical user interface for the specified trace file.
571 623
     """
572 624
     app = UptGui(proto_filename)
573
-    try:
574
-        Gtk.main()
575
-    except KeyboardInterrupt:
576
-        pass
577
-    finally:
578
-        del app
625
+    app.run(None)
579 626