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