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