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