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