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