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