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.28 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
Gtk4LayerShell.init_for_window(self)
360
Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.panel")
361
Gtk4LayerShell.set_monitor(self, monitor)
362
363
Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)
364
if can_capture_keyboard:
365
Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.ON_DEMAND)
366
else:
367
Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.NONE)
368
369
box = Gtk.CenterBox()
370
371
self.left_area = AppletArea(orientation=box.get_orientation())
372
self.centre_area = AppletArea(orientation=box.get_orientation())
373
self.right_area = AppletArea(orientation=box.get_orientation())
374
375
box.set_start_widget(self.left_area)
376
box.set_center_widget(self.centre_area)
377
box.set_end_widget(self.right_area)
378
379
self.set_child(box)
380
self.set_position(position)
381
self.set_size(size)
382
self.set_autohide(autohide, hide_time)
383
384
# Add a context menu
385
menu = Gio.Menu()
386
387
menu.append(_("Open _manager"), "panel.manager")
388
389
self.context_menu = Gtk.PopoverMenu.new_from_model(menu)
390
self.context_menu.set_has_arrow(False)
391
self.context_menu.set_parent(self)
392
self.context_menu.set_halign(Gtk.Align.START)
393
self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
394
panorama_panel.track_popover(self.context_menu)
395
396
right_click_controller = Gtk.GestureClick()
397
right_click_controller.set_button(3)
398
right_click_controller.connect("pressed", self.show_context_menu)
399
400
self.add_controller(right_click_controller)
401
402
action_group = Gio.SimpleActionGroup()
403
manager_action = Gio.SimpleAction.new("manager", None)
404
manager_action.connect("activate", self.show_manager)
405
action_group.add_action(manager_action)
406
self.insert_action_group("panel", action_group)
407
408
def reset_margins(self):
409
for edge in POSITION_TO_LAYER_SHELL_EDGE.values():
410
Gtk4LayerShell.set_margin(self, edge, 0)
411
412
def set_edit_mode(self, value):
413
for area in (self.left_area, self.centre_area, self.right_area):
414
area.set_edit_mode(value)
415
416
def show_context_menu(self, gesture, n_presses, x, y):
417
rect = Gdk.Rectangle()
418
rect.x = int(x)
419
rect.y = int(y)
420
rect.width = 1
421
rect.height = 1
422
423
self.context_menu.set_pointing_to(rect)
424
self.context_menu.popup()
425
426
def slide_in(self):
427
if not self.autohide:
428
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
429
return False
430
431
if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) >= 0:
432
return False
433
434
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) + 1)
435
return True
436
437
def slide_out(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]) <= 1 - self.size:
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 show_manager(self, _0=None, _1=None):
449
if self.get_application():
450
if not self.get_application().manager_window:
451
self.get_application().manager_window = PanelManager(self.get_application())
452
self.get_application().manager_window.connect("close-request", self.get_application().reset_manager_window)
453
self.get_application().manager_window.present()
454
455
def get_orientation(self):
456
box = self.get_first_child()
457
return box.get_orientation()
458
459
def set_size(self, value: int):
460
self.size = int(value)
461
if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
462
self.set_size_request(800, self.size)
463
self.set_default_size(800, self.size)
464
else:
465
self.set_size_request(self.size, 600)
466
self.set_default_size(self.size, 600)
467
468
def set_position(self, position: Gtk.PositionType):
469
self.position = position
470
self.reset_margins()
471
match self.position:
472
case Gtk.PositionType.TOP:
473
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, False)
474
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
475
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
476
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
477
case Gtk.PositionType.BOTTOM:
478
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, False)
479
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
480
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
481
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
482
case Gtk.PositionType.LEFT:
483
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, False)
484
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
485
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
486
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
487
case Gtk.PositionType.RIGHT:
488
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, False)
489
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
490
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
491
Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
492
box = self.get_first_child()
493
match self.position:
494
case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM:
495
box.set_orientation(Gtk.Orientation.HORIZONTAL)
496
for area in (self.left_area, self.centre_area, self.right_area):
497
area.set_orientation(Gtk.Orientation.HORIZONTAL)
498
case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
499
box.set_orientation(Gtk.Orientation.VERTICAL)
500
for area in (self.left_area, self.centre_area, self.right_area):
501
area.set_orientation(Gtk.Orientation.VERTICAL)
502
503
if self.autohide:
504
if not self.open_popovers:
505
GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)
506
507
def set_autohide(self, autohide: bool, hide_time: int):
508
self.autohide = autohide
509
self.hide_time = hide_time
510
if not self.autohide:
511
self.reset_margins()
512
Gtk4LayerShell.auto_exclusive_zone_enable(self)
513
if self.motion_controller is not None:
514
self.remove_controller(self.motion_controller)
515
if self.drop_motion_controller is not None:
516
self.remove_controller(self.drop_motion_controller)
517
self.motion_controller = None
518
self.drop_motion_controller = None
519
self.reset_margins()
520
else:
521
Gtk4LayerShell.set_exclusive_zone(self, 0)
522
# Only leave 1px of the window as a "mouse sensor"
523
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 1 - self.size)
524
self.motion_controller = Gtk.EventControllerMotion()
525
self.add_controller(self.motion_controller)
526
self.drop_motion_controller = Gtk.DropControllerMotion()
527
self.add_controller(self.drop_motion_controller)
528
529
self.motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
530
self.drop_motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
531
self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
532
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))
533
534
def get_all_subclasses(klass: type) -> list[type]:
535
subclasses = []
536
for subclass in klass.__subclasses__():
537
subclasses.append(subclass)
538
subclasses += get_all_subclasses(subclass)
539
540
return subclasses
541
542
def load_packages_from_dir(dir_path: Path):
543
loaded_modules = []
544
545
for path in dir_path.iterdir():
546
if path.name.startswith("_"):
547
continue
548
549
if path.is_dir() and (path / "__init__.py").exists():
550
try:
551
module_name = path.name
552
spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
553
module = importlib.util.module_from_spec(spec)
554
spec.loader.exec_module(module)
555
loaded_modules.append(module)
556
except Exception as e:
557
print(f"Failed to load applet module {path.name}: {e}", file=sys.stderr)
558
else:
559
continue
560
561
return loaded_modules
562
563
564
PANEL_POSITIONS = {
565
"top": Gtk.PositionType.TOP,
566
"bottom": Gtk.PositionType.BOTTOM,
567
"left": Gtk.PositionType.LEFT,
568
"right": Gtk.PositionType.RIGHT,
569
}
570
571
572
PANEL_POSITIONS_REVERSE = {
573
Gtk.PositionType.TOP: "top",
574
Gtk.PositionType.BOTTOM: "bottom",
575
Gtk.PositionType.LEFT: "left",
576
Gtk.PositionType.RIGHT: "right",
577
}
578
579
580
class PanoramaPanel(Gtk.Application):
581
def __init__(self):
582
super().__init__(
583
application_id="com.roundabout_host.roundabout.PanoramaPanel",
584
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
585
)
586
self.display = Gdk.Display.get_default()
587
self.monitors = self.display.get_monitors()
588
for monitor in self.monitors:
589
monitor.output_proxy = None
590
self.applets_by_name: dict[str, panorama_panel.Applet] = {}
591
self.panels: list[Panel] = []
592
self.manager_window = None
593
self.edit_mode = False
594
self.drags = {}
595
596
self.add_main_option(
597
"trigger",
598
ord("t"),
599
GLib.OptionFlags.NONE,
600
GLib.OptionArg.STRING,
601
_("Trigger an action which can be processed by the applets"),
602
_("NAME")
603
)
604
605
def do_startup(self):
606
Gtk.Application.do_startup(self)
607
for i, monitor in enumerate(self.monitors):
608
geometry = monitor.get_geometry()
609
print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")
610
611
all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
612
print("Applets:")
613
subclasses = get_all_subclasses(panorama_panel.Applet)
614
for subclass in subclasses:
615
if subclass.__name__ in self.applets_by_name:
616
print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr)
617
self.applets_by_name[subclass.__name__] = subclass
618
619
with open(get_config_file(), "r") as config_file:
620
yaml_loader = yaml.YAML(typ="rt")
621
yaml_file = yaml_loader.load(config_file)
622
for panel_data in yaml_file["panels"]:
623
position = PANEL_POSITIONS[panel_data["position"]]
624
monitor_index = panel_data["monitor"]
625
if monitor_index >= len(self.monitors):
626
continue
627
monitor = self.monitors[monitor_index]
628
size = panel_data["size"]
629
autohide = panel_data["autohide"]
630
hide_time = panel_data["hide_time"]
631
can_capture_keyboard = panel_data["can_capture_keyboard"]
632
633
panel = Panel(self, monitor, position, size, autohide, hide_time, can_capture_keyboard, monitor_index)
634
self.panels.append(panel)
635
636
print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)")
637
638
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
639
applet_list = panel_data["applets"].get(area_name)
640
if applet_list is None:
641
continue
642
643
for applet in applet_list:
644
item = list(applet.items())[0]
645
AppletClass = self.applets_by_name[item[0]]
646
options = item[1]
647
applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
648
applet_widget.connect("config-changed", self.save_config)
649
applet_widget.set_panel_position(panel.position)
650
651
area.append(applet_widget)
652
653
panel.present()
654
panel.realize()
655
656
def do_activate(self):
657
Gio.Application.do_activate(self)
658
659
def save_config(self, *args):
660
print("Saving configuration file")
661
with open(get_config_file(), "w") as config_file:
662
yaml_writer = yaml.YAML(typ="rt")
663
data = {"panels": []}
664
for panel in self.panels:
665
panel_data = {
666
"position": PANEL_POSITIONS_REVERSE[panel.position],
667
"monitor": panel.monitor_index,
668
"size": panel.size,
669
"autohide": panel.autohide,
670
"hide_time": panel.hide_time,
671
"can_capture_keyboard": panel.can_capture_keyboard,
672
"applets": {}
673
}
674
675
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
676
panel_data["applets"][area_name] = []
677
applet = area.get_first_child()
678
while applet is not None:
679
panel_data["applets"][area_name].append({
680
applet.__class__.__name__: applet.get_config(),
681
})
682
683
applet = applet.get_next_sibling()
684
685
data["panels"].append(panel_data)
686
687
yaml_writer.dump(data, config_file)
688
689
def do_shutdown(self):
690
print("Shutting down")
691
for panel in self.panels:
692
for area in (panel.left_area, panel.centre_area, panel.right_area):
693
applet = area.get_first_child()
694
while applet is not None:
695
applet.shutdown()
696
697
applet = applet.get_next_sibling()
698
Gtk.Application.do_shutdown(self)
699
700
def do_command_line(self, command_line: Gio.ApplicationCommandLine):
701
options = command_line.get_options_dict()
702
args = command_line.get_arguments()[1:]
703
704
trigger_variant = options.lookup_value("trigger", GLib.VariantType("s"))
705
if trigger_variant:
706
action_name = trigger_variant.get_string()
707
print(app.list_actions())
708
self.activate_action(action_name, None)
709
print(f"Triggered {action_name}")
710
711
return 0
712
713
def set_edit_mode(self, value):
714
self.edit_mode = value
715
for panel in self.panels:
716
panel.set_edit_mode(value)
717
718
def reset_manager_window(self, *args):
719
self.manager_window = None
720
721
722
if __name__ == "__main__":
723
app = PanoramaPanel()
724
app.run(sys.argv)
725