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 • 32.73 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, WlShm, 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
from OpenGL import GL
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.display = None
425
self.my_output = None
426
self.wl_surface_ptr = None
427
self.registry = None
428
self.compositor = None
429
self.seat = None
430
431
self.context_menu = self.make_context_menu()
432
panorama_panel.track_popover(self.context_menu)
433
434
right_click_controller = Gtk.GestureClick()
435
right_click_controller.set_button(3)
436
right_click_controller.connect("pressed", self.show_context_menu)
437
438
self.add_controller(right_click_controller)
439
440
action_group = Gio.SimpleActionGroup()
441
options_action = Gio.SimpleAction.new("options", None)
442
options_action.connect("activate", self.show_options)
443
action_group.add_action(options_action)
444
self.insert_action_group("applet", action_group)
445
# Wait for the widget to be in a layer-shell window before doing this
446
self.connect("realize", self.get_wl_resources)
447
448
self.options_window = None
449
450
# Support button reordering
451
self.drop_target = Gtk.DropTarget.new(GObject.TYPE_UINT64, Gdk.DragAction.MOVE)
452
self.drop_target.set_gtypes([GObject.TYPE_UINT64])
453
self.drop_target.connect("drop", self.drop_button)
454
455
self.add_controller(self.drop_target)
456
457
# Make a Wayfire socket for workspace handling
458
self.live_preview_tooltips = False
459
try:
460
import wayfire
461
from wayfire.core.template import get_msg_template
462
self.wf_socket = wayfire.WayfireSocket()
463
self.get_msg_template = get_msg_template
464
if "live_previews/request_stream" in self.wf_socket.list_methods():
465
self.live_preview_tooltips = True
466
print("WFWindowList: Live Preview Tooltips: enabled")
467
else:
468
print("WFWindowList: Live Preview Tooltips: disabled")
469
print("- Live preview tooltips requires wayfire wf-live-previews plugin. Is it enabled?")
470
self.wf_socket.watch(["view-workspace-changed", "wset-workspace-changed"])
471
fd = self.wf_socket.client.fileno()
472
GLib.io_add_watch(GLib.IOChannel.unix_new(fd), GLib.IO_IN, self.on_wf_event, priority=GLib.PRIORITY_HIGH)
473
except:
474
# Wayfire raises Exception itself, so it cannot be narrowed down
475
self.wf_socket = None
476
477
def get_wf_output_by_name(self, name):
478
if not self.wf_socket:
479
return None
480
for output in self.wf_socket.list_outputs():
481
if output["name"] == name:
482
return output
483
return None
484
485
def on_wf_event(self, source, condition):
486
if condition & GLib.IO_IN:
487
try:
488
message = self.wf_socket.read_next_event()
489
event = message.get("event")
490
match event:
491
case "view-workspace-changed":
492
view = message.get("view", {})
493
if view["output-name"] == self.get_root().monitor_name:
494
output = self.get_wf_output_by_name(view["output-name"])
495
current_workspace = output["workspace"]["x"], output["workspace"]["y"]
496
button = self.toplevel_buttons_by_wf_id[view["id"]]
497
if not self.show_only_this_wf_workspace or (message["to"]["x"], message["to"]["y"]) == current_workspace:
498
if button.get_parent() is None and button.output == self.my_output:
499
self.append(button)
500
else:
501
if button.get_parent() is self:
502
# Remove out-of-workspace window
503
self.remove(button)
504
case "wset-workspace-changed":
505
output_name = self.get_root().monitor_name
506
if message["wset-data"]["output-name"] == output_name:
507
# It has changed on this monitor; refresh the window list
508
self.filter_to_wf_workspace()
509
510
except Exception as e:
511
print("Error reading Wayfire event:", e)
512
return True
513
514
def drop_button(self, drop_target: Gtk.DropTarget, value: int, x: float, y: float):
515
button: WindowButton = self.get_root().get_application().drags.pop(value)
516
if button.get_parent() is not self:
517
# Prevent dropping a button from another window list
518
return False
519
520
self.remove(button)
521
# Find the position where to insert the applet
522
# Probably we could use the assumption that buttons are homogeneous here for efficiency
523
child = self.get_first_child()
524
while child:
525
allocation = child.get_allocation()
526
child_x, child_y = self.translate_coordinates(self, 0, 0)
527
if self.get_orientation() == Gtk.Orientation.HORIZONTAL:
528
midpoint = child_x + allocation.width / 2
529
if x < midpoint:
530
button.insert_before(self, child)
531
break
532
elif self.get_orientation() == Gtk.Orientation.VERTICAL:
533
midpoint = child_y + allocation.height / 2
534
if y < midpoint:
535
button.insert_before(self, child)
536
break
537
child = child.get_next_sibling()
538
else:
539
self.append(button)
540
button.show()
541
542
self.set_all_rectangles()
543
return True
544
545
def filter_to_wf_workspace(self):
546
if not self.show_only_this_wf_workspace:
547
return
548
549
output = self.get_wf_output_by_name(self.get_root().monitor_name)
550
for wf_id, button in self.toplevel_buttons_by_wf_id.items():
551
view = self.wf_socket.get_view(wf_id)
552
mid_x = view["geometry"]["x"] + view["geometry"]["width"] / 2
553
mid_y = view["geometry"]["y"] + view["geometry"]["height"] / 2
554
output_width = output["geometry"]["width"]
555
output_height = output["geometry"]["height"]
556
if 0 <= mid_x < output_width and 0 <= mid_y < output_height and button.output == self.my_output:
557
# It is in this workspace; keep it
558
if button.get_parent() is None:
559
self.append(button)
560
else:
561
# Remove it from this window list
562
if button.get_parent() is self:
563
self.remove(button)
564
565
def get_wl_resources(self, widget):
566
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
567
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = (ctypes.py_object,)
568
569
self.display = Display()
570
wl_display_ptr = gtk.gdk_wayland_display_get_wl_display(
571
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(self.get_root().get_native().get_display().__gpointer__, None)))
572
self.display._ptr = wl_display_ptr
573
self.event_queue = EventQueue(self.display)
574
575
# Intentionally commented: the display is already connected by GTK
576
# self.display.connect()
577
578
my_monitor = Gtk4LayerShell.get_monitor(self.get_root())
579
580
# Iterate through monitors and get their Wayland output (wl_output)
581
# This is a hack to ensure output_enter/leave is called for toplevels
582
for monitor in self.get_root().get_application().monitors:
583
wl_output = gtk.gdk_wayland_monitor_get_wl_output(ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(monitor.__gpointer__, None)))
584
if wl_output and not monitor.output_proxy:
585
print("Create proxy")
586
output_proxy = WlOutputProxy(wl_output, self.display)
587
output_proxy.interface.registry[output_proxy._ptr] = output_proxy
588
monitor.output_proxy = output_proxy
589
590
if monitor == my_monitor:
591
self.my_output = monitor.output_proxy
592
# End hack
593
594
self.output = None
595
self.wl_shm = None
596
self.manager = None
597
self.screencopy_manager = None
598
self.live_preview_output_name = None
599
self.registry = self.display.get_registry()
600
self.registry.dispatcher["global"] = self.on_global
601
self.display.roundtrip()
602
if self.manager is None:
603
print("Could not load wf-window-list. Is foreign-toplevel protocol advertised by the compositor?")
604
print("(Wayfire requires enabling foreign-toplevel plugin)")
605
return
606
if self.screencopy_manager is None:
607
print("Protocol zwlr_screencopy_manager_v1 not found, live (minimized) window previews will not work!")
608
self.manager.dispatcher["toplevel"] = self.on_new_toplevel
609
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished")
610
self.display.roundtrip()
611
fd = self.display.get_fd()
612
GLib.io_add_watch(fd, GLib.IO_IN, self.on_display_event)
613
614
if self.wf_socket is not None:
615
self.filter_to_wf_workspace()
616
617
def on_display_event(self, source, condition):
618
if condition & GLib.IO_IN:
619
self.display.dispatch(queue=self.event_queue)
620
return True
621
622
def set_live_preview_output(self, output):
623
self.output = output
624
625
def get_live_preview_output(self):
626
return self.output
627
628
def set_live_preview_output_name(self, name):
629
self.live_preview_output_name = name
630
631
def get_live_preview_output_name(self):
632
return self.live_preview_output_name
633
634
def wl_output_handle_name(self, output, name):
635
if name != self.get_live_preview_output_name():
636
output.release()
637
output.destroy()
638
return
639
self.set_live_preview_output(output)
640
641
def on_global(self, registry, name, interface, version):
642
if interface == "zwlr_foreign_toplevel_manager_v1":
643
self.print_log("Interface registered")
644
self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version)
645
elif interface == "wl_output":
646
if self.live_preview_output_name is None:
647
return
648
wl_output = registry.bind(name, WlOutput, version)
649
wl_output.dispatcher["name"] = self.wl_output_handle_name
650
elif interface == "wl_seat":
651
self.print_log("Seat found")
652
self.seat = registry.bind(name, WlSeat, version)
653
elif interface == "wl_shm":
654
self.wl_shm = registry.bind(name, WlShm, version)
655
elif interface == "wl_compositor":
656
self.compositor = registry.bind(name, WlCompositor, version)
657
wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface(
658
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(
659
self.get_root().get_native().get_surface().__gpointer__, None)))
660
self.wl_surface = WlSurface()
661
self.wl_surface._ptr = wl_surface_ptr
662
elif interface == "zwlr_screencopy_manager_v1":
663
self.screencopy_manager = registry.bind(name, ZwlrScreencopyManagerV1, version)
664
665
def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
666
handle: ZwlrForeignToplevelHandleV1):
667
handle.dispatcher["title"] = self.on_title_changed
668
handle.dispatcher["app_id"] = self.on_app_id_changed
669
handle.dispatcher["output_enter"] = self.on_output_entered
670
handle.dispatcher["output_leave"] = self.on_output_left
671
handle.dispatcher["state"] = self.on_state_changed
672
handle.dispatcher["closed"] = self.on_closed
673
self.display.roundtrip()
674
675
def on_output_entered(self, handle, output):
676
button = WindowButton(handle, handle.title, self)
677
button.icon.set_pixel_size(self.icon_size)
678
button.output = None
679
680
if self.show_only_this_output and output != self.my_output:
681
return
682
683
button.output = output
684
self.set_title(button, handle.title)
685
button.set_group(self.initial_button)
686
button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options))
687
button.connect("clicked", self.on_button_click)
688
self.set_app_id(button, handle.app_id)
689
self.toplevel_buttons[handle] = button
690
self.append(button)
691
self.set_all_rectangles()
692
693
def on_output_left(self, handle, output):
694
if handle in self.toplevel_buttons:
695
button = self.toplevel_buttons[handle]
696
if button.get_parent() == self:
697
button.output = None
698
self.remove(button)
699
self.set_all_rectangles()
700
701
def set_title(self, button, title):
702
button.window_title = title
703
if not self.live_preview_tooltips:
704
button.set_tooltip_text(title)
705
706
def on_title_changed(self, handle, title):
707
handle.title = title
708
if handle in self.toplevel_buttons:
709
button = self.toplevel_buttons[handle]
710
self.set_title(button, title)
711
712
def set_all_rectangles(self):
713
child = self.get_first_child()
714
while child is not None:
715
if isinstance(child, WindowButton):
716
child.window_id.set_rectangle(self.wl_surface, *get_widget_rect(child))
717
718
child = child.get_next_sibling()
719
720
def on_button_click(self, button: WindowButton):
721
# Set a rectangle for animation
722
button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button))
723
if button.window_state.minimised:
724
button.window_id.activate(self.seat)
725
else:
726
if button.window_state.focused:
727
button.window_id.set_minimized()
728
else:
729
button.window_id.activate(self.seat)
730
731
def on_state_changed(self, handle, states):
732
if handle in self.toplevel_buttons:
733
state_info = WindowState.from_state_array(states)
734
button = self.toplevel_buttons[handle]
735
button.window_state = state_info
736
if state_info.focused:
737
button.set_active(True)
738
else:
739
self.initial_button.set_active(True)
740
741
self.set_all_rectangles()
742
743
def set_app_id(self, button, app_id):
744
button.app_id = app_id
745
button.set_icon_from_app_id(app_id)
746
app_ids = app_id.split()
747
for app_id in app_ids:
748
if app_id.startswith("wf-ipc-"):
749
self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button
750
751
def on_app_id_changed(self, handle, app_id):
752
handle.app_id = app_id
753
if handle in self.toplevel_buttons:
754
button = self.toplevel_buttons[handle]
755
self.set_app_id(button, app_id)
756
757
def on_closed(self, handle):
758
if handle not in self.toplevel_buttons:
759
return
760
button: WindowButton = self.toplevel_buttons[handle]
761
wf_id = button.wf_ipc_id
762
if handle in self.toplevel_buttons:
763
self.remove(self.toplevel_buttons[handle])
764
self.toplevel_buttons.pop(handle)
765
if wf_id in self.toplevel_buttons_by_wf_id:
766
self.toplevel_buttons_by_wf_id.pop(wf_id)
767
768
self.set_all_rectangles()
769
770
def make_context_menu(self):
771
menu = Gio.Menu()
772
menu.append(_("Window list _options"), "applet.options")
773
context_menu = Gtk.PopoverMenu.new_from_model(menu)
774
context_menu.set_has_arrow(False)
775
context_menu.set_parent(self)
776
context_menu.set_halign(Gtk.Align.START)
777
context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
778
return context_menu
779
780
def show_context_menu(self, gesture, n_presses, x, y):
781
rect = Gdk.Rectangle()
782
rect.x = int(x)
783
rect.y = int(y)
784
rect.width = 1
785
rect.height = 1
786
787
self.context_menu.set_pointing_to(rect)
788
self.context_menu.popup()
789
790
def show_options(self, _0=None, _1=None):
791
if self.options_window is None:
792
self.options_window = WindowListOptions()
793
self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width)
794
self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options)
795
self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace)
796
self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter)
797
798
def reset_window(*args):
799
self.options_window = None
800
801
self.options_window.connect("close-request", reset_window)
802
self.options_window.present()
803
804
def remove_workspace_filtering(self):
805
for button in self.toplevel_buttons.values():
806
if button.get_parent() is None:
807
self.append(button)
808
809
def update_workspace_filter(self, checkbutton):
810
self.show_only_this_wf_workspace = checkbutton.get_active()
811
if checkbutton.get_active():
812
self.filter_to_wf_workspace()
813
else:
814
self.remove_workspace_filtering()
815
816
def update_button_options(self, adjustment):
817
self.window_button_options.max_width = adjustment.get_value()
818
child: Gtk.Widget = self.get_first_child()
819
while child:
820
child.queue_allocate()
821
child.queue_resize()
822
child.queue_draw()
823
child = child.get_next_sibling()
824
825
self.emit("config-changed")
826
827
def get_config(self):
828
return {
829
"max_button_width": self.window_button_options.max_width,
830
"show_only_this_wf_workspace": self.show_only_this_wf_workspace,
831
"show_only_this_output": self.show_only_this_output,
832
}
833
834
def output_changed(self):
835
self.get_wl_resources()
836
837
def make_draggable(self):
838
for button in self.toplevel_buttons.values():
839
button.remove_controller(button.drag_source)
840
button.set_sensitive(False)
841
842
def restore_drag(self):
843
for button in self.toplevel_buttons.values():
844
button.add_controller(button.drag_source)
845
button.set_sensitive(True)
846