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