__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 locale 24import typing 25from pathlib import Path 26from pywayland.client import Display, EventQueue 27from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput 28from pywayland.protocol.wayland.wl_output import WlOutputProxy 29from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import ( 30ZwlrForeignToplevelManagerV1, 31ZwlrForeignToplevelHandleV1 32) 33import panorama_panel 34 35import gi 36 37gi.require_version("Gtk", "4.0") 38gi.require_version("GdkWayland", "4.0") 39 40from gi.repository import Gtk, GLib, Gtk4LayerShell, Gio, Gdk, Pango, GObject 41 42 43module_directory = Path(__file__).resolve().parent 44 45locale.bindtextdomain("panorama-window-list", module_directory / "locale") 46_ = lambda x: locale.dgettext("panorama-window-list", x) 47 48 49import ctypes 50from cffi import FFI 51ffi = FFI() 52ffi.cdef(""" 53void * gdk_wayland_display_get_wl_display (void * display); 54void * gdk_wayland_surface_get_wl_surface (void * surface); 55void * gdk_wayland_monitor_get_wl_output (void * monitor); 56""") 57gtk = ffi.dlopen("libgtk-4.so.1") 58 59 60@Gtk.Template(filename=str(module_directory / "panorama-window-list-options.ui")) 61class WindowListOptions(Gtk.Window): 62__gtype_name__ = "WindowListOptions" 63button_width_adjustment: Gtk.Adjustment = Gtk.Template.Child() 64workspace_filter_checkbutton: Gtk.CheckButton = Gtk.Template.Child() 65 66def __init__(self, **kwargs): 67super().__init__(**kwargs) 68 69self.connect("close-request", lambda *args: self.destroy()) 70 71 72def split_bytes_into_ints(array: bytes, size: int = 4) -> list[int]: 73if len(array) % size: 74raise ValueError(f"The byte string's length must be a multiple of {size}") 75 76values: list[int] = [] 77for i in range(0, len(array), size): 78values.append(int.from_bytes(array[i : i+size], byteorder=sys.byteorder)) 79 80return values 81 82 83def get_widget_rect(widget: Gtk.Widget) -> tuple[int, int, int, int]: 84width = int(widget.get_width()) 85height = int(widget.get_height()) 86 87toplevel = widget.get_root() 88if not toplevel: 89return None, None, width, height 90 91x, y = widget.translate_coordinates(toplevel, 0, 0) 92x = int(x) 93y = int(y) 94 95return x, y, width, height 96 97 98@dataclasses.dataclass 99class WindowState: 100minimised: bool 101maximised: bool 102fullscreen: bool 103focused: bool 104 105@classmethod 106def from_state_array(cls, array: bytes): 107values = split_bytes_into_ints(array) 108instance = cls(False, False, False, False) 109for value in values: 110match value: 111case 0: 112instance.maximised = True 113case 1: 114instance.minimised = True 115case 2: 116instance.focused = True 117case 3: 118instance.fullscreen = True 119 120return instance 121 122 123from gi.repository import Gtk, Gdk 124 125class WindowButtonOptions: 126def __init__(self, max_width: int): 127self.max_width = max_width 128 129class WindowButtonLayoutManager(Gtk.LayoutManager): 130def __init__(self, options: WindowButtonOptions, **kwargs): 131super().__init__(**kwargs) 132self.options = options 133 134def do_measure(self, widget, orientation, for_size): 135child = widget.get_first_child() 136if child is None: 137return 0, 0, 0, 0 138 139if orientation == Gtk.Orientation.HORIZONTAL: 140min_width, nat_width, min_height, nat_height = child.measure(Gtk.Orientation.HORIZONTAL, for_size) 141width = self.options.max_width 142return min_width, width, min_height, nat_height 143else: 144min_width, nat_width, min_height, nat_height = child.measure(Gtk.Orientation.VERTICAL, for_size) 145return min_height, nat_height, 0, 0 146 147def do_allocate(self, widget, width, height, baseline): 148child = widget.get_first_child() 149if child is None: 150return 151alloc_width = min(width, self.options.max_width) 152alloc = Gdk.Rectangle() 153alloc.x = 0 154alloc.y = 0 155alloc.width = alloc_width 156alloc.height = height 157child.allocate(alloc.width, alloc.height, baseline) 158 159 160class WindowButton(Gtk.ToggleButton): 161def __init__(self, window_id, window_title, **kwargs): 162super().__init__(**kwargs) 163 164self.window_id: ZwlrForeignToplevelHandleV1 = window_id 165self.wf_ipc_id: typing.Optional[int] = None 166self.set_has_frame(False) 167self.label = Gtk.Label() 168self.icon = Gtk.Image.new_from_icon_name("application-x-executable") 169box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4) 170box.append(self.icon) 171box.append(self.label) 172self.set_child(box) 173 174self.window_title = window_title 175self.window_state = WindowState(False, False, False, False) 176 177self.label.set_ellipsize(Pango.EllipsizeMode.END) 178self.set_hexpand(True) 179self.set_vexpand(True) 180 181self.drag_source = Gtk.DragSource(actions=Gdk.DragAction.MOVE) 182self.drag_source.connect("prepare", self.provide_drag_data) 183self.drag_source.connect("drag-begin", self.drag_begin) 184self.drag_source.connect("drag-cancel", self.drag_cancel) 185 186self.add_controller(self.drag_source) 187 188self.menu = Gio.Menu() 189# TODO: toggle the labels when needed 190self.minimise_item = Gio.MenuItem.new(_("_Minimise"), "button.minimise") 191self.maximise_item = Gio.MenuItem.new(_("Ma_ximise"), "button.maximise") 192self.menu.append_item(self.minimise_item) 193self.menu.append_item(self.maximise_item) 194self.menu.append(_("_Close"), "button.close") 195self.menu.append(_("Window list _options"), "applet.options") 196self.popover_menu = Gtk.PopoverMenu.new_from_model(self.menu) 197self.popover_menu.set_parent(self) 198self.popover_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 199self.popover_menu.set_has_arrow(False) 200self.popover_menu.set_halign(Gtk.Align.END) 201 202self.right_click_controller = Gtk.GestureClick(button=3) 203self.right_click_controller.connect("pressed", self.show_menu) 204self.add_controller(self.right_click_controller) 205 206self.action_group = Gio.SimpleActionGroup() 207close_action = Gio.SimpleAction.new("close") 208close_action.connect("activate", self.close_associated) 209self.action_group.insert(close_action) 210minimise_action = Gio.SimpleAction.new("minimise") 211minimise_action.connect("activate", self.minimise_associated) 212self.action_group.insert(minimise_action) 213maximise_action = Gio.SimpleAction.new("maximise") 214maximise_action.connect("activate", self.maximise_associated) 215self.action_group.insert(maximise_action) 216 217self.insert_action_group("button", self.action_group) 218 219self.middle_click_controller = Gtk.GestureClick(button=2) 220self.middle_click_controller.connect("released", self.close_associated) 221self.add_controller(self.middle_click_controller) 222 223def show_menu(self, gesture, n_presses, x, y): 224rect = Gdk.Rectangle() 225rect.x = int(x) 226rect.y = int(y) 227rect.width = 1 228rect.height = 1 229self.popover_menu.popup() 230 231def close_associated(self, *args): 232self.window_id.close() 233 234def minimise_associated(self, action, *args): 235if self.window_state.minimised: 236self.window_id.unset_minimized() 237else: 238self.window_id.set_minimized() 239 240def maximise_associated(self, action, *args): 241if self.window_state.maximised: 242self.window_id.unset_maximized() 243else: 244self.window_id.set_maximized() 245 246def provide_drag_data(self, source: Gtk.DragSource, x: float, y: float): 247app = self.get_root().get_application() 248app.drags[id(self)] = self 249value = GObject.Value() 250value.init(GObject.TYPE_UINT64) 251value.set_uint64(id(self)) 252return Gdk.ContentProvider.new_for_value(value) 253 254def drag_begin(self, source: Gtk.DragSource, drag: Gdk.Drag): 255paintable = Gtk.WidgetPaintable.new(self).get_current_image() 256source.set_icon(paintable, 0, 0) 257self.hide() 258 259def drag_cancel(self, source: Gtk.DragSource, drag: Gdk.Drag, reason: Gdk.DragCancelReason): 260self.show() 261return False 262 263@property 264def window_title(self): 265return self.label.get_text() 266 267@window_title.setter 268def window_title(self, value): 269self.label.set_text(value) 270 271def set_icon_from_app_id(self, app_id): 272app_ids = app_id.split() 273 274# If on Wayfire, find the IPC ID 275for app_id in app_ids: 276if app_id.startswith("wf-ipc-"): 277self.wf_ipc_id = int(app_id.removeprefix("wf-ipc-")) 278break 279 280# Try getting an icon from the correct theme 281icon_theme = Gtk.IconTheme.get_for_display(self.get_display()) 282 283for app_id in app_ids: 284if icon_theme.has_icon(app_id): 285self.icon.set_from_icon_name(app_id) 286return 287 288# If that doesn't work, try getting one from .desktop files 289for app_id in app_ids: 290try: 291desktop_file = Gio.DesktopAppInfo.new(app_id + ".desktop") 292if desktop_file: 293self.icon.set_from_gicon(desktop_file.get_icon()) 294return 295except TypeError: 296# Due to a bug, the constructor may sometimes return C NULL 297pass 298 299 300class WFWindowList(panorama_panel.Applet): 301name = _("Wayfire window list") 302description = _("Traditional window list (for Wayfire and other wlroots compositors)") 303 304def __init__(self, orientation=Gtk.Orientation.HORIZONTAL, config=None): 305super().__init__(orientation=orientation, config=config) 306if config is None: 307config = {} 308 309self.set_homogeneous(True) 310self.window_button_options = WindowButtonOptions(config.get("max_button_width", 256)) 311self.show_only_this_wf_workspace = config.get("show_only_this_wf_workspace", True) 312self.show_only_this_output = config.get("show_only_this_output", True) 313self.icon_size = config.get("icon_size", 24) 314 315self.toplevel_buttons: dict[ZwlrForeignToplevelHandleV1, WindowButton] = {} 316self.toplevel_buttons_by_wf_id: dict[int, WindowButton] = {} 317# This button doesn't belong to any window but is used for the button group and to be 318# selected when no window is focused 319self.initial_button = Gtk.ToggleButton() 320 321self.display = None 322self.my_output = None 323self.wl_surface_ptr = None 324self.registry = None 325self.compositor = None 326self.seat = None 327 328self.context_menu = self.make_context_menu() 329panorama_panel.track_popover(self.context_menu) 330 331right_click_controller = Gtk.GestureClick() 332right_click_controller.set_button(3) 333right_click_controller.connect("pressed", self.show_context_menu) 334 335self.add_controller(right_click_controller) 336 337action_group = Gio.SimpleActionGroup() 338options_action = Gio.SimpleAction.new("options", None) 339options_action.connect("activate", self.show_options) 340action_group.add_action(options_action) 341self.insert_action_group("applet", action_group) 342# Wait for the widget to be in a layer-shell window before doing this 343self.connect("realize", self.get_wl_resources) 344 345self.options_window = None 346 347# Support button reordering 348self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE) 349self.drop_target.set_gtypes([GObject.TYPE_UINT64]) 350self.drop_target.connect("drop", self.drop_button) 351 352self.add_controller(self.drop_target) 353 354# Make a Wayfire socket for workspace handling 355try: 356import wayfire 357self.wf_socket = wayfire.WayfireSocket() 358self.wf_socket.watch(["view-workspace-changed", "wset-workspace-changed"]) 359fd = self.wf_socket.client.fileno() 360GLib.io_add_watch(GLib.IOChannel.unix_new(fd), GLib.IO_IN, self.on_wf_event, priority=GLib.PRIORITY_HIGH) 361except: 362# Wayfire raises Exception itself, so it cannot be narrowed down 363self.wf_socket = None 364 365def get_wf_output_by_name(self, name): 366if not self.wf_socket: 367return None 368for output in self.wf_socket.list_outputs(): 369if output["name"] == name: 370return output 371return None 372 373def on_wf_event(self, source, condition): 374if condition & GLib.IO_IN: 375try: 376message = self.wf_socket.read_next_event() 377event = message.get("event") 378match event: 379case "view-workspace-changed": 380view = message.get("view", {}) 381if view["output-name"] == self.get_root().monitor_name: 382output = self.get_wf_output_by_name(view["output-name"]) 383current_workspace = output["workspace"]["x"], output["workspace"]["y"] 384button = self.toplevel_buttons_by_wf_id[view["id"]] 385if not self.show_only_this_wf_workspace or (message["to"]["x"], message["to"]["y"]) == current_workspace: 386if button.get_parent() is None and button.output == self.my_output: 387self.append(button) 388else: 389if button.get_parent() is self: 390# Remove out-of-workspace window 391self.remove(button) 392case "wset-workspace-changed": 393output_name = self.get_root().monitor_name 394if message["wset-data"]["output-name"] == output_name: 395# It has changed on this monitor; refresh the window list 396self.filter_to_wf_workspace() 397 398except Exception as e: 399print("Error reading Wayfire event:", e) 400return True 401 402def drop_button(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float): 403button: WindowButton = self.get_root().get_application().drags.pop(value) 404if button.get_parent() is not self: 405# Prevent dropping a button from another window list 406return False 407 408self.remove(button) 409# Find the position where to insert the applet 410# Probably we could use the assumption that buttons are homogeneous here for efficiency 411child = self.get_first_child() 412while child: 413allocation = child.get_allocation() 414child_x, child_y = self.translate_coordinates(self, 0, 0) 415if self.get_orientation() == Gtk.Orientation.HORIZONTAL: 416midpoint = child_x + allocation.width / 2 417if x < midpoint: 418button.insert_before(self, child) 419break 420elif self.get_orientation() == Gtk.Orientation.VERTICAL: 421midpoint = child_y + allocation.height / 2 422if y < midpoint: 423button.insert_before(self, child) 424break 425child = child.get_next_sibling() 426else: 427self.append(button) 428button.show() 429 430self.set_all_rectangles() 431return True 432 433def filter_to_wf_workspace(self): 434if not self.show_only_this_wf_workspace: 435return 436 437output = self.get_wf_output_by_name(self.get_root().monitor_name) 438for wf_id, button in self.toplevel_buttons_by_wf_id.items(): 439view = self.wf_socket.get_view(wf_id) 440mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2 441mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2 442output_width = output["geometry"]["width"] 443output_height = output["geometry"]["height"] 444if 0 <= mid_x < output_width and 0 <= mid_y < output_height and button.output == self.my_output: 445# It is in this workspace; keep it 446if button.get_parent() is None: 447self.append(button) 448else: 449# Remove it from this window list 450if button.get_parent() is self: 451self.remove(button) 452 453def get_wl_resources(self, widget): 454ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p 455ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,) 456 457self.display = Display() 458wl_display_ptr = gtk.gdk_wayland_display_get_wl_display( 459ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.get_root().get_native().get_display().__gpointer__, None))) 460self.display._ptr = wl_display_ptr 461self.event_queue = EventQueue(self.display) 462 463# Intentionally commented: the display is already connected by GTK 464# self.display.connect() 465 466my_monitor = Gtk4LayerShell.get_monitor(self.get_root()) 467 468# Iterate through monitors and get their Wayland output (wl_output) 469# This is a hack to ensure output_enter/leave is called for toplevels 470for monitor in self.get_root().get_application().monitors: 471wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None))) 472if wl_output and not monitor.output_proxy: 473print("Create proxy") 474output_proxy = WlOutputProxy(wl_output, self.display) 475output_proxy.interface.registry[output_proxy._ptr] = output_proxy 476monitor.output_proxy = output_proxy 477 478if monitor == my_monitor: 479self.my_output = monitor.output_proxy 480# End hack 481 482self.manager = None 483self.registry = self.display.get_registry() 484self.registry.dispatcher["global"] = self.on_global 485self.display.roundtrip() 486if self.manager is None: 487print("Could not load wf-window-list. Is foreign-toplevel protocol advertised by the compositor?") 488print("(Wayfire requires enabling foreign-toplevel plugin)") 489return 490self.manager.dispatcher["toplevel"] = self.on_new_toplevel 491self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished") 492self.display.roundtrip() 493fd = self.display.get_fd() 494GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event) 495 496if self.wf_socket is not None: 497self.filter_to_wf_workspace() 498 499def on_display_event(self, source, condition): 500if condition & GLib.IO_IN: 501self.display.dispatch(queue=self.event_queue) 502return True 503 504def on_global(self, registry, name, interface, version): 505if interface == "zwlr_foreign_toplevel_manager_v1": 506self.print_log("Interface registered") 507self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version) 508elif interface == "wl_seat": 509self.print_log("Seat found") 510self.seat = registry.bind(name, WlSeat, version) 511elif interface == "wl_compositor": 512self.compositor = registry.bind(name, WlCompositor, version) 513wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface( 514ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer( 515self.get_root().get_native().get_surface().__gpointer__, None))) 516self.wl_surface = WlSurface() 517self.wl_surface._ptr = wl_surface_ptr 518 519def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1, 520handle: ZwlrForeignToplevelHandleV1): 521handle.dispatcher["title"] = self.on_title_changed 522handle.dispatcher["app_id"] = self.on_app_id_changed 523handle.dispatcher["output_enter"] = self.on_output_entered 524handle.dispatcher["output_leave"] = self.on_output_left 525handle.dispatcher["state"] = self.on_state_changed 526handle.dispatcher["closed"] = self.on_closed 527 528def on_output_entered(self, handle, output): 529button = WindowButton(handle, handle.title) 530button.icon.set_pixel_size(self.icon_size) 531button.output = None 532 533if self.show_only_this_output and output != self.my_output: 534return 535 536button.output = output 537self.set_title(button, handle.title) 538button.set_group(self.initial_button) 539button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options)) 540button.connect("clicked", self.on_button_click) 541self.set_app_id(button, handle.app_id) 542self.toplevel_buttons[handle] = button 543self.append(button) 544self.set_all_rectangles() 545 546def on_output_left(self, handle, output): 547if handle in self.toplevel_buttons: 548button = self.toplevel_buttons[handle] 549if button.get_parent() == self: 550button.output = None 551self.remove(button) 552self.set_all_rectangles() 553 554def set_title(self, button, title): 555button.window_title = title 556button.set_tooltip_text(title) 557 558def on_title_changed(self, handle, title): 559handle.title = title 560if handle in self.toplevel_buttons: 561button = self.toplevel_buttons[handle] 562self.set_title(button, title) 563 564def set_all_rectangles(self): 565child = self.get_first_child() 566while child is not None: 567if isinstance(child, WindowButton): 568child.window_id.set_rectangle(self.wl_surface, *get_widget_rect(child)) 569 570child = child.get_next_sibling() 571 572def on_button_click(self, button: WindowButton): 573# Set a rectangle for animation 574button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button)) 575if button.window_state.focused: 576# Already pressed in, so minimise the focused window 577button.window_id.set_minimized() 578else: 579button.window_id.unset_minimized() 580button.window_id.activate(self.seat) 581 582def on_state_changed(self, handle, states): 583if handle in self.toplevel_buttons: 584state_info = WindowState.from_state_array(states) 585button = self.toplevel_buttons[handle] 586button.window_state = state_info 587if state_info.focused: 588button.set_active(True) 589else: 590self.initial_button.set_active(True) 591 592self.set_all_rectangles() 593 594def set_app_id(self, button, app_id): 595button.set_icon_from_app_id(app_id) 596app_ids = app_id.split() 597for app_id in app_ids: 598if app_id.startswith("wf-ipc-"): 599self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button 600 601def on_app_id_changed(self, handle, app_id): 602handle.app_id = app_id 603if handle in self.toplevel_buttons: 604button = self.toplevel_buttons[handle] 605self.set_app_id(button, app_id) 606 607def on_closed(self, handle): 608if handle not in self.toplevel_buttons: 609return 610button: WindowButton = self.toplevel_buttons[handle] 611wf_id = button.wf_ipc_id 612if handle in self.toplevel_buttons: 613self.remove(self.toplevel_buttons[handle]) 614self.toplevel_buttons.pop(handle) 615if wf_id in self.toplevel_buttons_by_wf_id: 616self.toplevel_buttons_by_wf_id.pop(wf_id) 617 618self.set_all_rectangles() 619 620def make_context_menu(self): 621menu = Gio.Menu() 622menu.append(_("Window list _options"), "applet.options") 623context_menu = Gtk.PopoverMenu.new_from_model(menu) 624context_menu.set_has_arrow(False) 625context_menu.set_parent(self) 626context_menu.set_halign(Gtk.Align.START) 627context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED) 628return context_menu 629 630def show_context_menu(self, gesture, n_presses, x, y): 631rect = Gdk.Rectangle() 632rect.x = int(x) 633rect.y = int(y) 634rect.width = 1 635rect.height = 1 636 637self.context_menu.set_pointing_to(rect) 638self.context_menu.popup() 639 640def show_options(self, _0=None, _1=None): 641if self.options_window is None: 642self.options_window = WindowListOptions() 643self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width) 644self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options) 645self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace) 646self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter) 647 648def reset_window(*args): 649self.options_window = None 650 651self.options_window.connect("close-request", reset_window) 652self.options_window.present() 653 654def remove_workspace_filtering(self): 655for button in self.toplevel_buttons.values(): 656if button.get_parent() is None: 657self.append(button) 658 659def update_workspace_filter(self, checkbutton): 660self.show_only_this_wf_workspace = checkbutton.get_active() 661if checkbutton.get_active(): 662self.filter_to_wf_workspace() 663else: 664self.remove_workspace_filtering() 665 666def update_button_options(self, adjustment): 667self.window_button_options.max_width = adjustment.get_value() 668child: Gtk.Widget = self.get_first_child() 669while child: 670child.queue_allocate() 671child.queue_resize() 672child.queue_draw() 673child = child.get_next_sibling() 674 675self.emit("config-changed") 676 677def get_config(self): 678return { 679"max_button_width": self.window_button_options.max_width, 680"show_only_this_wf_workspace": self.show_only_this_wf_workspace, 681"show_only_this_output": self.show_only_this_output, 682} 683 684def output_changed(self): 685self.get_wl_resources() 686 687def make_draggable(self): 688for button in self.toplevel_buttons.values(): 689button.remove_controller(button.drag_source) 690button.set_sensitive(False) 691 692def restore_drag(self): 693for button in self.toplevel_buttons.values(): 694button.add_controller(button.drag_source) 695button.set_sensitive(True) 696