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