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