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