__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): 275self.on_button_leave(None) 276self.popover_open = True 277self.popover_menu.popup() 278 279def hide_menu(self, popover): 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.wl_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): 583if self.wf_socket is not None: 584self.filter_to_wf_workspace() 585 586if self.wl_shm: 587return 588 589self.my_output = None 590self.wl_seat = self.get_root().get_application().wl_seat 591self.output = None 592self.wl_shm = self.get_root().get_application().wl_shm 593self.screencopy_manager = self.get_root().get_application().screencopy_manager 594self.live_preview_output_name = None 595 596def set_live_preview_output(self, output): 597self.output = output 598 599def get_live_preview_output(self): 600return self.output 601 602def wl_output_enter(self, output, name): 603self.set_live_preview_output(None) 604if name.startswith("live-preview"): 605self.set_live_preview_output(output) 606return 607if name.startswith("live-preview"): 608return 609if not self.my_output and name == self.get_root().monitor_name: 610self.wl_surface = None 611if self.get_root().get_native().get_surface(): 612wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface( 613ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer( 614self.get_root().get_native().get_surface().__gpointer__, None))) 615self.wl_surface = WlSurface() 616self.wl_surface._ptr = wl_surface_ptr 617self.my_output = output 618else: 619toplevel_buttons = self.toplevel_buttons.copy() 620for handle in toplevel_buttons: 621button = self.toplevel_buttons[handle] 622if output != button.output: 623self.foreign_toplevel_closed(handle) 624 625def foreign_toplevel_output_enter(self, handle, output): 626if self.show_only_this_output and (not hasattr(output, "name") or output.name != self.get_root().monitor_name): 627return 628 629button = WindowButton(handle, handle.title, self) 630button.icon.set_pixel_size(self.icon_size) 631button.output = output 632self.set_title(button, handle.title) 633button.set_group(self.initial_button) 634button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options, button)) 635button.connect("clicked", self.on_button_click) 636self.set_app_id(button, handle.app_id) 637if handle.app_id.rsplit(maxsplit=1)[-1].startswith("wf-ipc-"): 638wf_output = self.get_wf_output_by_name(self.get_root().monitor_name) 639wf_id = int(handle.app_id.rsplit(maxsplit=1)[-1].removeprefix("wf-ipc-")) 640view = self.wf_socket.get_view(wf_id) 641mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2 642mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2 643output_width = wf_output["geometry"]["width"] 644output_height = wf_output["geometry"]["height"] 645if 0 <= mid_x < output_width and 0 <= mid_y < output_height and button.output == self.my_output: 646# It is in this workspace; keep it 647if button.get_parent() is None: 648self.toplevel_buttons[handle] = button 649self.append(button) 650else: 651self.toplevel_buttons[handle] = button 652self.append(button) 653 654def foreign_toplevel_output_leave(self, handle, output): 655if handle in self.toplevel_buttons: 656button = self.toplevel_buttons[handle] 657if button.get_parent() == self: 658button.output = None 659self.remove(button) 660self.set_all_rectangles() 661 662def set_title(self, button, title): 663button.window_title = title 664if not self.live_preview_tooltips: 665button.set_tooltip_text(title) 666 667def foreign_toplevel_title(self, handle, title): 668handle.title = title 669if handle in self.toplevel_buttons: 670button = self.toplevel_buttons[handle] 671self.set_title(button, title) 672 673def set_all_rectangles(self): 674child = self.get_first_child() 675while child is not None and self.wl_surface: 676if isinstance(child, WindowButton): 677x, y, w, h = get_widget_rect(child) 678if w > 0 and h > 0: 679child.window_id.set_rectangle(self.wl_surface, x, y, w, h) 680 681child = child.get_next_sibling() 682 683def on_button_click(self, button: WindowButton): 684# Set a rectangle for animation 685button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button)) 686if button.window_state.minimised: 687button.window_id.activate(self.wl_seat) 688else: 689if button.window_state.focused: 690button.window_id.set_minimized() 691else: 692button.window_id.activate(self.wl_seat) 693 694def foreign_toplevel_state(self, handle, states): 695if handle in self.toplevel_buttons: 696state_info = WindowState.from_state_array(states) 697button = self.toplevel_buttons[handle] 698button.window_state = state_info 699if state_info.focused: 700button.set_active(True) 701else: 702self.initial_button.set_active(True) 703 704def foreign_toplevel_refresh(self): 705for button in self.toplevel_buttons.values(): 706self.remove(button) 707 708self.toplevel_buttons.clear() 709self.toplevel_buttons_by_wf_id.clear() 710 711def set_app_id(self, button, app_id): 712button.app_id = app_id 713button.set_icon_from_app_id(app_id) 714app_ids = app_id.split() 715for app_id in app_ids: 716if app_id.startswith("wf-ipc-"): 717self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button 718 719def foreign_toplevel_app_id(self, handle, app_id): 720handle.app_id = app_id 721if handle in self.toplevel_buttons: 722button = self.toplevel_buttons[handle] 723self.set_app_id(button, app_id) 724 725def foreign_toplevel_closed(self, handle): 726if handle not in self.toplevel_buttons: 727return 728button: WindowButton = self.toplevel_buttons[handle] 729wf_id = button.wf_ipc_id 730if handle in self.toplevel_buttons: 731self.remove(self.toplevel_buttons[handle]) 732self.toplevel_buttons.pop(handle) 733if wf_id in self.toplevel_buttons_by_wf_id: 734self.toplevel_buttons_by_wf_id.pop(wf_id) 735 736self.set_all_rectangles() 737 738def make_context_menu(self): 739menu = Gio.Menu() 740menu.append(_("Window list _options"), "applet.options") 741context_menu = Gtk.PopoverMenu.new_from_model(menu) 742context_menu.set_has_arrow(False) 743context_menu.set_parent(self) 744context_menu.set_halign(Gtk.Align.START) 745context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 746return context_menu 747 748def show_context_menu(self, gesture, n_presses, x, y): 749rect = Gdk.Rectangle() 750rect.x = int(x) 751rect.y = int(y) 752rect.width = 1 753rect.height = 1 754 755self.context_menu.set_pointing_to(rect) 756self.context_menu.popup() 757 758def show_options(self, _0=None, _1=None): 759if self.options_window is None: 760self.options_window = WindowListOptions() 761self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width) 762self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options) 763self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace) 764self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter) 765 766def reset_window(*args): 767self.options_window = None 768 769self.options_window.connect("close-request", reset_window) 770self.options_window.present() 771 772def remove_workspace_filtering(self): 773for button in self.toplevel_buttons.values(): 774if button.get_parent() is None: 775self.append(button) 776 777def update_workspace_filter(self, checkbutton): 778self.show_only_this_wf_workspace = checkbutton.get_active() 779if checkbutton.get_active(): 780self.filter_to_wf_workspace() 781else: 782self.remove_workspace_filtering() 783 784def update_button_options(self, adjustment): 785self.window_button_options.max_width = adjustment.get_value() 786child: Gtk.Widget = self.get_first_child() 787while child: 788child.queue_allocate() 789child.queue_resize() 790child.queue_draw() 791child = child.get_next_sibling() 792 793self.emit("config-changed") 794 795def get_config(self): 796return { 797"max_button_width": self.window_button_options.max_width, 798"show_only_this_wf_workspace": self.show_only_this_wf_workspace, 799"show_only_this_output": self.show_only_this_output, 800} 801 802def make_draggable(self): 803for button in self.toplevel_buttons.values(): 804button.remove_controller(button.drag_source) 805button.set_sensitive(False) 806 807def restore_drag(self): 808for button in self.toplevel_buttons.values(): 809button.add_controller(button.drag_source) 810button.set_sensitive(True) 811