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.33 kiB
Python script, ASCII text executable
        
            
1
"""
2
Panorama panel: traditional desktop panel using Wayland layer-shell
3
protocol.
4
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
5
6
This program is free software: you can redistribute it and/or modify
7
it under the terms of the GNU General Public Licence as published by
8
the Free Software Foundation, either version 3 of the Licence, or
9
(at your option) any later version.
10
11
This program is distributed in the hope that it will be useful,
12
but WITHOUT ANY WARRANTY; without even the implied warranty of
13
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
GNU General Public Licence for more details.
15
16
You should have received a copy of the GNU General Public Licence
17
along with this program. If not, see <https://www.gnu.org/licenses/>.
18
"""
19
20
from __future__ import annotations
21
22
import os
23
import sys
24
import importlib
25
import time
26
import traceback
27
import 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.left_area = AppletArea(orientation=box.get_orientation())
351
self.centre_area = AppletArea(orientation=box.get_orientation())
352
self.right_area = AppletArea(orientation=box.get_orientation())
353
354
box.set_start_widget(self.left_area)
355
box.set_center_widget(self.centre_area)
356
box.set_end_widget(self.right_area)
357
358
self.set_child(box)
359
self.set_position(position)
360
self.set_size(size)
361
self.set_autohide(autohide, hide_time)
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
for area in (self.left_area, self.centre_area, self.right_area):
476
area.set_orientation(Gtk.Orientation.HORIZONTAL)
477
case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
478
box.set_orientation(Gtk.Orientation.VERTICAL)
479
for area in (self.left_area, self.centre_area, self.right_area):
480
area.set_orientation(Gtk.Orientation.VERTICAL)
481
482
if self.autohide:
483
if not self.open_popovers:
484
GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)
485
486
def set_autohide(self, autohide: bool, hide_time: int):
487
self.autohide = autohide
488
self.hide_time = hide_time
489
if not self.autohide:
490
self.reset_margins()
491
Gtk4LayerShell.auto_exclusive_zone_enable(self)
492
if self.motion_controller is not None:
493
self.remove_controller(self.motion_controller)
494
if self.drop_motion_controller is not None:
495
self.remove_controller(self.drop_motion_controller)
496
self.motion_controller = None
497
self.drop_motion_controller = None
498
self.reset_margins()
499
else:
500
Gtk4LayerShell.set_exclusive_zone(self, 0)
501
# Only leave 1px of the window as a "mouse sensor"
502
Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 1 - self.size)
503
self.motion_controller = Gtk.EventControllerMotion()
504
self.add_controller(self.motion_controller)
505
self.drop_motion_controller = Gtk.DropControllerMotion()
506
self.add_controller(self.drop_motion_controller)
507
508
self.motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
509
self.drop_motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
510
self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
511
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))
512
513
def get_all_subclasses(klass: type) -> list[type]:
514
subclasses = []
515
for subclass in klass.__subclasses__():
516
subclasses.append(subclass)
517
subclasses += get_all_subclasses(subclass)
518
519
return subclasses
520
521
def load_packages_from_dir(dir_path: Path):
522
loaded_modules = []
523
524
for path in dir_path.iterdir():
525
if path.name.startswith("_"):
526
continue
527
528
if path.is_dir() and (path / "__init__.py").exists():
529
module_name = path.name
530
spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
531
module = importlib.util.module_from_spec(spec)
532
spec.loader.exec_module(module)
533
loaded_modules.append(module)
534
else:
535
continue
536
537
return loaded_modules
538
539
540
PANEL_POSITIONS = {
541
"top": Gtk.PositionType.TOP,
542
"bottom": Gtk.PositionType.BOTTOM,
543
"left": Gtk.PositionType.LEFT,
544
"right": Gtk.PositionType.RIGHT,
545
}
546
547
548
PANEL_POSITIONS_REVERSE = {
549
Gtk.PositionType.TOP: "top",
550
Gtk.PositionType.BOTTOM: "bottom",
551
Gtk.PositionType.LEFT: "left",
552
Gtk.PositionType.RIGHT: "right",
553
}
554
555
556
class PanoramaPanel(Gtk.Application):
557
def __init__(self):
558
super().__init__(
559
application_id="com.roundabout_host.roundabout.PanoramaPanel",
560
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
561
)
562
self.display = Gdk.Display.get_default()
563
self.monitors = self.display.get_monitors()
564
self.applets_by_name: dict[str, panorama_panel.Applet] = {}
565
self.panels: list[Panel] = []
566
self.manager_window = None
567
self.edit_mode = False
568
self.drags = {}
569
570
self.add_main_option(
571
"trigger",
572
ord("t"),
573
GLib.OptionFlags.NONE,
574
GLib.OptionArg.STRING,
575
_("Trigger an action which can be processed by the applets"),
576
_("NAME")
577
)
578
579
def do_startup(self):
580
Gtk.Application.do_startup(self)
581
for i, monitor in enumerate(self.monitors):
582
geometry = monitor.get_geometry()
583
print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")
584
585
all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
586
print("Applets:")
587
subclasses = get_all_subclasses(panorama_panel.Applet)
588
for subclass in subclasses:
589
if subclass.__name__ in self.applets_by_name:
590
print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr)
591
self.applets_by_name[subclass.__name__] = subclass
592
593
with open(get_config_file(), "r") as config_file:
594
yaml_loader = yaml.YAML(typ="rt")
595
yaml_file = yaml_loader.load(config_file)
596
for panel_data in yaml_file["panels"]:
597
position = PANEL_POSITIONS[panel_data["position"]]
598
monitor_index = panel_data["monitor"]
599
if monitor_index >= len(self.monitors):
600
continue
601
monitor = self.monitors[monitor_index]
602
size = panel_data["size"]
603
autohide = panel_data["autohide"]
604
hide_time = panel_data["hide_time"]
605
can_capture_keyboard = panel_data["can_capture_keyboard"]
606
607
panel = Panel(self, monitor, position, size, monitor_index, autohide, hide_time, can_capture_keyboard)
608
self.panels.append(panel)
609
610
print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)")
611
612
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
613
applet_list = panel_data["applets"].get(area_name)
614
if applet_list is None:
615
continue
616
617
for applet in applet_list:
618
item = list(applet.items())[0]
619
AppletClass = self.applets_by_name[item[0]]
620
options = item[1]
621
applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
622
applet_widget.set_panel_position(panel.position)
623
624
area.append(applet_widget)
625
626
panel.present()
627
panel.realize()
628
629
def do_activate(self):
630
Gio.Application.do_activate(self)
631
632
def save_config(self):
633
with open(get_config_file(), "w") as config_file:
634
yaml_writer = yaml.YAML(typ="rt")
635
data = {"panels": []}
636
for panel in self.panels:
637
panel_data = {
638
"position": PANEL_POSITIONS_REVERSE[panel.position],
639
"monitor": panel.monitor_index,
640
"size": panel.size,
641
"autohide": panel.autohide,
642
"hide_time": panel.hide_time,
643
"can_capture_keyboard": panel.can_capture_keyboard,
644
"applets": {}
645
}
646
647
for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
648
panel_data["applets"][area_name] = []
649
applet = area.get_first_child()
650
while applet is not None:
651
panel_data["applets"][area_name].append({
652
applet.__class__.__name__: applet.get_config(),
653
})
654
655
applet = applet.get_next_sibling()
656
657
data["panels"].append(panel_data)
658
659
yaml_writer.dump(data, config_file)
660
661
def do_shutdown(self):
662
print("Shutting down")
663
for panel in self.panels:
664
for area in (panel.left_area, panel.centre_area, panel.right_area):
665
applet = area.get_first_child()
666
while applet is not None:
667
applet.shutdown()
668
669
applet = applet.get_next_sibling()
670
Gtk.Application.do_shutdown(self)
671
self.save_config()
672
673
def do_command_line(self, command_line: Gio.ApplicationCommandLine):
674
options = command_line.get_options_dict()
675
args = command_line.get_arguments()[1:]
676
677
trigger_variant = options.lookup_value("trigger", GLib.VariantType("s"))
678
if trigger_variant:
679
action_name = trigger_variant.get_string()
680
print(app.list_actions())
681
self.activate_action(action_name, None)
682
print(f"Triggered {action_name}")
683
684
return 0
685
686
def set_edit_mode(self, value):
687
self.edit_mode = value
688
for panel in self.panels:
689
panel.set_edit_mode(value)
690
691
def reset_manager_window(self, *args):
692
self.manager_window = None
693
694
695
if __name__ == "__main__":
696
app = PanoramaPanel()
697
app.run(sys.argv)
698