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.52 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.my_output = None
322
self.wl_surface_ptr = None
323
self.registry = None
324
self.compositor = None
325
self.seat = None
326
327
self.context_menu = self.make_context_menu()
328
panorama_panel.track_popover(self.context_menu)
329
330
right_click_controller = Gtk.GestureClick()
331
right_click_controller.set_button(3)
332
right_click_controller.connect("pressed", self.show_context_menu)
333
334
self.add_controller(right_click_controller)
335
336
action_group = Gio.SimpleActionGroup()
337
options_action = Gio.SimpleAction.new("options", None)
338
options_action.connect("activate", self.show_options)
339
action_group.add_action(options_action)
340
self.insert_action_group("applet", action_group)
341
# Wait for the widget to be in a layer-shell window before doing this
342
self.connect("realize", lambda *args: self.get_wl_resources())
343
344
self.options_window = None
345
346
# Support button reordering
347
self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE)
348
self.drop_target.set_gtypes([GObject.TYPE_UINT64])
349
self.drop_target.connect("drop", self.drop_button)
350
351
self.add_controller(self.drop_target)
352
353
# Make a Wayfire socket for workspace handling
354
try:
355
import wayfire
356
self.wf_socket = wayfire.WayfireSocket()
357
self.wf_socket.watch()
358
fd = self.wf_socket.client.fileno()
359
GLib.io_add_watch(GLib.IOChannel.unix_new(fd), GLib.IO_IN, self.on_wf_event, priority=GLib.PRIORITY_HIGH)
360
except:
361
# Wayfire raises Exception itself, so it cannot be narrowed down
362
self.wf_socket = None
363
364
def get_wf_output_by_name(self, name):
365
if not self.wf_socket:
366
return None
367
for output in self.wf_socket.list_outputs():
368
if output["name"] == name:
369
return output
370
return None
371
372
def on_wf_event(self, source, condition):
373
if condition & GLib.IO_IN:
374
try:
375
message = self.wf_socket.read_next_event()
376
event = message.get("event")
377
match event:
378
case "view-workspace-changed":
379
view = message.get("view", {})
380
if view["output-name"] == self.get_root().monitor_name:
381
current_workspace = output["workspace"]["x"], output["workspace"]["y"]
382
if not self.show_only_this_wf_workspace or (message["to"]["x"], message["to"]["y"]) == current_workspace:
383
if self.toplevel_buttons_by_wf_id[view["id"]].get_parent() is None:
384
self.append(self.toplevel_buttons_by_wf_id[view["id"]])
385
else:
386
if self.toplevel_buttons_by_wf_id[view["id"]].get_parent() is self:
387
# Remove out-of-workspace window
388
self.remove(self.toplevel_buttons_by_wf_id[view["id"]])
389
case "wset-workspace-changed":
390
output_name = self.get_root().monitor_name
391
if message["wset-data"]["output-name"] == output_name:
392
# It has changed on this monitor; refresh the window list
393
self.filter_to_wf_workspace()
394
395
except Exception as e:
396
print("Error reading Wayfire event:", e)
397
return True
398
399
def drop_button(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
400
button: WindowButton = self.get_root().get_application().drags.pop(value)
401
if button.get_parent() is not self:
402
# Prevent dropping a button from another window list
403
return False
404
405
self.remove(button)
406
# Find the position where to insert the applet
407
# Probably we could use the assumption that buttons are homogeneous here for efficiency
408
child = self.get_first_child()
409
while child:
410
allocation = child.get_allocation()
411
child_x, child_y = self.translate_coordinates(self, 0, 0)
412
if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
413
midpoint = child_x + allocation.width / 2
414
if x < midpoint:
415
button.insert_before(self, child)
416
break
417
elif self.get_orientation() == Gtk.Orientation.VERTICAL:
418
midpoint = child_y + allocation.height / 2
419
if y < midpoint:
420
button.insert_before(self, child)
421
break
422
child = child.get_next_sibling()
423
else:
424
self.append(button)
425
button.show()
426
427
self.set_all_rectangles()
428
return True
429
430
def filter_to_wf_workspace(self):
431
if not self.show_only_this_wf_workspace:
432
return
433
434
output = self.get_wf_output_by_name(self.get_root().monitor_name)
435
for wf_id, button in self.toplevel_buttons_by_wf_id.items():
436
view = self.wf_socket.get_view(wf_id)
437
mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2
438
mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2
439
output_width = output["geometry"]["width"]
440
output_height = output["geometry"]["height"]
441
if 0 <= mid_x < output_width and 0 <= mid_y < output_height:
442
# It is in this workspace; keep it
443
if button.get_parent() is None:
444
self.append(button)
445
else:
446
# Remove it from this window list
447
if button.get_parent() is self:
448
self.remove(button)
449
450
def get_wl_resources(self):
451
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
452
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
453
454
self.display = Display()
455
wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
456
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.get_root().get_native().get_display().__gpointer__, None)))
457
self.display._ptr = wl_display_ptr
458
self.event_queue = EventQueue(self.display)
459
460
# Intentionally commented: the display is already connected by GTK
461
# self.display.connect()
462
463
my_monitor = Gtk4LayerShell.get_monitor(self.get_root())
464
465
# Iterate through monitors and get their Wayland output (wl_output)
466
# This is a hack to ensure output_enter/leave is called for toplevels
467
for monitor in self.get_root().get_native().get_display().get_monitors():
468
wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None)))
469
if wl_output:
470
print("Create proxy")
471
output_proxy = WlOutputProxy(wl_output, self.display)
472
output_proxy.interface.registry[output_proxy._ptr] = output_proxy
473
474
if monitor == my_monitor:
475
self.my_output = output_proxy
476
# End hack
477
478
fd = self.display.get_fd()
479
GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event)
480
self.registry = self.display.get_registry()
481
self.registry.dispatcher["global"] = self.on_global
482
self.display.roundtrip()
483
484
if self.wf_socket is not None:
485
self.filter_to_wf_workspace()
486
487
def on_display_event(self, source, condition):
488
if condition & GLib.IO_IN:
489
self.display.dispatch(queue=self.event_queue)
490
return True
491
492
def on_global(self, registry, name, interface, version):
493
if interface == "zwlr_foreign_toplevel_manager_v1":
494
self.print_log("Interface registered")
495
self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version)
496
self.manager.dispatcher["toplevel"] = self.on_new_toplevel
497
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished")
498
self.display.roundtrip()
499
self.display.flush()
500
elif interface == "wl_seat":
501
self.print_log("Seat found")
502
self.seat = registry.bind(name, WlSeat, version)
503
elif interface == "wl_compositor":
504
self.compositor = registry.bind(name, WlCompositor, version)
505
self.wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface(
506
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(
507
self.get_root().get_native().get_surface().__gpointer__, None)))
508
509
def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
510
handle: ZwlrForeignToplevelHandleV1):
511
handle.dispatcher["title"] = lambda h, title: self.on_title_changed(h, title)
512
handle.dispatcher["app_id"] = lambda h, app_id: self.on_app_id_changed(h, app_id)
513
handle.dispatcher["output_enter"] = self.on_output_entered
514
handle.dispatcher["output_leave"] = self.on_output_left
515
handle.dispatcher["state"] = lambda h, states: self.on_state_changed(h, states)
516
handle.dispatcher["closed"] = lambda h: self.on_closed(h)
517
518
def on_output_entered(self, handle, output):
519
if handle not in self.toplevel_buttons:
520
button = WindowButton(handle, handle.title)
521
self.set_title(button, handle.title)
522
self.set_app_id(button, handle.app_id)
523
button.set_group(self.initial_button)
524
button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options))
525
button.connect("clicked", self.on_button_click)
526
self.toplevel_buttons[handle] = button
527
else:
528
button = self.toplevel_buttons[handle]
529
530
if output == self.my_output:
531
self.append(button)
532
self.set_all_rectangles()
533
534
def on_output_left(self, handle, output):
535
if not self.show_only_this_output:
536
return
537
if output != self.my_output:
538
return
539
if handle in self.toplevel_buttons:
540
button = self.toplevel_buttons[handle]
541
self.remove(button)
542
self.set_all_rectangles()
543
544
def set_title(self, button, title):
545
button.window_title = title
546
button.set_tooltip_text(title)
547
548
def on_title_changed(self, handle, title):
549
handle.title = title
550
if handle in self.toplevel_buttons:
551
button = self.toplevel_buttons[handle]
552
self.set_title(button, title)
553
554
def set_all_rectangles(self):
555
child = self.get_first_child()
556
while child is not None:
557
if isinstance(child, WindowButton):
558
surface = WlSurface()
559
surface._ptr = self.wl_surface_ptr
560
child.window_id.set_rectangle(surface, *get_widget_rect(child))
561
562
child = child.get_next_sibling()
563
564
def on_button_click(self, button: WindowButton):
565
# Set a rectangle for animation
566
surface = WlSurface()
567
surface._ptr = self.wl_surface_ptr
568
button.window_id.set_rectangle(surface, *get_widget_rect(button))
569
if button.window_state.focused:
570
# Already pressed in, so minimise the focused window
571
button.window_id.set_minimized()
572
else:
573
button.window_id.unset_minimized()
574
button.window_id.activate(self.seat)
575
576
def on_state_changed(self, handle, states):
577
if handle in self.toplevel_buttons:
578
state_info = WindowState.from_state_array(states)
579
button = self.toplevel_buttons[handle]
580
button.window_state = state_info
581
if state_info.focused:
582
button.set_active(True)
583
else:
584
self.initial_button.set_active(True)
585
586
self.set_all_rectangles()
587
588
def set_app_id(self, button, app_id):
589
button.set_icon_from_app_id(app_id)
590
app_ids = app_id.split()
591
for app_id in app_ids:
592
if app_id.startswith("wf-ipc-"):
593
self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button
594
595
def on_app_id_changed(self, handle, app_id):
596
handle.app_id = app_id
597
if handle in self.toplevel_buttons:
598
button = self.toplevel_buttons[handle]
599
self.set_app_id(button, app_id)
600
601
def on_closed(self, handle):
602
button: WindowButton = self.toplevel_buttons[handle]
603
wf_id = button.wf_ipc_id
604
if handle in self.toplevel_buttons:
605
self.remove(self.toplevel_buttons[handle])
606
self.toplevel_buttons.pop(handle)
607
if wf_id in self.toplevel_buttons_by_wf_id:
608
self.toplevel_buttons_by_wf_id.pop(wf_id)
609
610
self.set_all_rectangles()
611
612
def make_context_menu(self):
613
menu = Gio.Menu()
614
menu.append(_("Window list _options"), "applet.options")
615
context_menu = Gtk.PopoverMenu.new_from_model(menu)
616
context_menu.set_has_arrow(False)
617
context_menu.set_parent(self)
618
context_menu.set_halign(Gtk.Align.START)
619
context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
620
return context_menu
621
622
def show_context_menu(self, gesture, n_presses, x, y):
623
rect = Gdk.Rectangle()
624
rect.x = int(x)
625
rect.y = int(y)
626
rect.width = 1
627
rect.height = 1
628
629
self.context_menu.set_pointing_to(rect)
630
self.context_menu.popup()
631
632
def show_options(self, _0=None, _1=None):
633
if self.options_window is None:
634
self.options_window = WindowListOptions()
635
self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width)
636
self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options)
637
self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace)
638
self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter)
639
640
def reset_window(*args):
641
self.options_window = None
642
643
self.options_window.connect("close-request", reset_window)
644
self.options_window.present()
645
646
def remove_workspace_filtering(self):
647
for button in self.toplevel_buttons.values():
648
if button.get_parent() is None:
649
self.append(button)
650
651
def update_workspace_filter(self, checkbutton):
652
self.show_only_this_wf_workspace = checkbutton.get_active()
653
if checkbutton.get_active():
654
self.filter_to_wf_workspace()
655
else:
656
self.remove_workspace_filtering()
657
658
def update_button_options(self, adjustment):
659
self.window_button_options.max_width = adjustment.get_value()
660
child: Gtk.Widget = self.get_first_child()
661
while child:
662
child.queue_allocate()
663
child.queue_resize()
664
child.queue_draw()
665
child = child.get_next_sibling()
666
667
self.emit("config-changed")
668
669
def get_config(self):
670
return {
671
"max_button_width": self.window_button_options.max_width,
672
"show_only_this_wf_workspace": self.show_only_this_wf_workspace,
673
"show_only_this_output": self.show_only_this_output,
674
}
675
676
def output_changed(self):
677
self.get_wl_resources()
678
679
def make_draggable(self):
680
for button in self.toplevel_buttons.values():
681
button.remove_controller(button.drag_source)
682
button.set_sensitive(False)
683
684
def restore_drag(self):
685
for button in self.toplevel_buttons.values():
686
button.add_controller(button.drag_source)
687
button.set_sensitive(True)
688