__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, 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) 39 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 0, 0, 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, button: Gtk.ToggleButton, **kwargs): 138super().__init__(**kwargs) 139self.options = options 140self.button = button 141 142def do_measure(self, widget, orientation, for_size): 143child = widget.get_first_child() 144if child is None: 145return 0, 0, 0, 0 146 147if orientation == Gtk.Orientation.HORIZONTAL: 148min_width, nat_width, min_height, nat_height = child.measure(Gtk.Orientation.HORIZONTAL, for_size) 149width = self.options.max_width 150return min_width, width, min_height, nat_height 151else: 152min_width, nat_width, min_height, nat_height = child.measure(Gtk.Orientation.VERTICAL, for_size) 153return min_height, nat_height, 0, 0 154 155def do_allocate(self, widget, width, height, baseline): 156child = widget.get_first_child() 157if child is None: 158return 159alloc_width = min(width, self.options.max_width) 160alloc = Gdk.Rectangle() 161alloc.x = 0 162alloc.y = 0 163alloc.width = alloc_width 164alloc.height = height 165child.allocate(alloc.width, alloc.height, baseline) 166if alloc.width > 0 and alloc.height > 0 and self.button.window_list.wl_surface: 167x, y = widget.translate_coordinates(self.button.get_root(), 0, 0) 168x = int(x) 169y = int(y) 170self.button.window_id.set_rectangle(self.button.window_list.wl_surface, x, y, alloc.width, alloc.height) 171 172class WindowButton(Gtk.ToggleButton): 173def __init__(self, window_id, window_title, window_list, **kwargs): 174super().__init__(**kwargs) 175 176self.window_id: ZwlrForeignToplevelHandleV1 = window_id 177self.wf_sock = window_list.wf_socket 178self.window_list = window_list 179self.wf_ipc_id: typing.Optional[int] = None 180self.popover_open = False 181self.set_has_frame(False) 182self.label = Gtk.Label() 183self.icon = Gtk.Image.new_from_icon_name("application-x-executable") 184box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) 185box.append(self.icon) 186box.append(self.label) 187self.set_child(box) 188 189self.window_title = window_title 190self.window_state = WindowState(False, False, False, False) 191 192self.label.set_ellipsize(Pango.EllipsizeMode.END) 193self.set_hexpand(True) 194self.set_vexpand(True) 195 196self.drag_source = Gtk.DragSource(actions=Gdk.DragAction.MOVE) 197self.drag_source.connect("prepare", self.provide_drag_data) 198self.drag_source.connect("drag-begin", self.drag_begin) 199self.drag_source.connect("drag-cancel", self.drag_cancel) 200 201self.add_controller(self.drag_source) 202 203self.menu = Gio.Menu() 204# TODO: toggle the labels when needed 205self.minimise_item = Gio.MenuItem.new(_("_Minimise"), "button.minimise") 206self.maximise_item = Gio.MenuItem.new(_("Ma_ximise"), "button.maximise") 207self.menu.append_item(self.minimise_item) 208self.menu.append_item(self.maximise_item) 209self.menu.append(_("_Close"), "button.close") 210self.menu.append(_("Window list _options"), "applet.options") 211self.popover_menu = Gtk.PopoverMenu.new_from_model(self.menu) 212self.popover_menu.set_parent(self) 213self.popover_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 214self.popover_menu.set_has_arrow(False) 215self.popover_menu.set_halign(Gtk.Align.END) 216self.popover_menu.connect("closed", self.hide_menu) 217 218self.right_click_controller = Gtk.GestureClick(button=3) 219self.right_click_controller.connect("pressed", self.show_menu) 220self.add_controller(self.right_click_controller) 221 222self.action_group = Gio.SimpleActionGroup() 223close_action = Gio.SimpleAction.new("close") 224close_action.connect("activate", self.close_associated) 225self.action_group.insert(close_action) 226minimise_action = Gio.SimpleAction.new("minimise") 227minimise_action.connect("activate", self.minimise_associated) 228self.action_group.insert(minimise_action) 229maximise_action = Gio.SimpleAction.new("maximise") 230maximise_action.connect("activate", self.maximise_associated) 231self.action_group.insert(maximise_action) 232 233self.insert_action_group("button", self.action_group) 234 235self.middle_click_controller = Gtk.GestureClick(button=2) 236self.middle_click_controller.connect("released", self.close_associated) 237self.add_controller(self.middle_click_controller) 238 239if not self.window_list.live_preview_tooltips: 240return 241 242self.custom_tooltip_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) 243self.custom_tooltip_content.append(TooltipMedia(window_list)) 244self.hover_point = 0, 0 245self.hover_timeout = None 246self.connect("query-tooltip", self.query_tooltip) 247self.set_has_tooltip(True) 248motion_controller = Gtk.EventControllerMotion() 249motion_controller.connect("enter", self.on_button_enter) 250motion_controller.connect("leave", self.on_button_leave) 251self.add_controller(motion_controller) 252 253def query_tooltip(self, widget, x, y, keyboard_mode, tooltip): 254if self.window_list.get_live_preview_output() is None: 255return False 256tooltip.set_custom(self.custom_tooltip_content) 257return True 258 259def on_button_enter(self, button, x, y): 260if self.popover_open: 261return 262view_id = self.wf_ipc_id 263message = self.window_list.get_msg_template("live_previews/request_stream") 264message["data"]["id"] = view_id 265self.wf_sock.send_json(message) 266 267def on_button_leave(self, button): 268if self.popover_open: 269return 270message = self.window_list.get_msg_template("live_previews/release_output") 271self.wf_sock.send_json(message) 272self.window_list.set_live_preview_output(None) 273 274def show_menu(self, gesture, n_presses, x, y): 275if self.window_list.live_preview_tooltips: 276self.on_button_leave(None) 277self.popover_open = True 278self.popover_menu.popup() 279 280def hide_menu(self, popover): 281self.popover_open = False 282self.popover_menu.popdown() 283 284def close_associated(self, *args): 285self.window_id.close() 286 287def minimise_associated(self, action, *args): 288if self.window_state.minimised: 289self.window_id.unset_minimized() 290else: 291self.window_id.set_minimized() 292 293def maximise_associated(self, action, *args): 294if self.window_state.maximised: 295self.window_id.unset_maximized() 296else: 297self.window_id.set_maximized() 298 299def provide_drag_data(self, source: Gtk.DragSource, x: float, y: float): 300app = self.get_root().get_application() 301app.drags[id(self)] = self 302value = GObject.Value() 303value.init(GObject.TYPE_UINT64) 304value.set_uint64(id(self)) 305return Gdk.ContentProvider.new_for_value(value) 306 307def drag_begin(self, source: Gtk.DragSource, drag: Gdk.Drag): 308paintable = Gtk.WidgetPaintable.new(self).get_current_image() 309source.set_icon(paintable, 0, 0) 310self.hide() 311 312def drag_cancel(self, source: Gtk.DragSource, drag: Gdk.Drag, reason: Gdk.DragCancelReason): 313self.show() 314return False 315 316@property 317def window_title(self): 318return self.label.get_text() 319 320@window_title.setter 321def window_title(self, value): 322self.label.set_text(value) 323 324def set_icon_from_app_id(self, app_id): 325app_ids = app_id.split() 326 327# If on Wayfire, find the IPC ID 328for app_id in app_ids: 329if app_id.startswith("wf-ipc-"): 330self.wf_ipc_id = int(app_id.removeprefix("wf-ipc-")) 331break 332 333# Try getting an icon from the correct theme 334icon_theme = Gtk.IconTheme.get_for_display(self.get_display()) 335 336for app_id in app_ids: 337if icon_theme.has_icon(app_id): 338self.icon.set_from_icon_name(app_id) 339return 340 341# If that doesn't work, try getting one from .desktop files 342for app_id in app_ids: 343try: 344desktop_file = Gio.DesktopAppInfo.new(app_id + ".desktop") 345if desktop_file: 346self.icon.set_from_gicon(desktop_file.get_icon()) 347return 348except TypeError: 349# Due to a bug, the constructor may sometimes return C NULL 350pass 351 352class TooltipMedia(Gtk.Picture): 353def __init__(self, window_list): 354super().__init__() 355self.frame: ZwlrScreencopyFrameV1 = None 356self.screencopy_manager = window_list.screencopy_manager 357self.wl_shm = window_list.wl_shm 358self.window_list = window_list 359 360self.buffer_width = 200 361self.buffer_height = 200 362self.buffer_stride = 200 * 4 363 364self.screencopy_data = None 365self.wl_buffer = None 366self.size = 0 367 368self.add_tick_callback(self.on_tick) 369 370def frame_handle_buffer(self, frame, pixel_format, width, height, stride): 371size = width * height * int(stride / width) 372if self.size != size: 373self.set_size_request(width, height) 374self.size = size 375anon_file = AnonymousFile(self.size) 376anon_file.open() 377fd = anon_file.fd 378self.screencopy_data = mmap.mmap(fileno=fd, length=self.size, access=(mmap.ACCESS_READ | mmap.ACCESS_WRITE), offset=0) 379pool = self.wl_shm.create_pool(fd, self.size) 380self.wl_buffer = pool.create_buffer(0, width, height, stride, pixel_format) 381pool.destroy() 382anon_file.close() 383self.buffer_width = width 384self.buffer_height = height 385self.buffer_stride = stride 386self.frame.copy(self.wl_buffer) 387 388def frame_ready(self, frame, tv_sec_hi, tv_sec_low, tv_nsec): 389self.screencopy_data.seek(0) 390texture = Gdk.MemoryTexture.new( 391width=self.buffer_width, 392height=self.buffer_height, 393format=Gdk.MemoryFormat.R8G8B8A8, 394bytes=GLib.Bytes(self.screencopy_data.read(self.size)), 395stride=self.buffer_stride 396) 397self.set_paintable(texture) 398 399def request_next_frame(self): 400if self.frame: 401self.frame.destroy() 402 403wl_output = self.window_list.get_live_preview_output() 404 405if not wl_output: 406return 407 408self.frame = self.screencopy_manager.capture_output(1, wl_output) 409self.frame.dispatcher["buffer"] = self.frame_handle_buffer 410self.frame.dispatcher["ready"] = self.frame_ready 411 412def on_tick(self, widget, clock): 413self.request_next_frame() 414return GLib.SOURCE_CONTINUE 415 416class WFWindowList(panorama_panel.Applet): 417name = _("Wayfire window list") 418description = _("Traditional window list (for Wayfire and other wlroots compositors)") 419 420def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 421super().__init__(orientation=orientation, config=config) 422if config is None: 423config = {} 424 425self.set_homogeneous(True) 426self.window_button_options = WindowButtonOptions(config.get("max_button_width", 256)) 427self.show_only_this_wf_workspace = config.get("show_only_this_wf_workspace", True) 428self.show_only_this_output = config.get("show_only_this_output", True) 429self.icon_size = config.get("icon_size", 24) 430 431self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {} 432self.toplevel_buttons_by_wf_id: dict[int, WindowButton] = {} 433# This button doesn't belong to any window but is used for the button group and to be 434# selected when no window is focused 435self.initial_button = Gtk.ToggleButton() 436 437self.my_output = None 438self.wl_surface = None 439self.compositor = None 440self.wl_seat = None 441self.output = None 442self.wl_shm = None 443#self.manager = None 444self.screencopy_manager = None 445self.live_preview_output_name = None 446 447self.context_menu = self.make_context_menu() 448panorama_panel.track_popover(self.context_menu) 449 450right_click_controller = Gtk.GestureClick() 451right_click_controller.set_button(3) 452right_click_controller.connect("pressed", self.show_context_menu) 453 454self.add_controller(right_click_controller) 455 456action_group = Gio.SimpleActionGroup() 457options_action = Gio.SimpleAction.new("options", None) 458options_action.connect("activate", self.show_options) 459action_group.add_action(options_action) 460self.insert_action_group("applet", action_group) 461# Wait for the widget to be in a layer-shell window before doing this 462self.connect("realize", self.get_wl_resources) 463 464self.options_window = None 465 466# Support button reordering 467self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE) 468self.drop_target.set_gtypes([GObject.TYPE_UINT64]) 469self.drop_target.connect("drop", self.drop_button) 470 471self.add_controller(self.drop_target) 472 473# Make a Wayfire socket for workspace handling 474self.live_preview_tooltips = False 475try: 476import wayfire 477from wayfire.core.template import get_msg_template 478self.wf_socket = wayfire.WayfireSocket() 479self.get_msg_template = get_msg_template 480if "live_previews/request_stream" in self.wf_socket.list_methods(): 481self.live_preview_tooltips = True 482print("WFWindowList: Live Preview Tooltips: enabled") 483else: 484print("WFWindowList: Live Preview Tooltips: disabled") 485print("- Live preview tooltips requires wayfire wf-live-previews plugin. Is it enabled?") 486self.wf_socket.watch(["view-workspace-changed", "wset-workspace-changed"]) 487fd = self.wf_socket.client.fileno() 488GLib.io_add_watch(GLib.IOChannel.unix_new(fd), GLib.IO_IN, self.on_wf_event, priority=GLib.PRIORITY_HIGH) 489except: 490# Wayfire raises Exception itself, so it cannot be narrowed down 491self.wf_socket = None 492 493def get_wf_output_by_name(self, name): 494if not self.wf_socket: 495return None 496for output in self.wf_socket.list_outputs(): 497if output["name"] == name: 498return output 499return None 500 501def on_wf_event(self, source, condition): 502if condition & GLib.IO_IN: 503try: 504message = self.wf_socket.read_next_event() 505event = message.get("event") 506match event: 507case "view-workspace-changed": 508view = message.get("view", {}) 509if view["output-name"] == self.get_root().monitor_name: 510output = self.get_wf_output_by_name(view["output-name"]) 511current_workspace = output["workspace"]["x"], output["workspace"]["y"] 512button = self.toplevel_buttons_by_wf_id[view["id"]] 513if not self.show_only_this_wf_workspace or (message["to"]["x"], message["to"]["y"]) == current_workspace: 514if button.get_parent() is None and button.output == self.my_output: 515self.append(button) 516else: 517if button.get_parent() is self: 518# Remove out-of-workspace window 519self.remove(button) 520case "wset-workspace-changed": 521output_name = self.get_root().monitor_name 522if message["wset-data"]["output-name"] == output_name: 523# It has changed on this monitor; refresh the window list 524self.filter_to_wf_workspace() 525 526except Exception as e: 527print("Error reading Wayfire event:", e) 528return True 529 530def drop_button(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float): 531button: WindowButton = self.get_root().get_application().drags.pop(value) 532if button.get_parent() is not self: 533# Prevent dropping a button from another window list 534return False 535 536self.remove(button) 537# Find the position where to insert the applet 538# Probably we could use the assumption that buttons are homogeneous here for efficiency 539child = self.get_first_child() 540while child: 541allocation = child.get_allocation() 542child_x, child_y = self.translate_coordinates(self, 0, 0) 543if self.get_orientation() == Gtk.Orientation.HORIZONTAL: 544midpoint = child_x + allocation.width / 2 545if x < midpoint: 546button.insert_before(self, child) 547break 548elif self.get_orientation() == Gtk.Orientation.VERTICAL: 549midpoint = child_y + allocation.height / 2 550if y < midpoint: 551button.insert_before(self, child) 552break 553child = child.get_next_sibling() 554else: 555self.append(button) 556button.show() 557 558self.set_all_rectangles() 559return True 560 561def filter_to_wf_workspace(self): 562if not self.show_only_this_wf_workspace: 563return 564 565output = self.get_wf_output_by_name(self.get_root().monitor_name) 566if not output: 567return 568for wf_id, button in self.toplevel_buttons_by_wf_id.items(): 569view = self.wf_socket.get_view(wf_id) 570mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2 571mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2 572output_width = output["geometry"]["width"] 573output_height = output["geometry"]["height"] 574if 0 <= mid_x < output_width and 0 <= mid_y < output_height and button.output == self.my_output: 575# It is in this workspace; keep it 576if button.get_parent() is None: 577self.append(button) 578else: 579# Remove it from this window list 580if button.get_parent() is self: 581self.remove(button) 582 583def get_wl_resources(self, widget): 584if self.wf_socket is not None: 585self.filter_to_wf_workspace() 586 587if self.wl_shm: 588return 589 590self.my_output = None 591self.wl_seat = self.get_root().get_application().wl_seat 592self.output = None 593self.wl_shm = self.get_root().get_application().wl_shm 594self.screencopy_manager = self.get_root().get_application().screencopy_manager 595self.live_preview_output_name = None 596 597def set_live_preview_output(self, output): 598self.output = output 599 600def get_live_preview_output(self): 601return self.output 602 603def wl_output_enter(self, output, name): 604self.set_live_preview_output(None) 605if name.startswith("live-preview"): 606self.set_live_preview_output(output) 607return 608if name.startswith("live-preview"): 609return 610if not self.my_output and name == self.get_root().monitor_name: 611self.wl_surface = None 612if self.get_root().get_native().get_surface(): 613wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface( 614ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer( 615self.get_root().get_native().get_surface().__gpointer__, None))) 616self.wl_surface = WlSurface() 617self.wl_surface._ptr = wl_surface_ptr 618self.my_output = output 619else: 620toplevel_buttons = self.toplevel_buttons.copy() 621for handle in toplevel_buttons: 622button = self.toplevel_buttons[handle] 623if output != button.output: 624self.foreign_toplevel_closed(handle) 625 626def foreign_toplevel_output_enter(self, handle, output): 627if self.show_only_this_output and (not hasattr(output, "name") or output.name != self.get_root().monitor_name): 628return 629 630button = WindowButton(handle, handle.title, self) 631button.icon.set_pixel_size(self.icon_size) 632button.output = output 633self.set_title(button, handle.title) 634button.set_group(self.initial_button) 635button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options, button)) 636button.connect("clicked", self.on_button_click) 637self.set_app_id(button, handle.app_id) 638if handle.app_id.rsplit(maxsplit=1)[-1].startswith("wf-ipc-"): 639wf_output = self.get_wf_output_by_name(self.get_root().monitor_name) 640wf_id = int(handle.app_id.rsplit(maxsplit=1)[-1].removeprefix("wf-ipc-")) 641view = self.wf_socket.get_view(wf_id) 642mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2 643mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2 644output_width = wf_output["geometry"]["width"] 645output_height = wf_output["geometry"]["height"] 646if 0 <= mid_x < output_width and 0 <= mid_y < output_height and button.output == self.my_output: 647# It is in this workspace; keep it 648if button.get_parent() is None: 649self.toplevel_buttons[handle] = button 650self.append(button) 651else: 652self.toplevel_buttons[handle] = button 653self.append(button) 654 655def foreign_toplevel_output_leave(self, handle, output): 656if handle in self.toplevel_buttons: 657button = self.toplevel_buttons[handle] 658if button.get_parent() == self: 659button.output = None 660self.remove(button) 661self.set_all_rectangles() 662 663def set_title(self, button, title): 664button.window_title = title 665if not self.live_preview_tooltips: 666button.set_tooltip_text(title) 667 668def foreign_toplevel_title(self, handle, title): 669handle.title = title 670if handle in self.toplevel_buttons: 671button = self.toplevel_buttons[handle] 672self.set_title(button, title) 673 674def set_all_rectangles(self): 675child = self.get_first_child() 676while child is not None and self.wl_surface: 677if isinstance(child, WindowButton): 678x, y, w, h = get_widget_rect(child) 679if w > 0 and h > 0: 680child.window_id.set_rectangle(self.wl_surface, x, y, w, h) 681 682child = child.get_next_sibling() 683 684def on_button_click(self, button: WindowButton): 685# Set a rectangle for animation 686button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button)) 687if button.window_state.minimised: 688button.window_id.activate(self.wl_seat) 689else: 690if button.window_state.focused: 691button.window_id.set_minimized() 692else: 693button.window_id.activate(self.wl_seat) 694 695def foreign_toplevel_state(self, handle, states): 696if handle in self.toplevel_buttons: 697state_info = WindowState.from_state_array(states) 698button = self.toplevel_buttons[handle] 699button.window_state = state_info 700if state_info.focused: 701button.set_active(True) 702else: 703self.initial_button.set_active(True) 704 705def foreign_toplevel_refresh(self): 706for button in self.toplevel_buttons.values(): 707self.remove(button) 708 709self.toplevel_buttons.clear() 710self.toplevel_buttons_by_wf_id.clear() 711 712def set_app_id(self, button, app_id): 713button.app_id = app_id 714button.set_icon_from_app_id(app_id) 715app_ids = app_id.split() 716for app_id in app_ids: 717if app_id.startswith("wf-ipc-"): 718self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button 719 720def foreign_toplevel_app_id(self, handle, app_id): 721handle.app_id = app_id 722if handle in self.toplevel_buttons: 723button = self.toplevel_buttons[handle] 724self.set_app_id(button, app_id) 725 726def foreign_toplevel_closed(self, handle): 727if handle not in self.toplevel_buttons: 728return 729button: WindowButton = self.toplevel_buttons[handle] 730wf_id = button.wf_ipc_id 731if handle in self.toplevel_buttons: 732self.remove(self.toplevel_buttons[handle]) 733self.toplevel_buttons.pop(handle) 734if wf_id in self.toplevel_buttons_by_wf_id: 735self.toplevel_buttons_by_wf_id.pop(wf_id) 736 737self.set_all_rectangles() 738 739def make_context_menu(self): 740menu = Gio.Menu() 741menu.append(_("Window list _options"), "applet.options") 742context_menu = Gtk.PopoverMenu.new_from_model(menu) 743context_menu.set_has_arrow(False) 744context_menu.set_parent(self) 745context_menu.set_halign(Gtk.Align.START) 746context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 747return context_menu 748 749def show_context_menu(self, gesture, n_presses, x, y): 750rect = Gdk.Rectangle() 751rect.x = int(x) 752rect.y = int(y) 753rect.width = 1 754rect.height = 1 755 756self.context_menu.set_pointing_to(rect) 757self.context_menu.popup() 758 759def show_options(self, _0=None, _1=None): 760if self.options_window is None: 761self.options_window = WindowListOptions() 762self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width) 763self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options) 764self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace) 765self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter) 766 767def reset_window(*args): 768self.options_window = None 769 770self.options_window.connect("close-request", reset_window) 771self.options_window.present() 772 773def remove_workspace_filtering(self): 774for button in self.toplevel_buttons.values(): 775if button.get_parent() is None: 776self.append(button) 777 778def update_workspace_filter(self, checkbutton): 779self.show_only_this_wf_workspace = checkbutton.get_active() 780if checkbutton.get_active(): 781self.filter_to_wf_workspace() 782else: 783self.remove_workspace_filtering() 784 785def update_button_options(self, adjustment): 786self.window_button_options.max_width = adjustment.get_value() 787child: Gtk.Widget = self.get_first_child() 788while child: 789child.queue_allocate() 790child.queue_resize() 791child.queue_draw() 792child = child.get_next_sibling() 793 794self.emit("config-changed") 795 796def get_config(self): 797return { 798"max_button_width": self.window_button_options.max_width, 799"show_only_this_wf_workspace": self.show_only_this_wf_workspace, 800"show_only_this_output": self.show_only_this_output, 801} 802 803def make_draggable(self): 804for button in self.toplevel_buttons.values(): 805button.remove_controller(button.drag_source) 806button.set_sensitive(False) 807 808def restore_drag(self): 809for button in self.toplevel_buttons.values(): 810button.add_controller(button.drag_source) 811button.set_sensitive(True) 812