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