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