__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 = WindowState(False, False, False, False) 44print(values) 45for value in values: 46match value: 47case 0: 48instance.maximised = True 49case 1: 50instance.minimised = True 51case 2: 52instance.focused = True 53case 3: 54instance.fullscreen = True 55 56return instance 57 58 59class WindowButton(Gtk.ToggleButton): 60def __init__(self, window_id, window_title, **kwargs): 61super().__init__(**kwargs) 62 63self.window_id = window_id 64self.set_has_frame(False) 65self.label = Gtk.Label() 66box = Gtk.Box() 67box.append(self.label) 68self.set_child(box) 69 70self.window_title = window_title 71 72self.last_state = False 73 74@property 75def window_title(self): 76return self.label.get_text() 77 78@window_title.setter 79def window_title(self, value): 80self.label.set_text(value) 81 82 83class WFWindowList(panorama_panel.Applet): 84name = "Wayfire window list" 85description = "Traditional window list (for Wayfire)" 86 87def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 88super().__init__(orientation=orientation, config=config) 89if config is None: 90config = {} 91 92self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {} 93# This button doesn't belong to any window but is used for the button group and to be 94# selected when no window is focused 95self.initial_button = Gtk.ToggleButton() 96 97self.display = Display() 98self.display.connect() 99self.registry = self.display.get_registry() 100self.registry.dispatcher["global"] = self.on_global 101self.display.roundtrip() 102fd = self.display.get_fd() 103GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event) 104 105self.context_menu = self.make_context_menu() 106panorama_panel.track_popover(self.context_menu) 107 108right_click_controller = Gtk.GestureClick() 109right_click_controller.set_button(3) 110right_click_controller.connect("pressed", self.show_context_menu) 111 112self.add_controller(right_click_controller) 113 114action_group = Gio.SimpleActionGroup() 115options_action = Gio.SimpleAction.new("options", None) 116options_action.connect("activate", self.show_options) 117action_group.add_action(options_action) 118self.insert_action_group("applet", action_group) 119 120self.options_window = None 121 122def on_display_event(self, source, condition): 123if condition == GLib.IO_IN: 124self.display.dispatch(block=True) 125return True 126 127def on_global(self, registry, name, interface, version): 128print(f"Global: {interface} (v{version})") 129if interface == "zwlr_foreign_toplevel_manager_v1": 130print("Interface registered") 131self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version) 132self.manager.dispatcher["toplevel"] = self.on_new_toplevel 133self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished") 134self.display.roundtrip() 135self.display.flush() 136 137def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1, 138handle: ZwlrForeignToplevelHandleV1): 139print("Toplevel received") 140handle.dispatcher["title"] = lambda h, title: self.on_title_changed(h, title) 141#handle.dispatcher["app_id"] = lambda h, app_id: self.on_app_id_changed(h, app_id) 142handle.dispatcher["state"] = lambda h, states: self.on_state_changed(h, states) 143handle.dispatcher["closed"] = lambda h: self.on_closed(h) 144 145def on_title_changed(self, handle, title): 146print(f"Window title: {title}") 147if handle not in self.toplevel_buttons: 148button = WindowButton(handle, title) 149button.set_group(self.initial_button) 150button.connect("clicked", lambda *a: self.on_button_click(handle)) 151self.toplevel_buttons[handle] = button 152self.append(button) 153else: 154button = self.toplevel_buttons[handle] 155button.window_title = title 156 157def on_state_changed(self, handle, states): 158if handle in self.toplevel_buttons: 159state_info = WindowState.from_state_array(states) 160print(state_info) 161button = self.toplevel_buttons[handle] 162if state_info.focused: 163print("focused") 164button.set_active(True) 165else: 166self.initial_button.set_active(True) 167 168def on_closed(self, handle): 169if handle in self.toplevel_buttons: 170self.remove(self.toplevel_buttons[handle]) 171self.toplevel_buttons.pop(handle) 172 173def make_context_menu(self): 174menu = Gio.Menu() 175menu.append("Window list _options", "applet.options") 176context_menu = Gtk.PopoverMenu.new_from_model(menu) 177context_menu.set_has_arrow(False) 178context_menu.set_parent(self) 179context_menu.set_halign(Gtk.Align.START) 180context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 181return context_menu 182 183def show_context_menu(self, gesture, n_presses, x, y): 184rect = Gdk.Rectangle() 185rect.x = int(x) 186rect.y = int(y) 187rect.width = 1 188rect.height = 1 189 190self.context_menu.set_pointing_to(rect) 191self.context_menu.popup() 192 193def show_options(self, _0=None, _1=None): 194pass 195 196def get_config(self): 197return {} 198