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