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