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