By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 main.py

View raw Download
text/plain • 27.93 kiB
Python script, ASCII text executable
        
            
1
"""
2
Panorama panel: traditional desktop panel using Wayland layer-shell
3
protocol.
4
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
5
6
This program is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public Licence as published by
8
the Free Software Foundation, either version 3 of the Licence, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
GNU General Public Licence for more details.
15
16
You should have received a copy of the GNU General Public Licence
17
along with this program. If not, see <https://www.gnu.org/licenses/>.
18
"""
19
20
from __future__ import annotations
21
22
import os
23
import sys
24
import importlib
25
import time
26
import traceback
27
import faulthandler
28
import typing
29
import locale
30
from itertools import accumulate, chain
31
from pathlib import Path
32
import ruamel.yaml as yaml
33
from pywayland.client import Display
34
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor
35
36
faulthandler.enable()
37
38
os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"
39
40
from ctypes import CDLL
41
CDLL('libgtk4-layer-shell.so')
42
43
import gi
44
gi.require_version("Gtk", "4.0")
45
gi.require_version("Gtk4LayerShell", "1.0")
46
47
from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio, GObject
48
from gi.events import GLibEventLoopPolicy
49
50
sys.path.insert(0, str((Path(__file__).parent / "shared").resolve()))
51
52
locale.bindtextdomain("panorama-panel", "locale")
53
locale.textdomain("panorama-panel")
54
locale.setlocale(locale.LC_ALL)
55
_ = locale.gettext
56
57
import panorama_panel
58
59
import asyncio
60
asyncio.set_event_loop_policy(GLibEventLoopPolicy())
61
62
custom_css = """
63
.panel-flash {
64
animation: flash 333ms ease-in-out 0s 2;
65
}
66
67
@keyframes flash {
68
0% {
69
background-color: initial;
70
}
71
50% {
72
background-color: #ffff0080;
73
}
74
0% {
75
background-color: initial;
76
}
77
}
78
"""
79
80
css_provider = Gtk.CssProvider()
81
css_provider.load_from_data(custom_css)
82
Gtk.StyleContext.add_provider_for_display(
83
Gdk.Display.get_default(),
84
css_provider,
85
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
86
)
87
88
89
@Gtk.Template(filename="panel-configurator.ui")
90
class PanelConfigurator(Gtk.Frame):
91
__gtype_name__ = "PanelConfigurator"
92
93
panel_size_adjustment: Gtk.Adjustment = Gtk.Template.Child()
94
monitor_number_adjustment: Gtk.Adjustment = Gtk.Template.Child()
95
top_position_radio: Gtk.CheckButton = Gtk.Template.Child()
96
bottom_position_radio: Gtk.CheckButton = Gtk.Template.Child()
97
left_position_radio: Gtk.CheckButton = Gtk.Template.Child()
98
right_position_radio: Gtk.CheckButton = Gtk.Template.Child()
99
autohide_switch: Gtk.Switch = Gtk.Template.Child()
100
101
def __init__(self, panel: Panel, **kwargs):
102
super().__init__(**kwargs)
103
self.panel = panel
104
self.panel_size_adjustment.set_value(panel.size)
105
106
match self.panel.position:
107
case Gtk.PositionType.TOP:
108
self.top_position_radio.set_active(True)
109
case Gtk.PositionType.BOTTOM:
110
self.bottom_position_radio.set_active(True)
111
case Gtk.PositionType.LEFT:
112
self.left_position_radio.set_active(True)
113
case Gtk.PositionType.RIGHT:
114
self.right_position_radio.set_active(True)
115
116
self.top_position_radio.panel_position_target = Gtk.PositionType.TOP
117
self.bottom_position_radio.panel_position_target = Gtk.PositionType.BOTTOM
118
self.left_position_radio.panel_position_target = Gtk.PositionType.LEFT
119
self.right_position_radio.panel_position_target = Gtk.PositionType.RIGHT
120
self.autohide_switch.set_active(self.panel.autohide)
121
122
@Gtk.Template.Callback()
123
def update_panel_size(self, adjustment: Gtk.Adjustment):
124
if not self.get_root():
125
return
126
self.panel.set_size(int(adjustment.get_value()))
127
128
self.panel.get_application().save_config()
129
130
@Gtk.Template.Callback()
131
def toggle_autohide(self, switch: Gtk.Switch, value: bool):
132
if not self.get_root():
133
return
134
self.panel.set_autohide(value, self.panel.hide_time)
135
136
self.panel.get_application().save_config()
137
138
@Gtk.Template.Callback()
139
def move_panel(self, button: Gtk.CheckButton):
140
if not self.get_root():
141
return
142
if not button.get_active():
143
return
144
145
self.panel.set_position(button.panel_position_target)
146
self.update_panel_size(self.panel_size_adjustment)
147
148
# Make the applets aware of the changed orientation
149
for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
150
applet = area.get_first_child()
151
while applet:
152
applet.set_orientation(self.panel.get_orientation())
153
applet.set_panel_position(self.panel.position)
154
applet.queue_resize()
155
applet = applet.get_next_sibling()
156
157
self.panel.get_application().save_config()
158
159
@Gtk.Template.Callback()
160
def move_to_monitor(self, adjustment: Gtk.Adjustment):
161
if not self.get_root():
162
return
163
app: PanoramaPanel = self.get_root().get_application()
164
monitor = app.monitors[int(self.monitor_number_adjustment.get_value())]
165
self.panel.unmap()
166
Gtk4LayerShell.set_monitor(self.panel, monitor)
167
self.panel.show()
168
169
# Make the applets aware of the changed monitor
170
for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
171
applet = area.get_first_child()
172
while applet:
173
applet.output_changed()
174
applet = applet.get_next_sibling()
175
176
self.panel.get_application().save_config()
177
178
179
PANEL_POSITIONS_HUMAN = {
180
Gtk.PositionType.TOP: "top",
181
Gtk.PositionType.BOTTOM: "bottom",
182
Gtk.PositionType.LEFT: "left",
183
Gtk.PositionType.RIGHT: "right",
184
}
185
186
187
@Gtk.Template(filename="panel-manager.ui")
188
class PanelManager(Gtk.Window):
189
__gtype_name__ = "PanelManager"
190
191
panel_editing_switch: Gtk.Switch = Gtk.Template.Child()
192
panel_stack: Gtk.Stack = Gtk.Template.Child()
193
current_panel: typing.Optional[Panel] = None
194
195
def __init__(self, application: Gtk.Application, **kwargs):
196
super().__init__(application=application, **kwargs)
197
198
self.connect("close-request", lambda *args: self.destroy())
199
200
action_group = Gio.SimpleActionGroup()
201
202
self.next_panel_action = Gio.SimpleAction(name="next-panel")
203
action_group.add_action(self.next_panel_action)
204
self.next_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_next_sibling()))
205
206
self.previous_panel_action = Gio.SimpleAction(name="previous-panel")
207
action_group.add_action(self.previous_panel_action)
208
self.previous_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_prev_sibling()))
209
210
self.insert_action_group("win", action_group)
211
if isinstance(self.get_application(), PanoramaPanel):
212
self.panel_editing_switch.set_active(application.edit_mode)
213
self.panel_editing_switch.connect("state-set", self.set_edit_mode)
214
215
self.connect("close-request", lambda *args: self.unflash_old_panel())
216
self.connect("close-request", lambda *args: self.destroy())
217
218
if isinstance(self.get_application(), PanoramaPanel):
219
app: PanoramaPanel = self.get_application()
220
for panel in app.panels:
221
configurator = PanelConfigurator(panel)
222
self.panel_stack.add_child(configurator)
223
configurator.monitor_number_adjustment.set_upper(len(app.monitors))
224
225
self.panel_stack.set_visible_child(self.panel_stack.get_first_child())
226
self.panel_stack.connect("notify::visible-child", self.set_visible_panel)
227
self.panel_stack.notify("visible-child")
228
229
def unflash_old_panel(self):
230
if self.current_panel:
231
for area in (self.current_panel.left_area, self.current_panel.centre_area, self.current_panel.right_area):
232
area.unflash()
233
234
def set_visible_panel(self, stack: Gtk.Stack, pspec: GObject.ParamSpec):
235
self.unflash_old_panel()
236
237
panel: Panel = stack.get_visible_child().panel
238
239
self.current_panel = panel
240
self.next_panel_action.set_enabled(stack.get_visible_child().get_next_sibling() is not None)
241
self.previous_panel_action.set_enabled(stack.get_visible_child().get_prev_sibling() is not None)
242
243
# Start an animation to show the user what panel is being edited
244
for area in (panel.left_area, panel.centre_area, panel.right_area):
245
area.flash()
246
247
def set_edit_mode(self, switch, value):
248
if isinstance(self.get_application(), PanoramaPanel):
249
self.get_application().set_edit_mode(value)
250
251
@Gtk.Template.Callback()
252
def save_settings(self, *args):
253
if isinstance(self.get_application(), PanoramaPanel):
254
self.get_application().save_config()
255
256
257
def get_applet_directories():
258
data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
259
data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")]
260
261
all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs]
262
return [d for d in all_paths if d.is_dir()]
263
264
265
def get_config_file():
266
config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
267
268
return config_home / "panorama-panel" / "config.yaml"
269
270
271
class AppletArea(Gtk.Box):
272
def __init__(self, orientation=Gtk.Orientation.HORIZONTAL):
273
super().__init__()
274
275
self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE)
276
self.drop_target.set_gtypes([GObject.TYPE_UINT64])
277
self.drop_target.connect("drop", self.drop_applet)
278
279
def drop_applet(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
280
applet = self.get_root().get_application().drags[value]
281
old_area: AppletArea = applet.get_parent()
282
old_area.remove(applet)
283
# Find the position where to insert the applet
284
child = self.get_first_child()
285
while child:
286
allocation = child.get_allocation()
287
child_x, child_y = self.translate_coordinates(self, 0, 0)
288
if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
289
midpoint = child_x + allocation.width / 2
290
if x < midpoint:
291
applet.insert_before(self, child)
292
break
293
elif self.get_orientation() == Gtk.Orientation.VERTICAL:
294
midpoint = child_y + allocation.height / 2
295
if y < midpoint:
296
applet.insert_before(self, child)
297
break
298
child = child.get_next_sibling()
299
else:
300
self.append(applet)
301
applet.show()
302
303
self.get_root().get_application().drags.pop(value)
304
self.get_root().get_application().save_config()
305
306
return True
307
308
def set_edit_mode(self, value):
309
panel: Panel = self.get_root()
310
child = self.get_first_child()
311
while child is not None:
312
if value:
313
child.make_draggable()
314
else:
315
child.restore_drag()
316
child.set_opacity(0.75 if value else 1)
317
child = child.get_next_sibling()
318
319
if value:
320
self.add_controller(self.drop_target)
321
if panel.get_orientation() == Gtk.Orientation.HORIZONTAL:
322
self.set_size_request(48, 0)
323
elif panel.get_orientation() == Gtk.Orientation.VERTICAL:
324
self.set_size_request(0, 48)
325
else:
326
self.remove_controller(self.drop_target)
327
self.set_size_request(0, 0)
328
329
def flash(self):
330
self.add_css_class("panel-flash")
331
332
def unflash(self):
333
self.remove_css_class("panel-flash")
334
335
336
POSITION_TO_LAYER_SHELL_EDGE = {
337
Gtk.PositionType.TOP: Gtk4LayerShell.Edge.TOP,
338
Gtk.PositionType.BOTTOM: Gtk4LayerShell.Edge.BOTTOM,
339
Gtk.PositionType.LEFT: Gtk4LayerShell.Edge.LEFT,
340
Gtk.PositionType.RIGHT: Gtk4LayerShell.Edge.RIGHT,
341
}
342
343
344
class Panel(Gtk.Window):
345
def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, autohide: bool = False, hide_time: int = 0, can_capture_keyboard: bool = False):
346
super().__init__(application=application)
347
self.drop_motion_controller = None
348
self.motion_controller = None
349
self.set_decorated(False)
350
self.position = None
351
self.autohide = None
352
self.monitor_name = monitor.get_connector()
353
self.hide_time = None
354
self.can_capture_keyboard = can_capture_keyboard
355
self.open_popovers: set[int] = set()
356
357
Gtk4LayerShell.init_for_window(self)
358
Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.panel")
359
Gtk4LayerShell.set_monitor(self, monitor)
360
361
Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)
362
if can_capture_keyboard:
363
Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.ON_DEMAND)
364
else:
365
Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.NONE)
366
367
box = Gtk.CenterBox()
368
369
self.left_area = AppletArea(orientation=box.get_orientation())
370
self.centre_area = AppletArea(orientation=box.get_orientation())
371
self.right_area = AppletArea(orientation=box.get_orientation())
372
373
box.set_start_widget(self.left_area)
374
box.set_center_widget(self.centre_area)
375
box.set_end_widget(self.right_area)
376
377
self.set_child(box)
378
self.set_position(position)
379
self.set_size(size)
380
self.set_autohide(autohide, hide_time)
381
382
# Add a context menu
383
menu = Gio.Menu()
384
385
menu.append(_("Open _manager"), "panel.manager")
386
387
self.context_menu = Gtk.PopoverMenu.new_from_model(menu)
388
self.context_menu.set_has_arrow(False)
389
self.context_menu.set_parent(self)
390
self.context_menu.set_halign(Gtk.Align.START)
391
self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
392
panorama_panel.track_popover(self.context_menu)
393
394
right_click_controller = Gtk.GestureClick()
395
right_click_controller.set_button(3)
396
right_click_controller.connect("pressed", self.show_context_menu)
397
398
self.add_controller(right_click_controller)
399
400
action_group = Gio.SimpleActionGroup()
401
manager_action = Gio.SimpleAction.new("manager", None)
402
manager_action.connect("activate", self.show_manager)
403
action_group.add_action(manager_action)
404
self.insert_action_group("panel", action_group)
405
406
def reset_margins(self):
407
for edge in POSITION_TO_LAYER_SHELL_EDGE.values():
408
Gtk4LayerShell.set_margin(self, edge, 0)
409
410
def set_edit_mode(self, value):
411
for area in (self.left_area, self.centre_area, self.right_area):
412
area.set_edit_mode(value)
413
414
def show_context_menu(self, gesture, n_presses, x, y):
415
rect = Gdk.Rectangle()
416
rect.x = int(x)
417
rect.y = int(y)
418
rect.width = 1
419
rect.height = 1
420
421
self.context_menu.set_pointing_to(rect)
422
self.context_menu.popup()
423
424
def slide_in(self):
425
if not self.autohide:
426
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
427
return False
428
429
if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) >= 0:
430
return False
431
432
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) + 1)
433
return True
434
435
def slide_out(self):
436
if not self.autohide:
437
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
438
return False
439
440
if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) <= 1 - self.size:
441
return False
442
443
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) - 1)
444
return True
445
446
def show_manager(self, _0=None, _1=None):
447
if self.get_application():
448
if not self.get_application().manager_window:
449
self.get_application().manager_window = PanelManager(self.get_application())
450
self.get_application().manager_window.connect("close-request", self.get_application().reset_manager_window)
451
self.get_application().manager_window.present()
452
453
def get_orientation(self):
454
box = self.get_first_child()
455
return box.get_orientation()
456
457
def set_size(self, value: int):
458
self.size = int(value)
459
if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
460
self.set_size_request(800, self.size)
461
self.set_default_size(800, self.size)
462
else:
463
self.set_size_request(self.size, 600)
464
self.set_default_size(self.size, 600)
465
466
def set_position(self, position: Gtk.PositionType):
467
self.position = position
468
self.reset_margins()
469
match self.position:
470
case Gtk.PositionType.TOP:
471
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, False)
472
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
473
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
474
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
475
case Gtk.PositionType.BOTTOM:
476
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, False)
477
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
478
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
479
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
480
case Gtk.PositionType.LEFT:
481
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, False)
482
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
483
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
484
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
485
case Gtk.PositionType.RIGHT:
486
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, False)
487
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
488
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
489
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
490
box = self.get_first_child()
491
match self.position:
492
case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM:
493
box.set_orientation(Gtk.Orientation.HORIZONTAL)
494
for area in (self.left_area, self.centre_area, self.right_area):
495
area.set_orientation(Gtk.Orientation.HORIZONTAL)
496
case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
497
box.set_orientation(Gtk.Orientation.VERTICAL)
498
for area in (self.left_area, self.centre_area, self.right_area):
499
area.set_orientation(Gtk.Orientation.VERTICAL)
500
501
if self.autohide:
502
if not self.open_popovers:
503
GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)
504
505
def set_autohide(self, autohide: bool, hide_time: int):
506
self.autohide = autohide
507
self.hide_time = hide_time
508
if not self.autohide:
509
self.reset_margins()
510
Gtk4LayerShell.auto_exclusive_zone_enable(self)
511
if self.motion_controller is not None:
512
self.remove_controller(self.motion_controller)
513
if self.drop_motion_controller is not None:
514
self.remove_controller(self.drop_motion_controller)
515
self.motion_controller = None
516
self.drop_motion_controller = None
517
self.reset_margins()
518
else:
519
Gtk4LayerShell.set_exclusive_zone(self, 0)
520
# Only leave 1px of the window as a "mouse sensor"
521
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 1 - self.size)
522
self.motion_controller = Gtk.EventControllerMotion()
523
self.add_controller(self.motion_controller)
524
self.drop_motion_controller = Gtk.DropControllerMotion()
525
self.add_controller(self.drop_motion_controller)
526
527
self.motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
528
self.drop_motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
529
self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
530
self.drop_motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
531
532
def get_all_subclasses(klass: type) -> list[type]:
533
subclasses = []
534
for subclass in klass.__subclasses__():
535
subclasses.append(subclass)
536
subclasses += get_all_subclasses(subclass)
537
538
return subclasses
539
540
def load_packages_from_dir(dir_path: Path):
541
loaded_modules = []
542
543
for path in dir_path.iterdir():
544
if path.name.startswith("_"):
545
continue
546
547
if path.is_dir() and (path / "__init__.py").exists():
548
module_name = path.name
549
spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
550
module = importlib.util.module_from_spec(spec)
551
spec.loader.exec_module(module)
552
loaded_modules.append(module)
553
else:
554
continue
555
556
return loaded_modules
557
558
559
PANEL_POSITIONS = {
560
"top": Gtk.PositionType.TOP,
561
"bottom": Gtk.PositionType.BOTTOM,
562
"left": Gtk.PositionType.LEFT,
563
"right": Gtk.PositionType.RIGHT,
564
}
565
566
567
PANEL_POSITIONS_REVERSE = {
568
Gtk.PositionType.TOP: "top",
569
Gtk.PositionType.BOTTOM: "bottom",
570
Gtk.PositionType.LEFT: "left",
571
Gtk.PositionType.RIGHT: "right",
572
}
573
574
575
class PanoramaPanel(Gtk.Application):
576
def __init__(self):
577
super().__init__(
578
application_id="com.roundabout_host.roundabout.PanoramaPanel",
579
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
580
)
581
self.display = Gdk.Display.get_default()
582
self.monitors = self.display.get_monitors()
583
self.applets_by_name: dict[str, panorama_panel.Applet] = {}
584
self.panels: list[Panel] = []
585
self.manager_window = None
586
self.edit_mode = False
587
self.drags = {}
588
589
self.add_main_option(
590
"trigger",
591
ord("t"),
592
GLib.OptionFlags.NONE,
593
GLib.OptionArg.STRING,
594
_("Trigger an action which can be processed by the applets"),
595
_("NAME")
596
)
597
598
def do_startup(self):
599
Gtk.Application.do_startup(self)
600
for i, monitor in enumerate(self.monitors):
601
geometry = monitor.get_geometry()
602
print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")
603
604
all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
605
print("Applets:")
606
subclasses = get_all_subclasses(panorama_panel.Applet)
607
for subclass in subclasses:
608
if subclass.__name__ in self.applets_by_name:
609
print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr)
610
self.applets_by_name[subclass.__name__] = subclass
611
612
with open(get_config_file(), "r") as config_file:
613
yaml_loader = yaml.YAML(typ="rt")
614
yaml_file = yaml_loader.load(config_file)
615
for panel_data in yaml_file["panels"]:
616
position = PANEL_POSITIONS[panel_data["position"]]
617
monitor_index = panel_data["monitor"]
618
if monitor_index >= len(self.monitors):
619
continue
620
monitor = self.monitors[monitor_index]
621
size = panel_data["size"]
622
autohide = panel_data["autohide"]
623
hide_time = panel_data["hide_time"]
624
can_capture_keyboard = panel_data["can_capture_keyboard"]
625
626
panel = Panel(self, monitor, position, size, autohide, hide_time, can_capture_keyboard)
627
self.panels.append(panel)
628
629
print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)")
630
631
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
632
applet_list = panel_data["applets"].get(area_name)
633
if applet_list is None:
634
continue
635
636
for applet in applet_list:
637
item = list(applet.items())[0]
638
AppletClass = self.applets_by_name[item[0]]
639
options = item[1]
640
applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
641
applet_widget.connect("config-changed", self.save_config)
642
applet_widget.set_panel_position(panel.position)
643
644
area.append(applet_widget)
645
646
panel.present()
647
panel.realize()
648
649
def do_activate(self):
650
Gio.Application.do_activate(self)
651
652
def save_config(self, *args):
653
print("Saving configuration file")
654
with open(get_config_file(), "w") as config_file:
655
yaml_writer = yaml.YAML(typ="rt")
656
data = {"panels": []}
657
for panel in self.panels:
658
panel_data = {
659
"position": PANEL_POSITIONS_REVERSE[panel.position],
660
"monitor": panel.monitor_index,
661
"size": panel.size,
662
"autohide": panel.autohide,
663
"hide_time": panel.hide_time,
664
"can_capture_keyboard": panel.can_capture_keyboard,
665
"applets": {}
666
}
667
668
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
669
panel_data["applets"][area_name] = []
670
applet = area.get_first_child()
671
while applet is not None:
672
panel_data["applets"][area_name].append({
673
applet.__class__.__name__: applet.get_config(),
674
})
675
676
applet = applet.get_next_sibling()
677
678
data["panels"].append(panel_data)
679
680
yaml_writer.dump(data, config_file)
681
682
def do_shutdown(self):
683
print("Shutting down")
684
for panel in self.panels:
685
for area in (panel.left_area, panel.centre_area, panel.right_area):
686
applet = area.get_first_child()
687
while applet is not None:
688
applet.shutdown()
689
690
applet = applet.get_next_sibling()
691
Gtk.Application.do_shutdown(self)
692
693
def do_command_line(self, command_line: Gio.ApplicationCommandLine):
694
options = command_line.get_options_dict()
695
args = command_line.get_arguments()[1:]
696
697
trigger_variant = options.lookup_value("trigger", GLib.VariantType("s"))
698
if trigger_variant:
699
action_name = trigger_variant.get_string()
700
print(app.list_actions())
701
self.activate_action(action_name, None)
702
print(f"Triggered {action_name}")
703
704
return 0
705
706
def set_edit_mode(self, value):
707
self.edit_mode = value
708
for panel in self.panels:
709
panel.set_edit_mode(value)
710
711
def reset_manager_window(self, *args):
712
self.manager_window = None
713
714
715
if __name__ == "__main__":
716
app = PanoramaPanel()
717
app.run(sys.argv)
718