By using this site, you agree to have cookies stored on your device, strictly for functional purposes, such as storing your session and preferences.

Dismiss

 __init__.py

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