main.py
Python script, ASCII text executable
1import os 2import sys 3import importlib 4from itertools import accumulate, chain 5from pathlib import Path 6import ruamel.yaml as yaml 7 8os.environ["GI_TYPELIB_PATH"] = "/usr/local/lib/x86_64-linux-gnu/girepository-1.0" 9 10from ctypes import CDLL 11CDLL('libgtk4-layer-shell.so') 12 13import gi 14gi.require_version("Gtk", "4.0") 15gi.require_version("Gtk4LayerShell", "1.0") 16 17from gi.repository import Gtk, GLib, Gtk4LayerShell, Gdk, Gio 18 19sys.path.insert(0, str((Path(__file__).parent / "shared").resolve())) 20 21import panorama_panel 22 23 24def get_applet_directories(): 25data_home = Path(os.getenv("XDG_DATA_HOME", Path.home() / ".local" / "share")) 26data_dirs = [Path(d) for d in os.getenv("XDG_DATA_DIRS", "/usr/local/share:/usr/share").split(":")] 27 28all_paths = [data_home / "panorama-panel" / "applets"] + [d / "panorama-panel" / "applets" for d in data_dirs] 29return [d for d in all_paths if d.is_dir()] 30 31 32def get_config_file(): 33config_home = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) 34 35return config_home / "panorama-panel" / "config.yaml" 36 37 38class ManagerWindow(Gtk.Window): 39def __init__(self): 40super().__init__() 41box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 42self.set_child(box) 43switch_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) 44self.edit_mode_switch = Gtk.Switch() 45switch_box.append(self.edit_mode_switch) 46edit_mode_label = Gtk.Label() 47edit_mode_label.set_text("Panel editing") 48edit_mode_label.set_mnemonic_widget(self.edit_mode_switch) 49switch_box.append(edit_mode_label) 50box.append(switch_box) 51 52self.edit_mode_switch.connect("state-set", self.set_edit_mode) 53 54self.set_title("Panel configuration") 55 56self.connect("close-request", self.on_destroy) 57 58def on_destroy(self, widget): 59self.destroy() 60if self.get_application(): 61self.get_application().manager_window = None 62 63def set_edit_mode(self, switch, value): 64print(f"Editing is {value}") 65 66if value: 67for panel in panels: 68panel.set_edit_mode(True) 69else: 70for panel in panels: 71panel.set_edit_mode(False) 72 73 74class AppletArea(Gtk.Box): 75def __init__(self, orientation=Gtk.Orientation.HORIZONTAL): 76super().__init__() 77 78def set_edit_mode(self, value): 79child = self.get_first_child() 80while child is not None: 81child.set_sensitive(not value) 82child.set_opacity(0.75 if value else 1) 83child = child.get_next_sibling() 84 85 86class Panel(Gtk.Window): 87def __init__(self, application: Gtk.Application, monitor: Gdk.Monitor, position: Gtk.PositionType = Gtk.PositionType.TOP, size: int = 40, monitor_index: int = 0): 88super().__init__(application=application) 89self.set_default_size(800, size) 90self.set_decorated(False) 91self.position = position 92self.monitor_index = monitor_index 93self.size = size 94 95Gtk4LayerShell.init_for_window(self) 96 97Gtk4LayerShell.set_layer(self, Gtk4LayerShell.Layer.TOP) 98 99Gtk4LayerShell.auto_exclusive_zone_enable(self) 100Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.TOP, True) 101Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.LEFT, True) 102Gtk4LayerShell.set_anchor(self, Gtk4LayerShell.Edge.RIGHT, True) 103 104box = Gtk.CenterBox() 105 106match position: 107case Gtk.PositionType.TOP | Gtk.PositionType.BOTTOM: 108box.set_orientation(Gtk.Orientation.HORIZONTAL) 109case Gtk.PositionType.LEFT | Gtk.PositionType.RIGHT: 110box.set_orientation(Gtk.Orientation.VERTICAL) 111 112self.set_child(box) 113 114self.left_area = AppletArea(orientation=box.get_orientation()) 115self.centre_area = AppletArea(orientation=box.get_orientation()) 116self.right_area = AppletArea(orientation=box.get_orientation()) 117 118box.set_start_widget(self.left_area) 119box.set_center_widget(self.centre_area) 120box.set_end_widget(self.right_area) 121 122# Add a context menu 123menu = Gio.Menu() 124 125menu.append("Open _manager", "panel.manager") 126 127self.context_menu = Gtk.PopoverMenu.new_from_model(menu) 128self.context_menu.set_has_arrow(False) 129self.context_menu.set_parent(self) 130self.context_menu.set_halign(Gtk.Align.START) 131self.context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 132 133right_click_controller = Gtk.GestureClick() 134right_click_controller.set_button(3) 135right_click_controller.connect("pressed", self.show_context_menu) 136 137self.add_controller(right_click_controller) 138 139action_group = Gio.SimpleActionGroup() 140manager_action = Gio.SimpleAction.new("manager", None) 141manager_action.connect("activate", self.show_manager) 142action_group.add_action(manager_action) 143self.insert_action_group("panel", action_group) 144 145def set_edit_mode(self, value): 146for area in (self.left_area, self.centre_area, self.right_area): 147area.set_edit_mode(value) 148 149def show_context_menu(self, gesture, n_presses, x, y): 150rect = Gdk.Rectangle() 151rect.x = int(x) 152rect.y = int(y) 153rect.width = 1 154rect.height = 1 155 156self.context_menu.set_pointing_to(rect) 157self.context_menu.popup() 158 159def show_manager(self, _0=None, _1=None): 160print("Showing manager") 161if self.get_application(): 162if not self.get_application().manager_window: 163self.get_application().manager_window = ManagerWindow() 164self.get_application().manager_window.set_application(self.get_application()) 165self.get_application().manager_window.present() 166 167def get_orientation(self): 168box = self.get_first_child() 169return box.get_orientation() 170 171 172def get_all_subclasses(klass: type) -> list[type]: 173subclasses = [] 174for subclass in klass.__subclasses__(): 175subclasses.append(subclass) 176subclasses += get_all_subclasses(subclass) 177 178return subclasses 179 180def load_packages_from_dir(dir_path: Path): 181loaded_modules = [] 182 183for path in dir_path.iterdir(): 184if path.name.startswith("_"): 185continue 186 187if path.is_dir() and (path / "__init__.py").exists(): 188module_name = path.name 189spec = importlib.util.spec_from_file_location(module_name, path / "__init__.py") 190module = importlib.util.module_from_spec(spec) 191spec.loader.exec_module(module) 192loaded_modules.append(module) 193else: 194continue 195 196return loaded_modules 197 198 199PANEL_POSITIONS = { 200"top": Gtk.PositionType.TOP, 201"bottom": Gtk.PositionType.BOTTOM, 202"left": Gtk.PositionType.LEFT, 203"right": Gtk.PositionType.RIGHT, 204} 205 206 207PANEL_POSITIONS_REVERSE = { 208Gtk.PositionType.TOP: "top", 209Gtk.PositionType.BOTTOM: "bottom", 210Gtk.PositionType.LEFT: "left", 211Gtk.PositionType.RIGHT: "right", 212} 213 214 215class PanoramaPanel(Gtk.Application): 216def __init__(self): 217super().__init__(application_id="com.roundabout_host.panorama.panel") 218self.display = Gdk.Display.get_default() 219self.monitors = self.display.get_monitors() 220self.applets_by_name = {} 221self.panels = [] 222self.manager_window = None 223 224def do_startup(self): 225Gtk.Application.do_startup(self) 226for i, monitor in enumerate(self.monitors): 227geometry = monitor.get_geometry() 228print(f"Monitor {i}: {geometry.width}x{geometry.height} at {geometry.x},{geometry.y}") 229 230all_applets = list(chain.from_iterable(load_packages_from_dir(d) for d in get_applet_directories())) 231print("Applets:") 232subclasses = get_all_subclasses(panorama_panel.Applet) 233for subclass in subclasses: 234if subclass.__name__ in self.applets_by_name: 235print(f"Name conflict for applet {subclass.__name__}. Only one will be loaded.", file=sys.stderr) 236self.applets_by_name[subclass.__name__] = subclass 237 238with open(get_config_file(), "r") as config_file: 239yaml_loader = yaml.YAML(typ="rt") 240yaml_file = yaml_loader.load(config_file) 241for panel_data in yaml_file["panels"]: 242position = PANEL_POSITIONS[panel_data["position"]] 243monitor_index = panel_data["monitor"] 244monitor = self.monitors[monitor_index] 245size = panel_data["size"] 246 247panel = Panel(self, monitor, position, size, monitor_index) 248self.panels.append(panel) 249panel.show() 250 251print(f"{size}px panel on {position} edge of monitor {monitor_index}") 252 253for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)): 254applet_list = panel_data["applets"].get(area_name) 255if applet_list is None: 256continue 257 258for applet in applet_list: 259item = list(applet.items())[0] 260AppletClass = self.applets_by_name[item[0]] 261options = item[1] 262applet_widget = AppletClass(orientation=panel.get_orientation(), config=options) 263 264area.append(applet_widget) 265 266def do_activate(self): 267Gio.Application.do_activate(self) 268 269def save_config(self): 270with open(get_config_file(), "w") as config_file: 271yaml_writer = yaml.YAML(typ="rt") 272data = {"panels": []} 273for panel in self.panels: 274panel_data = { 275"position": PANEL_POSITIONS_REVERSE[panel.position], 276"monitor": panel.monitor_index, 277"size": panel.size, 278"applets": {} 279} 280 281for area_name, area in (("left", panel.left_area), ("centre", panel.centre_area), ("right", panel.right_area)): 282panel_data["applets"][area_name] = [] 283applet = area.get_first_child() 284while applet is not None: 285panel_data["applets"][area_name].append({ 286applet.__class__.__name__: applet.get_config(), 287}) 288 289applet = applet.get_next_sibling() 290 291data["panels"].append(panel_data) 292 293yaml_writer.dump(data, config_file) 294 295def do_shutdown(self): 296print("Shutting down") 297Gtk.Application.do_shutdown(self) 298self.save_config() 299 300 301if __name__ == "__main__": 302app = PanoramaPanel() 303app.run(sys.argv) 304