__init__.py
Python script, ASCII text executable
1""" 2Traditional window list applet for the Panorama panel, compatible 3with wlroots-based compositors. 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 20import dataclasses 21import os 22import sys 23import mmap 24import locale 25import typing 26from pathlib import Path 27from pywayland.utils import AnonymousFile 28from pywayland.client import Display, EventQueue 29from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput, WlBuffer, WlShm, WlShmPool 30from pywayland.protocol.wayland.wl_output import WlOutputProxy 31from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import ( 32ZwlrForeignToplevelManagerV1, 33ZwlrForeignToplevelHandleV1 34) 35from pywayland.protocol.wlr_screencopy_unstable_v1 import ( 36ZwlrScreencopyManagerV1, 37ZwlrScreencopyFrameV1 38) 39from OpenGL import GL 40import panorama_panel 41 42import gi 43 44gi.require_version("Gtk", "4.0") 45gi.require_version("GdkWayland", "4.0") 46 47from gi.repository import Gtk, GLib, Gtk4LayerShell, Gio, Gdk, Pango, GObject 48 49 50module_directory = Path(__file__).resolve().parent 51 52locale.bindtextdomain("panorama-window-list", module_directory / "locale") 53_ = lambda x: locale.dgettext("panorama-window-list", x) 54 55 56import ctypes 57from cffi import FFI 58ffi = FFI() 59ffi.cdef(""" 60void * gdk_wayland_display_get_wl_display (void * display); 61void * gdk_wayland_surface_get_wl_surface (void * surface); 62void * gdk_wayland_monitor_get_wl_output (void * monitor); 63""") 64gtk = ffi.dlopen("libgtk-4.so.1") 65 66 67@Gtk.Template(filename=str(module_directory / "panorama-window-list-options.ui")) 68class WindowListOptions(Gtk.Window): 69__gtype_name__ = "WindowListOptions" 70button_width_adjustment: Gtk.Adjustment = Gtk.Template.Child() 71workspace_filter_checkbutton: Gtk.CheckButton = Gtk.Template.Child() 72 73def __init__(self, **kwargs): 74super().__init__(**kwargs) 75 76self.connect("close-request", lambda *args: self.destroy()) 77 78 79def split_bytes_into_ints(array: bytes, size: int = 4) -> list[int]: 80if len(array) % size: 81raise ValueError(f"The byte string's length must be a multiple of {size}") 82 83values: list[int] = [] 84for i in range(0, len(array), size): 85values.append(int.from_bytes(array[i : i+size], byteorder=sys.byteorder)) 86 87return values 88 89 90def get_widget_rect(widget: Gtk.Widget) -> tuple[int, int, int, int]: 91width = int(widget.get_width()) 92height = int(widget.get_height()) 93 94toplevel = widget.get_root() 95if not toplevel: 96return None, None, width, height 97 98x, y = widget.translate_coordinates(toplevel, 0, 0) 99x = int(x) 100y = int(y) 101 102return x, y, width, height 103 104 105@dataclasses.dataclass 106class WindowState: 107minimised: bool 108maximised: bool 109fullscreen: bool 110focused: bool 111 112@classmethod 113def from_state_array(cls, array: bytes): 114values = split_bytes_into_ints(array) 115instance = cls(False, False, False, False) 116for value in values: 117match value: 118case 0: 119instance.maximised = True 120case 1: 121instance.minimised = True 122case 2: 123instance.focused = True 124case 3: 125instance.fullscreen = True 126 127return instance 128 129 130from gi.repository import Gtk, Gdk 131 132class WindowButtonOptions: 133def __init__(self, max_width: int): 134self.max_width = max_width 135 136class WindowButtonLayoutManager(Gtk.LayoutManager): 137def __init__(self, options: WindowButtonOptions, **kwargs): 138super().__init__(**kwargs) 139self.options = options 140 141def do_measure(self, widget, orientation, for_size): 142child = widget.get_first_child() 143if child is None: 144return 0, 0, 0, 0 145 146if orientation == Gtk.Orientation.HORIZONTAL: 147min_width, nat_width, min_height, nat_height = child.measure(Gtk.Orientation.HORIZONTAL, for_size) 148width = self.options.max_width 149return min_width, width, min_height, nat_height 150else: 151min_width, nat_width, min_height, nat_height = child.measure(Gtk.Orientation.VERTICAL, for_size) 152return min_height, nat_height, 0, 0 153 154def do_allocate(self, widget, width, height, baseline): 155child = widget.get_first_child() 156if child is None: 157return 158alloc_width = min(width, self.options.max_width) 159alloc = Gdk.Rectangle() 160alloc.x = 0 161alloc.y = 0 162alloc.width = alloc_width 163alloc.height = height 164child.allocate(alloc.width, alloc.height, baseline) 165 166 167class WindowButton(Gtk.ToggleButton): 168def __init__(self, window_id, window_title, window_list, **kwargs): 169super().__init__(**kwargs) 170 171self.window_id: ZwlrForeignToplevelHandleV1 = window_id 172self.wf_sock = window_list.wf_socket 173self.window_list = window_list 174self.wf_ipc_id: typing.Optional[int] = None 175self.set_has_frame(False) 176self.label = Gtk.Label() 177self.icon = Gtk.Image.new_from_icon_name("application-x-executable") 178box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) 179box.append(self.icon) 180box.append(self.label) 181self.set_child(box) 182 183self.window_title = window_title 184self.window_state = WindowState(False, False, False, False) 185 186self.label.set_ellipsize(Pango.EllipsizeMode.END) 187self.set_hexpand(True) 188self.set_vexpand(True) 189 190self.drag_source = Gtk.DragSource(actions=Gdk.DragAction.MOVE) 191self.drag_source.connect("prepare", self.provide_drag_data) 192self.drag_source.connect("drag-begin", self.drag_begin) 193self.drag_source.connect("drag-cancel", self.drag_cancel) 194 195self.add_controller(self.drag_source) 196 197self.menu = Gio.Menu() 198# TODO: toggle the labels when needed 199self.minimise_item = Gio.MenuItem.new(_("_Minimise"), "button.minimise") 200self.maximise_item = Gio.MenuItem.new(_("Ma_ximise"), "button.maximise") 201self.menu.append_item(self.minimise_item) 202self.menu.append_item(self.maximise_item) 203self.menu.append(_("_Close"), "button.close") 204self.menu.append(_("Window list _options"), "applet.options") 205self.popover_menu = Gtk.PopoverMenu.new_from_model(self.menu) 206self.popover_menu.set_parent(self) 207self.popover_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 208self.popover_menu.set_has_arrow(False) 209self.popover_menu.set_halign(Gtk.Align.END) 210 211self.right_click_controller = Gtk.GestureClick(button=3) 212self.right_click_controller.connect("pressed", self.show_menu) 213self.add_controller(self.right_click_controller) 214 215self.action_group = Gio.SimpleActionGroup() 216close_action = Gio.SimpleAction.new("close") 217close_action.connect("activate", self.close_associated) 218self.action_group.insert(close_action) 219minimise_action = Gio.SimpleAction.new("minimise") 220minimise_action.connect("activate", self.minimise_associated) 221self.action_group.insert(minimise_action) 222maximise_action = Gio.SimpleAction.new("maximise") 223maximise_action.connect("activate", self.maximise_associated) 224self.action_group.insert(maximise_action) 225 226self.insert_action_group("button", self.action_group) 227 228self.middle_click_controller = Gtk.GestureClick(button=2) 229self.middle_click_controller.connect("released", self.close_associated) 230self.add_controller(self.middle_click_controller) 231 232if not self.window_list.live_preview_tooltips: 233return 234 235self.custom_tooltip_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 236self.custom_tooltip_content.append(TooltipMedia(window_list)) 237self.hover_point = 0, 0 238self.hover_timeout = None 239self.connect("query-tooltip", self.query_tooltip) 240self.set_has_tooltip(True) 241motion_controller = Gtk.EventControllerMotion() 242motion_controller.connect("enter", self.on_button_enter) 243motion_controller.connect("leave", self.on_button_leave) 244self.add_controller(motion_controller) 245 246def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): 247tooltip.set_custom(self.custom_tooltip_content) 248return True 249 250def on_button_enter(self, button, x, y): 251view_id = self.wf_ipc_id 252self.window_list.set_live_preview_output_name("live-preview-" + str(view_id)) 253message = self.window_list.get_msg_template("live_previews/request_stream") 254message["data"]["id"] = view_id 255self.wf_sock.send_json(message) 256 257def on_button_leave(self, button): 258message = self.window_list.get_msg_template("live_previews/release_output") 259self.wf_sock.send_json(message) 260 261def show_menu(self, gesture, n_presses, x, y): 262rect = Gdk.Rectangle() 263rect.x = int(x) 264rect.y = int(y) 265rect.width = 1 266rect.height = 1 267self.popover_menu.popup() 268 269def close_associated(self, *args): 270self.window_id.close() 271 272def minimise_associated(self, action, *args): 273if self.window_state.minimised: 274self.window_id.unset_minimized() 275else: 276self.window_id.set_minimized() 277 278def maximise_associated(self, action, *args): 279if self.window_state.maximised: 280self.window_id.unset_maximized() 281else: 282self.window_id.set_maximized() 283 284def provide_drag_data(self, source: Gtk.DragSource, x: float, y: float): 285app = self.get_root().get_application() 286app.drags[id(self)] = self 287value = GObject.Value() 288value.init(GObject.TYPE_UINT64) 289value.set_uint64(id(self)) 290return Gdk.ContentProvider.new_for_value(value) 291 292def drag_begin(self, source: Gtk.DragSource, drag: Gdk.Drag): 293paintable = Gtk.WidgetPaintable.new(self).get_current_image() 294source.set_icon(paintable, 0, 0) 295self.hide() 296 297def drag_cancel(self, source: Gtk.DragSource, drag: Gdk.Drag, reason: Gdk.DragCancelReason): 298self.show() 299return False 300 301@property 302def window_title(self): 303return self.label.get_text() 304 305@window_title.setter 306def window_title(self, value): 307self.label.set_text(value) 308 309def set_icon_from_app_id(self, app_id): 310app_ids = app_id.split() 311 312# If on Wayfire, find the IPC ID 313for app_id in app_ids: 314if app_id.startswith("wf-ipc-"): 315self.wf_ipc_id = int(app_id.removeprefix("wf-ipc-")) 316break 317 318# Try getting an icon from the correct theme 319icon_theme = Gtk.IconTheme.get_for_display(self.get_display()) 320 321for app_id in app_ids: 322if icon_theme.has_icon(app_id): 323self.icon.set_from_icon_name(app_id) 324return 325 326# If that doesn't work, try getting one from .desktop files 327for app_id in app_ids: 328try: 329desktop_file = Gio.DesktopAppInfo.new(app_id + ".desktop") 330if desktop_file: 331self.icon.set_from_gicon(desktop_file.get_icon()) 332return 333except TypeError: 334# Due to a bug, the constructor may sometimes return C NULL 335pass 336 337class TooltipMedia(Gtk.GLArea): 338def __init__(self, window_list): 339super().__init__() 340self.frame: ZwlrScreencopyFrameV1 = None 341self.screencopy_manager = window_list.screencopy_manager 342self.wl_shm = window_list.wl_shm 343self.window_list = window_list 344self.set_auto_render(False) 345self.connect("render", self.on_render) 346 347self.buffer_width = 200 348self.buffer_height = 200 349 350self.screencopy_data = None 351self.wl_buffer = None 352self.size = 0 353 354self.add_tick_callback(self.on_tick) 355 356self.vertices = [ 3571.0, 1.0, 358-1.0, 1.0, 359-1.0, -1.0, 3601.0, -1.0, 361] 362self.uv_coords = [ 3631.0, 0.0, 3640.0, 0.0, 3650.0, 1.0, 3661.0, 1.0, 367] 368vertex_shader_source = """ 369precision highp float; 370attribute vec2 in_position; 371attribute vec2 uvpos; 372varying vec2 uv; 373 374void main() { 375uv = uvpos; 376gl_Position = vec4(in_position, 0.0, 1.0); 377} 378""" 379 380fragment_shader_source = """ 381precision highp float; 382uniform sampler2D texture; 383 384varying vec2 uv; 385 386void main() { 387gl_FragColor = texture2D(texture, uv); 388} 389""" 390vertex_shader = GL.glCreateShader(GL.GL_VERTEX_SHADER) 391fragment_shader = GL.glCreateShader(GL.GL_FRAGMENT_SHADER) 392GL.glShaderSource(vertex_shader, vertex_shader_source) 393GL.glShaderSource(fragment_shader, fragment_shader_source) 394GL.glCompileShader(vertex_shader) 395GL.glCompileShader(fragment_shader) 396self.shader_program = GL.glCreateProgram() 397GL.glAttachShader(self.shader_program, vertex_shader) 398GL.glAttachShader(self.shader_program, fragment_shader) 399GL.glLinkProgram(self.shader_program) 400 401self.texture_id = GL.glGenTextures(1) 402GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture_id) 403GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR) 404GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR) 405self.request_next_frame() 406 407 408def frame_handle_buffer(self, frame, pixel_format, width, height, stride): 409size = width * height * int(stride / width) 410if self.size != size: 411self.set_size_request(width, height) 412self.size = size 413anon_file = AnonymousFile(self.size) 414anon_file.open() 415fd = anon_file.fd 416self.screencopy_data = mmap.mmap(fileno=fd, length=self.size, access=(mmap.ACCESS_READ | mmap.ACCESS_WRITE), offset=0) 417pool = self.wl_shm.create_pool(fd, self.size) 418self.wl_buffer = pool.create_buffer(0, width, height, stride, pixel_format) 419pool.destroy() 420anon_file.close() 421self.buffer_width = width 422self.buffer_height = height 423self.frame.copy(self.wl_buffer) 424 425def frame_ready(self, frame, tv_sec_hi, tv_sec_low, tv_nsec): 426self.screencopy_data.seek(0) 427GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture_id) 428GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, self.buffer_width, self.buffer_height, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, self.screencopy_data.read(self.size)) 429self.request_next_frame() 430 431def request_next_frame(self): 432if self.frame: 433self.frame.destroy() 434 435self.queue_render() 436 437wl_output = self.window_list.get_live_preview_output() 438 439if not wl_output: 440return 441 442self.frame = self.screencopy_manager.capture_output(1, wl_output) 443self.frame.dispatcher["buffer"] = self.frame_handle_buffer 444self.frame.dispatcher["ready"] = self.frame_ready 445 446def on_render(self, gl_area, gl_context): 447Gdk.GLContext.make_current(gl_context) 448GL.glClear(GL.GL_COLOR_BUFFER_BIT) 449GL.glUseProgram(self.shader_program) 450GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture_id) 451GL.glEnableVertexAttribArray(0); 452GL.glVertexAttribPointer(0, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, self.vertices); 453GL.glEnableVertexAttribArray(1); 454GL.glVertexAttribPointer(1, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, self.uv_coords); 455GL.glDrawArrays(GL.GL_TRIANGLE_FAN, 0, 4); 456GL.glDisableVertexAttribArray(0); 457GL.glDisableVertexAttribArray(1); 458GL.glBindTexture(GL.GL_TEXTURE_2D, 0) 459gl_area.queue_draw() 460 461def on_tick(self, widget, frame_clock): 462self.request_next_frame() 463return GLib.SOURCE_CONTINUE 464 465class WFWindowList(panorama_panel.Applet): 466name = _("Wayfire window list") 467description = _("Traditional window list (for Wayfire and other wlroots compositors)") 468 469def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 470super().__init__(orientation=orientation, config=config) 471if config is None: 472config = {} 473 474self.set_homogeneous(True) 475self.window_button_options = WindowButtonOptions(config.get("max_button_width", 256)) 476self.show_only_this_wf_workspace = config.get("show_only_this_wf_workspace", True) 477self.show_only_this_output = config.get("show_only_this_output", True) 478self.icon_size = config.get("icon_size", 24) 479 480self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {} 481self.toplevel_buttons_by_wf_id: dict[int, WindowButton] = {} 482# This button doesn't belong to any window but is used for the button group and to be 483# selected when no window is focused 484self.initial_button = Gtk.ToggleButton() 485 486self.display = None 487self.my_output = None 488self.wl_surface_ptr = None 489self.registry = None 490self.compositor = None 491self.seat = None 492 493self.context_menu = self.make_context_menu() 494panorama_panel.track_popover(self.context_menu) 495 496right_click_controller = Gtk.GestureClick() 497right_click_controller.set_button(3) 498right_click_controller.connect("pressed", self.show_context_menu) 499 500self.add_controller(right_click_controller) 501 502action_group = Gio.SimpleActionGroup() 503options_action = Gio.SimpleAction.new("options", None) 504options_action.connect("activate", self.show_options) 505action_group.add_action(options_action) 506self.insert_action_group("applet", action_group) 507# Wait for the widget to be in a layer-shell window before doing this 508self.connect("realize", self.get_wl_resources) 509 510self.options_window = None 511 512# Support button reordering 513self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE) 514self.drop_target.set_gtypes([GObject.TYPE_UINT64]) 515self.drop_target.connect("drop", self.drop_button) 516 517self.add_controller(self.drop_target) 518 519# Make a Wayfire socket for workspace handling 520self.live_preview_tooltips = False 521try: 522import wayfire 523from wayfire.core.template import get_msg_template 524self.wf_socket = wayfire.WayfireSocket() 525self.get_msg_template = get_msg_template 526if "live_previews/request_stream" in self.wf_socket.list_methods(): 527self.live_preview_tooltips = True 528self.wf_socket.watch(["view-workspace-changed", "wset-workspace-changed"]) 529fd = self.wf_socket.client.fileno() 530GLib.io_add_watch(GLib.IOChannel.unix_new(fd), GLib.IO_IN, self.on_wf_event, priority=GLib.PRIORITY_HIGH) 531except: 532# Wayfire raises Exception itself, so it cannot be narrowed down 533self.wf_socket = None 534 535def get_wf_output_by_name(self, name): 536if not self.wf_socket: 537return None 538for output in self.wf_socket.list_outputs(): 539if output["name"] == name: 540return output 541return None 542 543def on_wf_event(self, source, condition): 544if condition & GLib.IO_IN: 545try: 546message = self.wf_socket.read_next_event() 547event = message.get("event") 548match event: 549case "view-workspace-changed": 550view = message.get("view", {}) 551if view["output-name"] == self.get_root().monitor_name: 552output = self.get_wf_output_by_name(view["output-name"]) 553current_workspace = output["workspace"]["x"], output["workspace"]["y"] 554button = self.toplevel_buttons_by_wf_id[view["id"]] 555if not self.show_only_this_wf_workspace or (message["to"]["x"], message["to"]["y"]) == current_workspace: 556if button.get_parent() is None and button.output == self.my_output: 557self.append(button) 558else: 559if button.get_parent() is self: 560# Remove out-of-workspace window 561self.remove(button) 562case "wset-workspace-changed": 563output_name = self.get_root().monitor_name 564if message["wset-data"]["output-name"] == output_name: 565# It has changed on this monitor; refresh the window list 566self.filter_to_wf_workspace() 567 568except Exception as e: 569print("Error reading Wayfire event:", e) 570return True 571 572def drop_button(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float): 573button: WindowButton = self.get_root().get_application().drags.pop(value) 574if button.get_parent() is not self: 575# Prevent dropping a button from another window list 576return False 577 578self.remove(button) 579# Find the position where to insert the applet 580# Probably we could use the assumption that buttons are homogeneous here for efficiency 581child = self.get_first_child() 582while child: 583allocation = child.get_allocation() 584child_x, child_y = self.translate_coordinates(self, 0, 0) 585if self.get_orientation() == Gtk.Orientation.HORIZONTAL: 586midpoint = child_x + allocation.width / 2 587if x < midpoint: 588button.insert_before(self, child) 589break 590elif self.get_orientation() == Gtk.Orientation.VERTICAL: 591midpoint = child_y + allocation.height / 2 592if y < midpoint: 593button.insert_before(self, child) 594break 595child = child.get_next_sibling() 596else: 597self.append(button) 598button.show() 599 600self.set_all_rectangles() 601return True 602 603def filter_to_wf_workspace(self): 604if not self.show_only_this_wf_workspace: 605return 606 607output = self.get_wf_output_by_name(self.get_root().monitor_name) 608for wf_id, button in self.toplevel_buttons_by_wf_id.items(): 609view = self.wf_socket.get_view(wf_id) 610mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2 611mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2 612output_width = output["geometry"]["width"] 613output_height = output["geometry"]["height"] 614if 0 <= mid_x < output_width and 0 <= mid_y < output_height and button.output == self.my_output: 615# It is in this workspace; keep it 616if button.get_parent() is None: 617self.append(button) 618else: 619# Remove it from this window list 620if button.get_parent() is self: 621self.remove(button) 622 623def get_wl_resources(self, widget): 624ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p 625ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,) 626 627self.display = Display() 628wl_display_ptr = gtk.gdk_wayland_display_get_wl_display( 629ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.get_root().get_native().get_display().__gpointer__, None))) 630self.display._ptr = wl_display_ptr 631self.event_queue = EventQueue(self.display) 632 633# Intentionally commented: the display is already connected by GTK 634# self.display.connect() 635 636my_monitor = Gtk4LayerShell.get_monitor(self.get_root()) 637 638# Iterate through monitors and get their Wayland output (wl_output) 639# This is a hack to ensure output_enter/leave is called for toplevels 640for monitor in self.get_root().get_application().monitors: 641wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None))) 642if wl_output and not monitor.output_proxy: 643print("Create proxy") 644output_proxy = WlOutputProxy(wl_output, self.display) 645output_proxy.interface.registry[output_proxy._ptr] = output_proxy 646monitor.output_proxy = output_proxy 647 648if monitor == my_monitor: 649self.my_output = monitor.output_proxy 650# End hack 651 652self.output = None 653self.wl_shm = None 654self.manager = None 655self.screencopy_manager = None 656self.live_preview_output_name = None 657self.registry = self.display.get_registry() 658self.registry.dispatcher["global"] = self.on_global 659self.display.roundtrip() 660if self.manager is None: 661print("Could not load wf-window-list. Is foreign-toplevel protocol advertised by the compositor?") 662print("(Wayfire requires enabling foreign-toplevel plugin)") 663return 664if self.screencopy_manager is None: 665print("Protocol zwlr_screencopy_manager_v1 not found, live (minimized) window previews will not work!") 666self.manager.dispatcher["toplevel"] = self.on_new_toplevel 667self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished") 668self.display.roundtrip() 669fd = self.display.get_fd() 670GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event) 671 672if self.wf_socket is not None: 673self.filter_to_wf_workspace() 674 675def on_display_event(self, source, condition): 676if condition & GLib.IO_IN: 677self.display.dispatch(queue=self.event_queue) 678return True 679 680def set_live_preview_output(self, output): 681self.output = output 682 683def get_live_preview_output(self): 684return self.output 685 686def set_live_preview_output_name(self, name): 687self.live_preview_output_name = name 688 689def get_live_preview_output_name(self): 690return self.live_preview_output_name 691 692def wl_output_handle_name(self, output, name): 693if name != self.get_live_preview_output_name(): 694output.release() 695output.destroy() 696return 697self.set_live_preview_output(output) 698 699def on_global(self, registry, name, interface, version): 700if interface == "zwlr_foreign_toplevel_manager_v1": 701self.print_log("Interface registered") 702self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version) 703elif interface == "wl_output": 704if self.live_preview_output_name is None: 705return 706wl_output = registry.bind(name, WlOutput, version) 707wl_output.dispatcher["name"] = self.wl_output_handle_name 708elif interface == "wl_seat": 709self.print_log("Seat found") 710self.seat = registry.bind(name, WlSeat, version) 711elif interface == "wl_shm": 712self.wl_shm = registry.bind(name, WlShm, version) 713elif interface == "wl_compositor": 714self.compositor = registry.bind(name, WlCompositor, version) 715wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface( 716ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer( 717self.get_root().get_native().get_surface().__gpointer__, None))) 718self.wl_surface = WlSurface() 719self.wl_surface._ptr = wl_surface_ptr 720elif interface == "zwlr_screencopy_manager_v1": 721self.screencopy_manager = registry.bind(name, ZwlrScreencopyManagerV1, version) 722 723def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1, 724handle: ZwlrForeignToplevelHandleV1): 725handle.dispatcher["title"] = self.on_title_changed 726handle.dispatcher["app_id"] = self.on_app_id_changed 727handle.dispatcher["output_enter"] = self.on_output_entered 728handle.dispatcher["output_leave"] = self.on_output_left 729handle.dispatcher["state"] = self.on_state_changed 730handle.dispatcher["closed"] = self.on_closed 731self.display.roundtrip() 732 733def on_output_entered(self, handle, output): 734button = WindowButton(handle, handle.title, self) 735button.icon.set_pixel_size(self.icon_size) 736button.output = None 737 738if self.show_only_this_output and output != self.my_output: 739return 740 741button.output = output 742self.set_title(button, handle.title) 743button.set_group(self.initial_button) 744button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options)) 745button.connect("clicked", self.on_button_click) 746self.set_app_id(button, handle.app_id) 747self.toplevel_buttons[handle] = button 748self.append(button) 749self.set_all_rectangles() 750 751def on_output_left(self, handle, output): 752if handle in self.toplevel_buttons: 753button = self.toplevel_buttons[handle] 754if button.get_parent() == self: 755button.output = None 756self.remove(button) 757self.set_all_rectangles() 758 759def set_title(self, button, title): 760button.window_title = title 761if not self.live_preview_tooltips: 762button.set_tooltip_text(title) 763 764def on_title_changed(self, handle, title): 765handle.title = title 766if handle in self.toplevel_buttons: 767button = self.toplevel_buttons[handle] 768self.set_title(button, title) 769 770def set_all_rectangles(self): 771child = self.get_first_child() 772while child is not None: 773if isinstance(child, WindowButton): 774child.window_id.set_rectangle(self.wl_surface, *get_widget_rect(child)) 775 776child = child.get_next_sibling() 777 778def on_button_click(self, button: WindowButton): 779# Set a rectangle for animation 780button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button)) 781if button.window_state.minimised: 782button.window_id.activate(self.seat) 783else: 784if button.window_state.focused: 785button.window_id.set_minimized() 786else: 787button.window_id.activate(self.seat) 788 789def on_state_changed(self, handle, states): 790if handle in self.toplevel_buttons: 791state_info = WindowState.from_state_array(states) 792button = self.toplevel_buttons[handle] 793button.window_state = state_info 794if state_info.focused: 795button.set_active(True) 796else: 797self.initial_button.set_active(True) 798 799self.set_all_rectangles() 800 801def set_app_id(self, button, app_id): 802button.app_id = app_id 803button.set_icon_from_app_id(app_id) 804app_ids = app_id.split() 805for app_id in app_ids: 806if app_id.startswith("wf-ipc-"): 807self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button 808 809def on_app_id_changed(self, handle, app_id): 810handle.app_id = app_id 811if handle in self.toplevel_buttons: 812button = self.toplevel_buttons[handle] 813self.set_app_id(button, app_id) 814 815def on_closed(self, handle): 816if handle not in self.toplevel_buttons: 817return 818button: WindowButton = self.toplevel_buttons[handle] 819wf_id = button.wf_ipc_id 820if handle in self.toplevel_buttons: 821self.remove(self.toplevel_buttons[handle]) 822self.toplevel_buttons.pop(handle) 823if wf_id in self.toplevel_buttons_by_wf_id: 824self.toplevel_buttons_by_wf_id.pop(wf_id) 825 826self.set_all_rectangles() 827 828def make_context_menu(self): 829menu = Gio.Menu() 830menu.append(_("Window list _options"), "applet.options") 831context_menu = Gtk.PopoverMenu.new_from_model(menu) 832context_menu.set_has_arrow(False) 833context_menu.set_parent(self) 834context_menu.set_halign(Gtk.Align.START) 835context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 836return context_menu 837 838def show_context_menu(self, gesture, n_presses, x, y): 839rect = Gdk.Rectangle() 840rect.x = int(x) 841rect.y = int(y) 842rect.width = 1 843rect.height = 1 844 845self.context_menu.set_pointing_to(rect) 846self.context_menu.popup() 847 848def show_options(self, _0=None, _1=None): 849if self.options_window is None: 850self.options_window = WindowListOptions() 851self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width) 852self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options) 853self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace) 854self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter) 855 856def reset_window(*args): 857self.options_window = None 858 859self.options_window.connect("close-request", reset_window) 860self.options_window.present() 861 862def remove_workspace_filtering(self): 863for button in self.toplevel_buttons.values(): 864if button.get_parent() is None: 865self.append(button) 866 867def update_workspace_filter(self, checkbutton): 868self.show_only_this_wf_workspace = checkbutton.get_active() 869if checkbutton.get_active(): 870self.filter_to_wf_workspace() 871else: 872self.remove_workspace_filtering() 873 874def update_button_options(self, adjustment): 875self.window_button_options.max_width = adjustment.get_value() 876child: Gtk.Widget = self.get_first_child() 877while child: 878child.queue_allocate() 879child.queue_resize() 880child.queue_draw() 881child = child.get_next_sibling() 882 883self.emit("config-changed") 884 885def get_config(self): 886return { 887"max_button_width": self.window_button_options.max_width, 888"show_only_this_wf_workspace": self.show_only_this_wf_workspace, 889"show_only_this_output": self.show_only_this_output, 890} 891 892def output_changed(self): 893self.get_wl_resources() 894 895def make_draggable(self): 896for button in self.toplevel_buttons.values(): 897button.remove_controller(button.drag_source) 898button.set_sensitive(False) 899 900def restore_drag(self): 901for button in self.toplevel_buttons.values(): 902button.add_controller(button.drag_source) 903button.set_sensitive(True) 904