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 • 31.26 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.popover_open = False
176
self.set_has_frame(False)
177
self.label = Gtk.Label()
178
self.icon = Gtk.Image.new_from_icon_name("application-x-executable")
179
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
180
box.append(self.icon)
181
box.append(self.label)
182
self.set_child(box)
183
184
self.window_title = window_title
185
self.window_state = WindowState(False, False, False, False)
186
187
self.label.set_ellipsize(Pango.EllipsizeMode.END)
188
self.set_hexpand(True)
189
self.set_vexpand(True)
190
191
self.drag_source = Gtk.DragSource(actions=Gdk.DragAction.MOVE)
192
self.drag_source.connect("prepare", self.provide_drag_data)
193
self.drag_source.connect("drag-begin", self.drag_begin)
194
self.drag_source.connect("drag-cancel", self.drag_cancel)
195
196
self.add_controller(self.drag_source)
197
198
self.menu = Gio.Menu()
199
# TODO: toggle the labels when needed
200
self.minimise_item = Gio.MenuItem.new(_("_Minimise"), "button.minimise")
201
self.maximise_item = Gio.MenuItem.new(_("Ma_ximise"), "button.maximise")
202
self.menu.append_item(self.minimise_item)
203
self.menu.append_item(self.maximise_item)
204
self.menu.append(_("_Close"), "button.close")
205
self.menu.append(_("Window list _options"), "applet.options")
206
self.popover_menu = Gtk.PopoverMenu.new_from_model(self.menu)
207
self.popover_menu.set_parent(self)
208
self.popover_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
209
self.popover_menu.set_has_arrow(False)
210
self.popover_menu.set_halign(Gtk.Align.END)
211
self.popover_menu.connect("closed", self.hide_menu)
212
213
self.right_click_controller = Gtk.GestureClick(button=3)
214
self.right_click_controller.connect("pressed", self.show_menu)
215
self.add_controller(self.right_click_controller)
216
217
self.action_group = Gio.SimpleActionGroup()
218
close_action = Gio.SimpleAction.new("close")
219
close_action.connect("activate", self.close_associated)
220
self.action_group.insert(close_action)
221
minimise_action = Gio.SimpleAction.new("minimise")
222
minimise_action.connect("activate", self.minimise_associated)
223
self.action_group.insert(minimise_action)
224
maximise_action = Gio.SimpleAction.new("maximise")
225
maximise_action.connect("activate", self.maximise_associated)
226
self.action_group.insert(maximise_action)
227
228
self.insert_action_group("button", self.action_group)
229
230
self.middle_click_controller = Gtk.GestureClick(button=2)
231
self.middle_click_controller.connect("released", self.close_associated)
232
self.add_controller(self.middle_click_controller)
233
234
if not self.window_list.live_preview_tooltips:
235
return
236
237
self.custom_tooltip_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
238
self.custom_tooltip_content.append(TooltipMedia(window_list))
239
self.hover_point = 0, 0
240
self.hover_timeout = None
241
self.connect("query-tooltip", self.query_tooltip)
242
self.set_has_tooltip(True)
243
motion_controller = Gtk.EventControllerMotion()
244
motion_controller.connect("enter", self.on_button_enter)
245
motion_controller.connect("leave", self.on_button_leave)
246
self.add_controller(motion_controller)
247
248
def query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
249
if self.window_list.get_live_preview_output() is None:
250
return False
251
tooltip.set_custom(self.custom_tooltip_content)
252
return True
253
254
def on_button_enter(self, button, x, y):
255
print("on_button_enter")
256
if self.popover_open:
257
return
258
view_id = self.wf_ipc_id
259
#self.window_list.set_live_preview_output_name("live-preview-" + str(view_id))
260
message = self.window_list.get_msg_template("live_previews/request_stream")
261
message["data"]["id"] = view_id
262
self.wf_sock.send_json(message)
263
264
def on_button_leave(self, button):
265
print("on_button_leave")
266
if self.popover_open:
267
return
268
message = self.window_list.get_msg_template("live_previews/release_output")
269
self.wf_sock.send_json(message)
270
self.window_list.set_live_preview_output(None)
271
#self.window_list.set_live_preview_output_name(None)
272
273
def show_menu(self, gesture, n_presses, x, y):
274
print("show_menu")
275
self.popover_open = True
276
self.popover_menu.popup()
277
278
def hide_menu(self, popover):
279
print("hide_menu")
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.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
584
if self.wl_shm:
585
return
586
587
self.my_output = None
588
#self.wl_surface = self.get_root().get_application().wl_surface
589
self.compositor = self.get_root().get_application().compositor
590
self.seat = self.get_root().get_application().seat
591
self.output = None
592
self.wl_shm = self.get_root().get_application().wl_shm
593
#self.manager = self.get_root().get_application().manager
594
self.screencopy_manager = self.get_root().get_application().screencopy_manager
595
self.live_preview_output_name = None
596
597
if self.wf_socket is not None:
598
self.filter_to_wf_workspace()
599
600
def set_live_preview_output(self, output):
601
self.output = output
602
603
def get_live_preview_output(self):
604
return self.output
605
606
#def set_live_preview_output_name(self, name):
607
# self.live_preview_output_name = name
608
#
609
#def get_live_preview_output_name(self):
610
# return self.live_preview_output_name
611
612
def wl_output_enter(self, output, name):
613
print("wl_output_enter")
614
self.set_live_preview_output(None)
615
if name.startswith("live-preview"):
616
self.set_live_preview_output(output)
617
return
618
if name.startswith("live-preview"):
619
return
620
if not self.my_output and name == self.get_root().monitor_name:
621
self.wl_surface = None
622
if self.get_root().get_native().get_surface():
623
wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface(
624
ffi.cast("void *", ctypes.pythonapi.PyCapsule_GetPointer(
625
self.get_root().get_native().get_surface().__gpointer__, None)))
626
self.wl_surface = WlSurface()
627
self.wl_surface._ptr = wl_surface_ptr
628
self.my_output = output
629
else:
630
toplevel_buttons = self.toplevel_buttons.copy()
631
for handle in toplevel_buttons:
632
button = self.toplevel_buttons[handle]
633
if output != button.output:
634
self.foreign_toplevel_closed(handle)
635
636
def wl_output_leave(self, output, name):
637
print("wl_output_leave")
638
if self.get_live_preview_output() == output:
639
self.set_live_preview_output(None)
640
return
641
if name.startswith("live-preview"):
642
return
643
toplevel_buttons = self.toplevel_buttons.copy()
644
for handle in toplevel_buttons:
645
self.foreign_toplevel_closed(handle)
646
647
def foreign_toplevel_output_enter(self, handle, output):
648
print("foreign_toplevel_output_enter")
649
650
if self.show_only_this_output and (not hasattr(output, "name") or output.name != self.get_root().monitor_name):
651
return
652
653
button = WindowButton(handle, handle.title, self)
654
button.icon.set_pixel_size(self.icon_size)
655
button.output = output
656
self.set_title(button, handle.title)
657
button.set_group(self.initial_button)
658
button.set_layout_manager(WindowButtonLayoutManager(self.window_button_options))
659
button.connect("clicked", self.on_button_click)
660
self.set_app_id(button, handle.app_id)
661
self.toplevel_buttons[handle] = button
662
self.append(button)
663
self.set_all_rectangles()
664
665
def foreign_toplevel_output_leave(self, handle, output):
666
print("foreign_toplevel_output_leave")
667
if handle in self.toplevel_buttons:
668
button = self.toplevel_buttons[handle]
669
if button.get_parent() == self:
670
button.output = None
671
self.remove(button)
672
self.set_all_rectangles()
673
674
def set_title(self, button, title):
675
button.window_title = title
676
if not self.live_preview_tooltips:
677
button.set_tooltip_text(title)
678
679
def foreign_toplevel_title(self, handle, title):
680
handle.title = title
681
if handle in self.toplevel_buttons:
682
button = self.toplevel_buttons[handle]
683
self.set_title(button, title)
684
685
def set_all_rectangles(self):
686
child = self.get_first_child()
687
while child is not None and self.wl_surface:
688
if isinstance(child, WindowButton):
689
x, y, w, h = get_widget_rect(child)
690
if w > 0 and h > 0:
691
child.window_id.set_rectangle(self.wl_surface, x, y, w, h)
692
693
child = child.get_next_sibling()
694
695
def on_button_click(self, button: WindowButton):
696
# Set a rectangle for animation
697
button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button))
698
if button.window_state.minimised:
699
button.window_id.activate(self.seat)
700
else:
701
if button.window_state.focused:
702
button.window_id.set_minimized()
703
else:
704
button.window_id.activate(self.seat)
705
706
def foreign_toplevel_state(self, handle, states):
707
if handle in self.toplevel_buttons:
708
state_info = WindowState.from_state_array(states)
709
button = self.toplevel_buttons[handle]
710
button.window_state = state_info
711
if state_info.focused:
712
button.set_active(True)
713
else:
714
self.initial_button.set_active(True)
715
716
self.set_all_rectangles()
717
718
def foreign_toplevel_refresh(self):
719
for button in self.toplevel_buttons.values():
720
self.remove(button)
721
722
self.toplevel_buttons.clear()
723
self.toplevel_buttons_by_wf_id.clear()
724
725
def set_app_id(self, button, app_id):
726
button.app_id = app_id
727
button.set_icon_from_app_id(app_id)
728
app_ids = app_id.split()
729
for app_id in app_ids:
730
if app_id.startswith("wf-ipc-"):
731
self.toplevel_buttons_by_wf_id[int(app_id.removeprefix("wf-ipc-"))] = button
732
733
def foreign_toplevel_app_id(self, handle, app_id):
734
handle.app_id = app_id
735
if handle in self.toplevel_buttons:
736
button = self.toplevel_buttons[handle]
737
self.set_app_id(button, app_id)
738
739
def foreign_toplevel_closed(self, handle):
740
if handle not in self.toplevel_buttons:
741
return
742
button: WindowButton = self.toplevel_buttons[handle]
743
wf_id = button.wf_ipc_id
744
if handle in self.toplevel_buttons:
745
self.remove(self.toplevel_buttons[handle])
746
self.toplevel_buttons.pop(handle)
747
if wf_id in self.toplevel_buttons_by_wf_id:
748
self.toplevel_buttons_by_wf_id.pop(wf_id)
749
750
self.set_all_rectangles()
751
752
def make_context_menu(self):
753
menu = Gio.Menu()
754
menu.append(_("Window list _options"), "applet.options")
755
context_menu = Gtk.PopoverMenu.new_from_model(menu)
756
context_menu.set_has_arrow(False)
757
context_menu.set_parent(self)
758
context_menu.set_halign(Gtk.Align.START)
759
context_menu.set_flags(Gtk.PopoverMenuFlags.NESTED)
760
return context_menu
761
762
def show_context_menu(self, gesture, n_presses, x, y):
763
rect = Gdk.Rectangle()
764
rect.x = int(x)
765
rect.y = int(y)
766
rect.width = 1
767
rect.height = 1
768
769
self.context_menu.set_pointing_to(rect)
770
self.context_menu.popup()
771
772
def show_options(self, _0=None, _1=None):
773
if self.options_window is None:
774
self.options_window = WindowListOptions()
775
self.options_window.button_width_adjustment.set_value(self.window_button_options.max_width)
776
self.options_window.button_width_adjustment.connect("value-changed", self.update_button_options)
777
self.options_window.workspace_filter_checkbutton.set_active(self.show_only_this_wf_workspace)
778
self.options_window.workspace_filter_checkbutton.connect("toggled", self.update_workspace_filter)
779
780
def reset_window(*args):
781
self.options_window = None
782
783
self.options_window.connect("close-request", reset_window)
784
self.options_window.present()
785
786
def remove_workspace_filtering(self):
787
for button in self.toplevel_buttons.values():
788
if button.get_parent() is None:
789
self.append(button)
790
791
def update_workspace_filter(self, checkbutton):
792
self.show_only_this_wf_workspace = checkbutton.get_active()
793
if checkbutton.get_active():
794
self.filter_to_wf_workspace()
795
else:
796
self.remove_workspace_filtering()
797
798
def update_button_options(self, adjustment):
799
self.window_button_options.max_width = adjustment.get_value()
800
child: Gtk.Widget = self.get_first_child()
801
while child:
802
child.queue_allocate()
803
child.queue_resize()
804
child.queue_draw()
805
child = child.get_next_sibling()
806
807
self.emit("config-changed")
808
809
def get_config(self):
810
return {
811
"max_button_width": self.window_button_options.max_width,
812
"show_only_this_wf_workspace": self.show_only_this_wf_workspace,
813
"show_only_this_output": self.show_only_this_output,
814
}
815
816
def output_changed(self):
817
self.get_wl_resources()
818
819
def make_draggable(self):
820
for button in self.toplevel_buttons.values():
821
button.remove_controller(button.drag_source)
822
button.set_sensitive(False)
823
824
def restore_drag(self):
825
for button in self.toplevel_buttons.values():
826
button.add_controller(button.drag_source)
827
button.set_sensitive(True)
828