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