By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 main.py

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