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