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