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