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