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