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