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