__init__.py
Python script, ASCII text executable
1import dataclasses 2import os 3from pathlib import Path 4from pywayland.client import Display 5from pywayland.protocol.wayland import WlRegistry 6from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import ( 7ZwlrForeignToplevelManagerV1, 8ZwlrForeignToplevelHandleV1 9) 10import panorama_panel 11 12import gi 13 14gi.require_version("Gtk", "4.0") 15 16from gi.repository import Gtk, GLib, Gio, Gdk 17 18 19module_directory = Path(__file__).resolve().parent 20 21 22def split_bytes_into_ints(array: bytes, size: int = 4) -> list[int]: 23if len(array) % size: 24raise ValueError(f"The byte string's length must be a multiple of {size}") 25 26values: list[int] = [] 27for i in range(0, len(array), size): 28values.append(int.from_bytes(array[i : i+size], byteorder="little")) 29 30return values 31 32 33@dataclasses.dataclass 34class WindowState: 35minimised: bool 36maximised: bool 37fullscreen: bool 38focused: bool 39 40@classmethod 41def from_state_array(cls, array: bytes): 42values = split_bytes_into_ints(array) 43instance = cls(False, False, False, False) 44for value in values: 45match value: 46case 0: 47instance.maximised = True 48case 1: 49instance.minimised = True 50case 2: 51instance.focused = True 52case 3: 53instance.fullscreen = True 54 55return instance 56 57 58class WindowButton(Gtk.ToggleButton): 59def __init__(self, window_id, window_title, **kwargs): 60super().__init__(**kwargs) 61 62self.window_id = window_id 63self.set_has_frame(False) 64self.label = Gtk.Label() 65self.icon = Gtk.Image.new_from_icon_name("application-x-executable") 66box = Gtk.Box() 67box.append(self.icon) 68box.append(self.label) 69self.set_child(box) 70 71self.window_title = window_title 72 73self.last_state = False 74 75@property 76def window_title(self): 77return self.label.get_text() 78 79@window_title.setter 80def window_title(self, value): 81self.label.set_text(value) 82 83def set_icon_from_app_id(self, app_id): 84# Try getting an icon from the correct theme 85app_ids = app_id.split() 86icon_theme = Gtk.IconTheme.get_for_display(self.get_display()) 87 88for app_id in app_ids: 89if icon_theme.has_icon(app_id): 90self.icon.set_from_icon_name(app_id) 91return 92 93# If that doesn't work, try getting one from .desktop files 94for app_id in app_ids: 95try: 96desktop_file = Gio.DesktopAppInfo.new(app_id + ".desktop") 97if desktop_file: 98self.icon.set_from_gicon(desktop_file.get_icon()) 99return 100except TypeError: 101# Due to a bug, the constructor may sometimes return C NULL 102pass 103 104 105class WFWindowList(panorama_panel.Applet): 106name = "Wayfire window list" 107description = "Traditional window list (for Wayfire)" 108 109def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 110super().__init__(orientation=orientation, config=config) 111if config is None: 112config = {} 113 114self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {} 115# This button doesn't belong to any window but is used for the button group and to be 116# selected when no window is focused 117self.initial_button = Gtk.ToggleButton() 118 119self.display = Display() 120self.display.connect() 121self.registry = self.display.get_registry() 122self.registry.dispatcher["global"] = self.on_global 123self.display.roundtrip() 124fd = self.display.get_fd() 125GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event) 126 127self.context_menu = self.make_context_menu() 128panorama_panel.track_popover(self.context_menu) 129 130right_click_controller = Gtk.GestureClick() 131right_click_controller.set_button(3) 132right_click_controller.connect("pressed", self.show_context_menu) 133 134self.add_controller(right_click_controller) 135 136action_group = Gio.SimpleActionGroup() 137options_action = Gio.SimpleAction.new("options", None) 138options_action.connect("activate", self.show_options) 139action_group.add_action(options_action) 140self.insert_action_group("applet", action_group) 141 142self.options_window = None 143 144def on_display_event(self, source, condition): 145if condition == GLib.IO_IN: 146self.display.dispatch(block=True) 147return True 148 149def on_global(self, registry, name, interface, version): 150if interface == "zwlr_foreign_toplevel_manager_v1": 151self.print_log("Interface registered") 152self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version) 153self.manager.dispatcher["toplevel"] = self.on_new_toplevel 154self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished") 155self.display.roundtrip() 156self.display.flush() 157 158def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1, 159handle: ZwlrForeignToplevelHandleV1): 160handle.dispatcher["title"] = lambda h, title: self.on_title_changed(h, title) 161handle.dispatcher["app_id"] = lambda h, app_id: self.on_app_id_changed(h, app_id) 162handle.dispatcher["state"] = lambda h, states: self.on_state_changed(h, states) 163handle.dispatcher["closed"] = lambda h: self.on_closed(h) 164 165def on_title_changed(self, handle, title): 166if handle not in self.toplevel_buttons: 167button = WindowButton(handle, title) 168button.set_group(self.initial_button) 169button.connect("clicked", lambda *a: self.on_button_click(handle)) 170self.toplevel_buttons[handle] = button 171self.append(button) 172else: 173button = self.toplevel_buttons[handle] 174button.window_title = title 175 176def on_state_changed(self, handle, states): 177if handle in self.toplevel_buttons: 178state_info = WindowState.from_state_array(states) 179button = self.toplevel_buttons[handle] 180if state_info.focused: 181button.set_active(True) 182else: 183self.initial_button.set_active(True) 184 185def on_app_id_changed(self, handle, app_id): 186if handle in self.toplevel_buttons: 187button = self.toplevel_buttons[handle] 188button.set_icon_from_app_id(app_id) 189 190def on_closed(self, handle): 191if handle in self.toplevel_buttons: 192self.remove(self.toplevel_buttons[handle]) 193self.toplevel_buttons.pop(handle) 194 195def make_context_menu(self): 196menu = Gio.Menu() 197menu.append("Window list _options", "applet.options") 198context_menu = Gtk.PopoverMenu.new_from_model(menu) 199context_menu.set_has_arrow(False) 200context_menu.set_parent(self) 201context_menu.set_halign(Gtk.Align.START) 202context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 203return context_menu 204 205def show_context_menu(self, gesture, n_presses, x, y): 206rect = Gdk.Rectangle() 207rect.x = int(x) 208rect.y = int(y) 209rect.width = 1 210rect.height = 1 211 212self.context_menu.set_pointing_to(rect) 213self.context_menu.popup() 214 215def show_options(self, _0=None, _1=None): 216pass 217 218def get_config(self): 219return {} 220