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