"""
Panorama panel: traditional desktop panel using Wayland layer-shell
protocol.
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public Licence as published by
the Free Software Foundation, either version 3 of the Licence, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public Licence for more details.

You should have received a copy of the GNU General Public Licence
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from __future__ import annotations

import os
import sys
import importlib
import time
import traceback
import faulthandler
import typing
import locale
from itertools import accumulate, chain
from pathlib import Path
import ruamel.yaml as yaml
from pywayland.client import Display
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor

faulthandler.enable()

os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0"

from ctypes import CDLL
CDLL('libgtk4-layer-shell.so')

import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gtk4LayerShell", "1.0")

from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio, GObject
from gi.events import GLibEventLoopPolicy

sys.path.insert(0, str((Path(__file__).parent / "shared").resolve()))

locale.bindtextdomain("panorama-panel", "locale")
locale.textdomain("panorama-panel")
locale.setlocale(locale.LC_ALL)
_ = locale.gettext

import panorama_panel

import asyncio
asyncio.set_event_loop_policy(GLibEventLoopPolicy())

custom_css = """
.panel-flash {
    animation: flash 333ms ease-in-out 0s 2;
}

@keyframes flash {
    0% {
        background-color: initial;
    }
    50% {
        background-color: #ffff0080;
    }
    0% {
        background-color: initial;
    }
}
"""

css_provider = Gtk.CssProvider()
css_provider.load_from_data(custom_css)
Gtk.StyleContext.add_provider_for_display(
    Gdk.Display.get_default(),
    css_provider,
    100
)


@Gtk.Template(filename="panel-configurator.ui")
class PanelConfigurator(Gtk.Frame):
    __gtype_name__ = "PanelConfigurator"

    panel_size_adjustment: Gtk.Adjustment = Gtk.Template.Child()
    monitor_number_adjustment: Gtk.Adjustment = Gtk.Template.Child()
    top_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    bottom_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    left_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    right_position_radio: Gtk.CheckButton = Gtk.Template.Child()
    autohide_switch: Gtk.Switch = Gtk.Template.Child()

    def __init__(self, panel: Panel, **kwargs):
        super().__init__(**kwargs)
        self.panel = panel
        self.panel_size_adjustment.set_value(panel.size)

        match self.panel.position:
            case Gtk.PositionType.TOP:
                self.top_position_radio.set_active(True)
            case Gtk.PositionType.BOTTOM:
                self.bottom_position_radio.set_active(True)
            case Gtk.PositionType.LEFT:
                self.left_position_radio.set_active(True)
            case Gtk.PositionType.RIGHT:
                self.right_position_radio.set_active(True)

        self.top_position_radio.panel_position_target = Gtk.PositionType.TOP
        self.bottom_position_radio.panel_position_target = Gtk.PositionType.BOTTOM
        self.left_position_radio.panel_position_target = Gtk.PositionType.LEFT
        self.right_position_radio.panel_position_target = Gtk.PositionType.RIGHT
        self.autohide_switch.set_active(self.panel.autohide)

    @Gtk.Template.Callback()
    def update_panel_size(self, adjustment: Gtk.Adjustment):
        if not self.get_root():
            return
        self.panel.set_size(int(adjustment.get_value()))

        self.panel.get_application().save_config()

    @Gtk.Template.Callback()
    def toggle_autohide(self, switch: Gtk.Switch, value: bool):
        if not self.get_root():
            return
        self.panel.set_autohide(value, self.panel.hide_time)

        self.panel.get_application().save_config()

    @Gtk.Template.Callback()
    def move_panel(self, button: Gtk.CheckButton):
        if not self.get_root():
            return
        if not button.get_active():
            return

        self.panel.set_position(button.panel_position_target)
        self.update_panel_size(self.panel_size_adjustment)

        # Make the applets aware of the changed orientation
        for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
            applet = area.get_first_child()
            while applet:
                applet.set_orientation(self.panel.get_orientation())
                applet.set_panel_position(self.panel.position)
                applet.queue_resize()
                applet = applet.get_next_sibling()

        self.panel.get_application().save_config()

    @Gtk.Template.Callback()
    def move_to_monitor(self, adjustment: Gtk.Adjustment):
        if not self.get_root():
            return
        app: PanoramaPanel = self.get_root().get_application()
        monitor = app.monitors[int(self.monitor_number_adjustment.get_value())]
        self.panel.unmap()
        self.panel.monitor_index = int(self.monitor_number_adjustment.get_value())
        Gtk4LayerShell.set_monitor(self.panel, monitor)
        self.panel.show()

        # Make the applets aware of the changed monitor
        for area in (self.panel.left_area, self.panel.centre_area, self.panel.right_area):
            applet = area.get_first_child()
            while applet:
                applet.output_changed()
                applet = applet.get_next_sibling()

        self.panel.get_application().save_config()


PANEL_POSITIONS_HUMAN = {
    Gtk.PositionType.TOP: "top",
    Gtk.PositionType.BOTTOM: "bottom",
    Gtk.PositionType.LEFT: "left",
    Gtk.PositionType.RIGHT: "right",
}


@Gtk.Template(filename="panel-manager.ui")
class PanelManager(Gtk.Window):
    __gtype_name__ = "PanelManager"

    panel_editing_switch: Gtk.Switch = Gtk.Template.Child()
    panel_stack: Gtk.Stack = Gtk.Template.Child()
    current_panel: typing.Optional[Panel] = None

    def __init__(self, application: Gtk.Application, **kwargs):
        super().__init__(application=application, **kwargs)

        self.connect("close-request", lambda *args: self.destroy())

        action_group = Gio.SimpleActionGroup()

        self.next_panel_action = Gio.SimpleAction(name="next-panel")
        action_group.add_action(self.next_panel_action)
        self.next_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_next_sibling()))

        self.previous_panel_action = Gio.SimpleAction(name="previous-panel")
        action_group.add_action(self.previous_panel_action)
        self.previous_panel_action.connect("activate", lambda *args: self.panel_stack.set_visible_child(self.panel_stack.get_visible_child().get_prev_sibling()))

        self.insert_action_group("win", action_group)
        if isinstance(self.get_application(), PanoramaPanel):
            self.panel_editing_switch.set_active(application.edit_mode)
        self.panel_editing_switch.connect("state-set", self.set_edit_mode)

        self.connect("close-request", lambda *args: self.unflash_old_panel())
        self.connect("close-request", lambda *args: self.destroy())

        if isinstance(self.get_application(), PanoramaPanel):
            app: PanoramaPanel = self.get_application()
            for panel in app.panels:
                configurator = PanelConfigurator(panel)
                self.panel_stack.add_child(configurator)
                configurator.monitor_number_adjustment.set_upper(len(app.monitors))

        self.panel_stack.set_visible_child(self.panel_stack.get_first_child())
        self.panel_stack.connect("notify::visible-child", self.set_visible_panel)
        self.panel_stack.notify("visible-child")

    def unflash_old_panel(self):
        if self.current_panel:
            for area in (self.current_panel.left_area, self.current_panel.centre_area, self.current_panel.right_area):
                area.unflash()

    def set_visible_panel(self, stack: Gtk.Stack, pspec: GObject.ParamSpec):
        self.unflash_old_panel()

        panel: Panel = stack.get_visible_child().panel

        self.current_panel = panel
        self.next_panel_action.set_enabled(stack.get_visible_child().get_next_sibling() is not None)
        self.previous_panel_action.set_enabled(stack.get_visible_child().get_prev_sibling() is not None)

        # Start an animation to show the user what panel is being edited
        for area in (panel.left_area, panel.centre_area, panel.right_area):
            area.flash()

    def set_edit_mode(self, switch, value):
        if isinstance(self.get_application(), PanoramaPanel):
            self.get_application().set_edit_mode(value)

    @Gtk.Template.Callback()
    def save_settings(self, *args):
        if isinstance(self.get_application(), PanoramaPanel):
            self.get_application().save_config()


def get_applet_directories():
    data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share"))
    data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")]

    all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs]
    return [d for d in all_paths if d.is_dir()]


def get_config_file():
    config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))

    return config_home / "panorama-panel" / "config.yaml"


class AppletArea(Gtk.Box):
    def __init__(self, orientation=Gtk.Orientation.HORIZONTAL):
        super().__init__()

        self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE)
        self.drop_target.set_gtypes([GObject.TYPE_UINT64])
        self.drop_target.connect("drop", self.drop_applet)

    def drop_applet(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
        applet = self.get_root().get_application().drags[value]
        old_area: AppletArea = applet.get_parent()
        old_area.remove(applet)
        # Find the position where to insert the applet
        child = self.get_first_child()
        while child:
            allocation = child.get_allocation()
            child_x, child_y = self.translate_coordinates(self, 0, 0)
            if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
                midpoint = child_x + allocation.width / 2
                if x < midpoint:
                    applet.insert_before(self, child)
                    break
            elif self.get_orientation() == Gtk.Orientation.VERTICAL:
                midpoint = child_y + allocation.height / 2
                if y < midpoint:
                    applet.insert_before(self, child)
                    break
            child = child.get_next_sibling()
        else:
            self.append(applet)
        applet.show()

        self.get_root().get_application().drags.pop(value)
        self.get_root().get_application().save_config()

        return True

    def set_edit_mode(self, value):
        panel: Panel = self.get_root()
        child = self.get_first_child()
        while child is not None:
            if value:
                child.make_draggable()
            else:
                child.restore_drag()
            child.set_opacity(0.75 if value else 1)
            child = child.get_next_sibling()

        if value:
            self.add_controller(self.drop_target)
            if panel.get_orientation() == Gtk.Orientation.HORIZONTAL:
                self.set_size_request(48, 0)
            elif panel.get_orientation() == Gtk.Orientation.VERTICAL:
                self.set_size_request(0, 48)
        else:
            self.remove_controller(self.drop_target)
            self.set_size_request(0, 0)

    def flash(self):
        self.add_css_class("panel-flash")

    def unflash(self):
        self.remove_css_class("panel-flash")


POSITION_TO_LAYER_SHELL_EDGE = {
    Gtk.PositionType.TOP: Gtk4LayerShell.Edge.TOP,
    Gtk.PositionType.BOTTOM: Gtk4LayerShell.Edge.BOTTOM,
    Gtk.PositionType.LEFT: Gtk4LayerShell.Edge.LEFT,
    Gtk.PositionType.RIGHT: Gtk4LayerShell.Edge.RIGHT,
}


class Panel(Gtk.Window):
    def __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):
        super().__init__(application=application)
        self.drop_motion_controller = None
        self.motion_controller = None
        self.set_decorated(False)
        self.position = None
        self.autohide = None
        self.monitor_name = monitor.get_connector()
        self.hide_time = None
        self.monitor_index = 0
        self.can_capture_keyboard = can_capture_keyboard
        self.open_popovers: set[int] = set()

        self.add_css_class("panorama-panel")

        Gtk4LayerShell.init_for_window(self)
        Gtk4LayerShell.set_namespace(self, "com.roundabout_host.panorama.panel")
        Gtk4LayerShell.set_monitor(self, monitor)

        Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP)
        if can_capture_keyboard:
            Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.ON_DEMAND)
        else:
            Gtk4LayerShell.set_keyboard_mode(self, Gtk4LayerShell.KeyboardMode.NONE)

        box = Gtk.CenterBox()

        self.left_area = AppletArea(orientation=box.get_orientation())
        self.centre_area = AppletArea(orientation=box.get_orientation())
        self.right_area = AppletArea(orientation=box.get_orientation())

        box.set_start_widget(self.left_area)
        box.set_center_widget(self.centre_area)
        box.set_end_widget(self.right_area)

        self.set_child(box)
        self.set_position(position)
        self.set_size(size)
        self.set_autohide(autohide, hide_time)

        # Add a context menu
        menu = Gio.Menu()

        menu.append(_("Open _manager"), "panel.manager")

        self.context_menu = Gtk.PopoverMenu.new_from_model(menu)
        self.context_menu.set_has_arrow(False)
        self.context_menu.set_parent(self)
        self.context_menu.set_halign(Gtk.Align.START)
        self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
        panorama_panel.track_popover(self.context_menu)

        right_click_controller = Gtk.GestureClick()
        right_click_controller.set_button(3)
        right_click_controller.connect("pressed", self.show_context_menu)

        self.add_controller(right_click_controller)

        action_group = Gio.SimpleActionGroup()
        manager_action = Gio.SimpleAction.new("manager", None)
        manager_action.connect("activate", self.show_manager)
        action_group.add_action(manager_action)
        self.insert_action_group("panel", action_group)

    def reset_margins(self):
        for edge in POSITION_TO_LAYER_SHELL_EDGE.values():
            Gtk4LayerShell.set_margin(self, edge, 0)

    def set_edit_mode(self, value):
        for area in (self.left_area, self.centre_area, self.right_area):
            area.set_edit_mode(value)

    def show_context_menu(self, gesture, n_presses, x, y):
        rect = Gdk.Rectangle()
        rect.x = int(x)
        rect.y = int(y)
        rect.width = 1
        rect.height = 1

        self.context_menu.set_pointing_to(rect)
        self.context_menu.popup()

    def slide_in(self):
        if not self.autohide:
            Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
            return False

        if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) >= 0:
            return False

        Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) + 1)
        return True

    def slide_out(self):
        if not self.autohide:
            Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 0)
            return False

        if Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) <= 1 - self.size:
            return False

        Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], Gtk4LayerShell.get_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position]) - 1)
        return True

    def show_manager(self, _0=None, _1=None):
        if self.get_application():
            if not self.get_application().manager_window:
                self.get_application().manager_window = PanelManager(self.get_application())
                self.get_application().manager_window.connect("close-request", self.get_application().reset_manager_window)
            self.get_application().manager_window.present()

    def get_orientation(self):
        box = self.get_first_child()
        return box.get_orientation()

    def set_size(self, value: int):
        self.size = int(value)
        if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
            self.set_size_request(800, self.size)
            self.set_default_size(800, self.size)
        else:
            self.set_size_request(self.size, 600)
            self.set_default_size(self.size, 600)

    def set_position(self, position: Gtk.PositionType):
        self.position = position
        self.reset_margins()
        match self.position:
            case Gtk.PositionType.TOP:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
            case Gtk.PositionType.BOTTOM:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
            case Gtk.PositionType.LEFT:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
            case Gtk.PositionType.RIGHT:
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, False)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True)
                Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.BOTTOM, True)
        box = self.get_first_child()
        match self.position:
            case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM:
                box.set_orientation(Gtk.Orientation.HORIZONTAL)
                for area in (self.left_area, self.centre_area, self.right_area):
                    area.set_orientation(Gtk.Orientation.HORIZONTAL)
            case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT:
                box.set_orientation(Gtk.Orientation.VERTICAL)
                for area in (self.left_area, self.centre_area, self.right_area):
                    area.set_orientation(Gtk.Orientation.VERTICAL)

        if self.autohide:
            if not self.open_popovers:
                GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out)

    def set_autohide(self, autohide: bool, hide_time: int):
        self.autohide = autohide
        self.hide_time = hide_time
        if not self.autohide:
            self.reset_margins()
            Gtk4LayerShell.auto_exclusive_zone_enable(self)
            if self.motion_controller is not None:
                self.remove_controller(self.motion_controller)
            if self.drop_motion_controller is not None:
                self.remove_controller(self.drop_motion_controller)
            self.motion_controller = None
            self.drop_motion_controller = None
            self.reset_margins()
        else:
            Gtk4LayerShell.set_exclusive_zone(self, 0)
            # Only leave 1px of the window as a "mouse sensor"
            Gtk4LayerShell.set_margin(self, POSITION_TO_LAYER_SHELL_EDGE[self.position], 1 - self.size)
            self.motion_controller = Gtk.EventControllerMotion()
            self.add_controller(self.motion_controller)
            self.drop_motion_controller = Gtk.DropControllerMotion()
            self.add_controller(self.drop_motion_controller)

            self.motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
            self.drop_motion_controller.connect("enter", lambda *args: GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_in))
            self.motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))
            self.drop_motion_controller.connect("leave", lambda *args: not self.open_popovers and GLib.timeout_add(self.hide_time // (self.size - 1), self.slide_out))

def get_all_subclasses(klass: type) -> list[type]:
    subclasses = []
    for subclass in klass.__subclasses__():
        subclasses.append(subclass)
        subclasses += get_all_subclasses(subclass)

    return subclasses

def load_packages_from_dir(dir_path: Path):
    loaded_modules = []

    for path in dir_path.iterdir():
        if path.name.startswith("_"):
            continue

        if path.is_dir() and (path / "__init__.py").exists():
            try:
                module_name = path.name
                spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py")
                module = importlib.util.module_from_spec(spec)
                spec.loader.exec_module(module)
                loaded_modules.append(module)
            except Exception as e:
                print(f"Failed to load applet module {path.name}: {e}", file=sys.stderr)
        else:
            continue

    return loaded_modules


PANEL_POSITIONS = {
    "top": Gtk.PositionType.TOP,
    "bottom": Gtk.PositionType.BOTTOM,
    "left": Gtk.PositionType.LEFT,
    "right": Gtk.PositionType.RIGHT,
}


PANEL_POSITIONS_REVERSE = {
    Gtk.PositionType.TOP: "top",
    Gtk.PositionType.BOTTOM: "bottom",
    Gtk.PositionType.LEFT: "left",
    Gtk.PositionType.RIGHT: "right",
}


class PanoramaPanel(Gtk.Application):
    def __init__(self):
        super().__init__(
                application_id="com.roundabout_host.roundabout.PanoramaPanel",
                flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE
        )
        self.display = Gdk.Display.get_default()
        self.monitors = self.display.get_monitors()
        for monitor in self.monitors:
            monitor.output_proxy = None
        self.applets_by_name: dict[str, panorama_panel.Applet] = {}
        self.panels: list[Panel] = []
        self.manager_window = None
        self.edit_mode = False
        self.drags = {}

        self.add_main_option(
            "trigger",
            ord("t"),
            GLib.OptionFlags.NONE,
            GLib.OptionArg.STRING,
            _("Trigger an action which can be processed by the applets"),
            _("NAME")
        )

    def do_startup(self):
        Gtk.Application.do_startup(self)
        for i, monitor in enumerate(self.monitors):
            geometry = monitor.get_geometry()
            print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}")

        all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories()))
        print("Applets:")
        subclasses = get_all_subclasses(panorama_panel.Applet)
        for subclass in subclasses:
            if subclass.__name__ in self.applets_by_name:
                print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr)
            self.applets_by_name[subclass.__name__] = subclass

        with open(get_config_file(), "r") as config_file:
            yaml_loader = yaml.YAML(typ="rt")
            yaml_file = yaml_loader.load(config_file)
            for panel_data in yaml_file["panels"]:
                position = PANEL_POSITIONS[panel_data["position"]]
                monitor_index = panel_data["monitor"]
                if monitor_index >= len(self.monitors):
                    continue
                monitor = self.monitors[monitor_index]
                size = panel_data["size"]
                autohide = panel_data["autohide"]
                hide_time = panel_data["hide_time"]
                can_capture_keyboard = panel_data["can_capture_keyboard"]

                panel = Panel(self, monitor, position, size, autohide, hide_time, can_capture_keyboard, monitor_index)
                self.panels.append(panel)

                print(f"{size}px panel on {position} edge of monitor {monitor_index}, autohide is {autohide} ({hide_time}ms)")

                for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
                    applet_list = panel_data["applets"].get(area_name)
                    if applet_list is None:
                        continue

                    for applet in applet_list:
                        item = list(applet.items())[0]
                        AppletClass = self.applets_by_name[item[0]]
                        options = item[1]
                        applet_widget = AppletClass(orientation=panel.get_orientation(), config=options)
                        applet_widget.connect("config-changed", self.save_config)
                        applet_widget.set_panel_position(panel.position)

                        area.append(applet_widget)

                panel.present()
                panel.realize()

    def do_activate(self):
        Gio.Application.do_activate(self)

    def save_config(self, *args):
        print("Saving configuration file")
        with open(get_config_file(), "w") as config_file:
            yaml_writer = yaml.YAML(typ="rt")
            data = {"panels": []}
            for panel in self.panels:
                panel_data = {
                    "position": PANEL_POSITIONS_REVERSE[panel.position],
                    "monitor": panel.monitor_index,
                    "size": panel.size,
                    "autohide": panel.autohide,
                    "hide_time": panel.hide_time,
                    "can_capture_keyboard": panel.can_capture_keyboard,
                    "applets": {}
                }

                for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)):
                    panel_data["applets"][area_name] = []
                    applet = area.get_first_child()
                    while applet is not None:
                        panel_data["applets"][area_name].append({
                            applet.__class__.__name__: applet.get_config(),
                        })

                        applet = applet.get_next_sibling()

                data["panels"].append(panel_data)

            yaml_writer.dump(data, config_file)

    def do_shutdown(self):
        print("Shutting down")
        for panel in self.panels:
            for area in (panel.left_area, panel.centre_area, panel.right_area):
                applet = area.get_first_child()
                while applet is not None:
                    applet.shutdown()

                    applet = applet.get_next_sibling()
        Gtk.Application.do_shutdown(self)

    def do_command_line(self, command_line: Gio.ApplicationCommandLine):
        options = command_line.get_options_dict()
        args = command_line.get_arguments()[1:]

        trigger_variant = options.lookup_value("trigger", GLib.VariantType("s"))
        if trigger_variant:
            action_name = trigger_variant.get_string()
            print(app.list_actions())
            self.activate_action(action_name, None)
            print(f"Triggered {action_name}")

        return 0

    def set_edit_mode(self, value):
        self.edit_mode = value
        for panel in self.panels:
            panel.set_edit_mode(value)

    def reset_manager_window(self, *args):
        self.manager_window = None


if __name__ == "__main__":
    app = PanoramaPanel()
    app.run(sys.argv)
