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() 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 359Gtk4LayerShell.init_for_window(self) 360Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.panel") 361Gtk4LayerShell.set_monitor(self, monitor) 362 363Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP) 364if can_capture_keyboard: 365Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.ON_DEMAND) 366else: 367Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.NONE) 368 369box = Gtk.CenterBox() 370 371self.left_area = AppletArea(orientation=box.get_orientation()) 372self.centre_area = AppletArea(orientation=box.get_orientation()) 373self.right_area = AppletArea(orientation=box.get_orientation()) 374 375box.set_start_widget(self.left_area) 376box.set_center_widget(self.centre_area) 377box.set_end_widget(self.right_area) 378 379self.set_child(box) 380self.set_position(position) 381self.set_size(size) 382self.set_autohide(autohide, hide_time) 383 384# Add a context menu 385menu = Gio.Menu() 386 387menu.append(_("Open _manager"), "panel.manager") 388 389self.context_menu = Gtk.PopoverMenu.new_from_model(menu) 390self.context_menu.set_has_arrow(False) 391self.context_menu.set_parent(self) 392self.context_menu.set_halign(Gtk.Align.START) 393self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 394panorama_panel.track_popover(self.context_menu) 395 396right_click_controller = Gtk.GestureClick() 397right_click_controller.set_button(3) 398right_click_controller.connect("pressed", self.show_context_menu) 399 400self.add_controller(right_click_controller) 401 402action_group = Gio.SimpleActionGroup() 403manager_action = Gio.SimpleAction.new("manager", None) 404manager_action.connect("activate", self.show_manager) 405action_group.add_action(manager_action) 406self.insert_action_group("panel", action_group) 407 408def reset_margins(self): 409for edge in POSITION_TO_LAYER_SHELL_EDGE.values(): 410Gtk4LayerShell.set_margin(self, edge, 0) 411 412def set_edit_mode(self, value): 413for area in (self.left_area, self.centre_area, self.right_area): 414area.set_edit_mode(value) 415 416def show_context_menu(self, gesture, n_presses, x, y): 417rect = Gdk.Rectangle() 418rect.x = int(x) 419rect.y = int(y) 420rect.width = 1 421rect.height = 1 422 423self.context_menu.set_pointing_to(rect) 424self.context_menu.popup() 425 426def slide_in(self): 427if not self.autohide: 428Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0) 429return False 430 431if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) >= 0: 432return False 433 434Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) + 1) 435return True 436 437def slide_out(self): 438if not self.autohide: 439Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0) 440return False 441 442if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) <= 1 - self.size: 443return False 444 445Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) - 1) 446return True 447 448def show_manager(self, _0=None, _1=None): 449if self.get_application(): 450if not self.get_application().manager_window: 451self.get_application().manager_window = PanelManager(self.get_application()) 452self.get_application().manager_window.connect("close-request", self.get_application().reset_manager_window) 453self.get_application().manager_window.present() 454 455def get_orientation(self): 456box = self.get_first_child() 457return box.get_orientation() 458 459def set_size(self, value: int): 460self.size = int(value) 461if self.get_orientation() == Gtk.Orientation.HORIZONTAL: 462self.set_size_request(800, self.size) 463self.set_default_size(800, self.size) 464else: 465self.set_size_request(self.size, 600) 466self.set_default_size(self.size, 600) 467 468def set_position(self, position: Gtk.PositionType): 469self.position = position 470self.reset_margins() 471match self.position: 472case Gtk.PositionType.TOP: 473Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, False) 474Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True) 475Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True) 476Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True) 477case Gtk.PositionType.BOTTOM: 478Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, False) 479Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True) 480Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True) 481Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True) 482case Gtk.PositionType.LEFT: 483Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, False) 484Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True) 485Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True) 486Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True) 487case Gtk.PositionType.RIGHT: 488Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, False) 489Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True) 490Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True) 491Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True) 492box = self.get_first_child() 493match self.position: 494case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM: 495box.set_orientation(Gtk.Orientation.HORIZONTAL) 496for area in (self.left_area, self.centre_area, self.right_area): 497area.set_orientation(Gtk.Orientation.HORIZONTAL) 498case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT: 499box.set_orientation(Gtk.Orientation.VERTICAL) 500for area in (self.left_area, self.centre_area, self.right_area): 501area.set_orientation(Gtk.Orientation.VERTICAL) 502 503if self.autohide: 504if not self.open_popovers: 505GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out) 506 507def set_autohide(self, autohide: bool, hide_time: int): 508self.autohide = autohide 509self.hide_time = hide_time 510if not self.autohide: 511self.reset_margins() 512Gtk4LayerShell.auto_exclusive_zone_enable(self) 513if self.motion_controller is not None: 514self.remove_controller(self.motion_controller) 515if self.drop_motion_controller is not None: 516self.remove_controller(self.drop_motion_controller) 517self.motion_controller = None 518self.drop_motion_controller = None 519self.reset_margins() 520else: 521Gtk4LayerShell.set_exclusive_zone(self, 0) 522# Only leave 1px of the window as a "mouse sensor" 523Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 1 - self.size) 524self.motion_controller = Gtk.EventControllerMotion() 525self.add_controller(self.motion_controller) 526self.drop_motion_controller = Gtk.DropControllerMotion() 527self.add_controller(self.drop_motion_controller) 528 529self.motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in)) 530self.drop_motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in)) 531self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)) 532self.drop_motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)) 533 534def get_all_subclasses(klass: type) -> list[type]: 535subclasses = [] 536for subclass in klass.__subclasses__(): 537subclasses.append(subclass) 538subclasses += get_all_subclasses(subclass) 539 540return subclasses 541 542def load_packages_from_dir(dir_path: Path): 543loaded_modules = [] 544 545for path in dir_path.iterdir(): 546if path.name.startswith("_"): 547continue 548 549if path.is_dir() and (path / "__init__.py").exists(): 550module_name = path.name 551spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py") 552module = importlib.util.module_from_spec(spec) 553spec.loader.exec_module(module) 554loaded_modules.append(module) 555else: 556continue 557 558return loaded_modules 559 560 561PANEL_POSITIONS = { 562"top": Gtk.PositionType.TOP, 563"bottom": Gtk.PositionType.BOTTOM, 564"left": Gtk.PositionType.LEFT, 565"right": Gtk.PositionType.RIGHT, 566} 567 568 569PANEL_POSITIONS_REVERSE = { 570Gtk.PositionType.TOP: "top", 571Gtk.PositionType.BOTTOM: "bottom", 572Gtk.PositionType.LEFT: "left", 573Gtk.PositionType.RIGHT: "right", 574} 575 576 577class PanoramaPanel(Gtk.Application): 578def __init__(self): 579super().__init__( 580application_id="com.roundabout_host.roundabout.PanoramaPanel", 581flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE 582) 583self.display = Gdk.Display.get_default() 584self.monitors = self.display.get_monitors() 585for monitor in self.monitors: 586monitor.output_proxy = None 587self.applets_by_name: dict[str, panorama_panel.Applet] = {} 588self.panels: list[Panel] = [] 589self.manager_window = None 590self.edit_mode = False 591self.drags = {} 592 593self.add_main_option( 594"trigger", 595ord("t"), 596GLib.OptionFlags.NONE, 597GLib.OptionArg.STRING, 598_("Trigger an action which can be processed by the applets"), 599_("NAME") 600) 601 602def do_startup(self): 603Gtk.Application.do_startup(self) 604for i, monitor in enumerate(self.monitors): 605geometry = monitor.get_geometry() 606print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}") 607 608all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories())) 609print("Applets:") 610subclasses = get_all_subclasses(panorama_panel.Applet) 611for subclass in subclasses: 612if subclass.__name__ in self.applets_by_name: 613print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr) 614self.applets_by_name[subclass.__name__] = subclass 615 616with open(get_config_file(), "r") as config_file: 617yaml_loader = yaml.YAML(typ="rt") 618yaml_file = yaml_loader.load(config_file) 619for panel_data in yaml_file["panels"]: 620position = PANEL_POSITIONS[panel_data["position"]] 621monitor_index = panel_data["monitor"] 622if monitor_index >= len(self.monitors): 623continue 624monitor = self.monitors[monitor_index] 625size = panel_data["size"] 626autohide = panel_data["autohide"] 627hide_time = panel_data["hide_time"] 628can_capture_keyboard = panel_data["can_capture_keyboard"] 629 630panel = Panel(self, monitor, position, size, autohide, hide_time, can_capture_keyboard, monitor_index) 631self.panels.append(panel) 632 633print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)") 634 635for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)): 636applet_list = panel_data["applets"].get(area_name) 637if applet_list is None: 638continue 639 640for applet in applet_list: 641item = list(applet.items())[0] 642AppletClass = self.applets_by_name[item[0]] 643options = item[1] 644applet_widget = AppletClass(orientation=panel.get_orientation(), config=options) 645applet_widget.connect("config-changed", self.save_config) 646applet_widget.set_panel_position(panel.position) 647 648area.append(applet_widget) 649 650panel.present() 651panel.realize() 652 653def do_activate(self): 654Gio.Application.do_activate(self) 655 656def save_config(self, *args): 657print("Saving configuration file") 658with open(get_config_file(), "w") as config_file: 659yaml_writer = yaml.YAML(typ="rt") 660data = {"panels": []} 661for panel in self.panels: 662panel_data = { 663"position": PANEL_POSITIONS_REVERSE[panel.position], 664"monitor": panel.monitor_index, 665"size": panel.size, 666"autohide": panel.autohide, 667"hide_time": panel.hide_time, 668"can_capture_keyboard": panel.can_capture_keyboard, 669"applets": {} 670} 671 672for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)): 673panel_data["applets"][area_name] = [] 674applet = area.get_first_child() 675while applet is not None: 676panel_data["applets"][area_name].append({ 677applet.__class__.__name__: applet.get_config(), 678}) 679 680applet = applet.get_next_sibling() 681 682data["panels"].append(panel_data) 683 684yaml_writer.dump(data, config_file) 685 686def do_shutdown(self): 687print("Shutting down") 688for panel in self.panels: 689for area in (panel.left_area, panel.centre_area, panel.right_area): 690applet = area.get_first_child() 691while applet is not None: 692applet.shutdown() 693 694applet = applet.get_next_sibling() 695Gtk.Application.do_shutdown(self) 696 697def do_command_line(self, command_line: Gio.ApplicationCommandLine): 698options = command_line.get_options_dict() 699args = command_line.get_arguments()[1:] 700 701trigger_variant = options.lookup_value("trigger", GLib.VariantType("s")) 702if trigger_variant: 703action_name = trigger_variant.get_string() 704print(app.list_actions()) 705self.activate_action(action_name, None) 706print(f"Triggered {action_name}") 707 708return 0 709 710def set_edit_mode(self, value): 711self.edit_mode = value 712for panel in self.panels: 713panel.set_edit_mode(value) 714 715def reset_manager_window(self, *args): 716self.manager_window = None 717 718 719if __name__ == "__main__": 720app = PanoramaPanel() 721app.run(sys.argv) 722