roundabout,
created on Friday, 22 August 2025, 04:54:59 (1755838499),
received on Saturday, 23 August 2025, 10:54:39 (1755946479)
Author identity: Scott Moreau <oreaus@gmail.com>
1ffb702dcf2b2285593e4bc936ba3699be86ac7b
applets/wf-window-list/__init__.py
@@ -20,16 +20,23 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import dataclasses
import os
import sys
import mmap
import locale
import typing
from pathlib import Path
from pywayland.utils import AnonymousFile
from pywayland.client import Display, EventQueue
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput
from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput, WlBuffer, WlShm, WlShmPool
from pywayland.protocol.wayland.wl_output import WlOutputProxy
from pywayland.protocol.wlr_foreign_toplevel_management_unstable_v1 import (
ZwlrForeignToplevelManagerV1,
ZwlrForeignToplevelHandleV1
)
from pywayland.protocol.wlr_screencopy_unstable_v1 import (
ZwlrScreencopyManagerV1,
ZwlrScreencopyFrameV1
)
from OpenGL import GL
import panorama_panel
import gi
@@ -158,10 +165,12 @@ class WindowButtonLayoutManager(Gtk.LayoutManager):
class WindowButton(Gtk.ToggleButton):
def __init__(self, window_id, window_title, **kwargs):
def __init__(self, window_id, window_title, window_list, **kwargs):
super().__init__(**kwargs)
self.window_id: ZwlrForeignToplevelHandleV1 = window_id
self.wf_sock = window_list.wf_socket
self.window_list = window_list
self.wf_ipc_id: typing.Optional[int] = None
self.set_has_frame(False)
self.label = Gtk.Label()
@@ -220,6 +229,35 @@ class WindowButton(Gtk.ToggleButton):
self.middle_click_controller.connect("released", self.close_associated)
self.add_controller(self.middle_click_controller)
if not self.window_list.live_preview_tooltips:
return
self.custom_tooltip_content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.custom_tooltip_content.append(TooltipMedia(window_list))
self.hover_point = 0, 0
self.hover_timeout = None
self.connect("query-tooltip", self.query_tooltip)
self.set_has_tooltip(True)
motion_controller = Gtk.EventControllerMotion()
motion_controller.connect("enter", self.on_button_enter)
motion_controller.connect("leave", self.on_button_leave)
self.add_controller(motion_controller)
def query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
tooltip.set_custom(self.custom_tooltip_content)
return True
def on_button_enter(self, button, x, y):
view_id = self.wf_ipc_id
self.window_list.set_live_preview_output_name("live-preview-" + str(view_id))
message = self.window_list.get_msg_template("live_previews/request_stream")
message["data"]["id"] = view_id
self.wf_sock.send_json(message)
def on_button_leave(self, button):
message = self.window_list.get_msg_template("live_previews/release_output")
self.wf_sock.send_json(message)
def show_menu(self, gesture, n_presses, x, y):
rect = Gdk.Rectangle()
rect.x = int(x)
@@ -296,6 +334,133 @@ class WindowButton(Gtk.ToggleButton):
# Due to a bug, the constructor may sometimes return C NULL
pass
class TooltipMedia(Gtk.GLArea):
def __init__(self, window_list):
super().__init__()
self.frame: ZwlrScreencopyFrameV1 = None
self.screencopy_manager = window_list.screencopy_manager
self.wl_shm = window_list.wl_shm
self.window_list = window_list
self.set_auto_render(False)
self.connect("render", self.on_render)
self.buffer_width = 200
self.buffer_height = 200
self.screencopy_data = None
self.wl_buffer = None
self.size = 0
self.add_tick_callback(self.on_tick)
self.vertices = [
1.0, 1.0,
-1.0, 1.0,
-1.0, -1.0,
1.0, -1.0,
]
self.uv_coords = [
1.0, 0.0,
0.0, 0.0,
0.0, 1.0,
1.0, 1.0,
]
vertex_shader_source = """
precision highp float;
attribute vec2 in_position;
attribute vec2 uvpos;
varying vec2 uv;
void main() {
uv = uvpos;
gl_Position = vec4(in_position, 0.0, 1.0);
}
"""
fragment_shader_source = """
precision highp float;
uniform sampler2D texture;
varying vec2 uv;
void main() {
gl_FragColor = texture2D(texture, uv);
}
"""
vertex_shader = GL.glCreateShader(GL.GL_VERTEX_SHADER)
fragment_shader = GL.glCreateShader(GL.GL_FRAGMENT_SHADER)
GL.glShaderSource(vertex_shader, vertex_shader_source)
GL.glShaderSource(fragment_shader, fragment_shader_source)
GL.glCompileShader(vertex_shader)
GL.glCompileShader(fragment_shader)
self.shader_program = GL.glCreateProgram()
GL.glAttachShader(self.shader_program, vertex_shader)
GL.glAttachShader(self.shader_program, fragment_shader)
GL.glLinkProgram(self.shader_program)
self.texture_id = GL.glGenTextures(1)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture_id)
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
GL.glTexParameterf(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
self.request_next_frame()
def frame_handle_buffer(self, frame, pixel_format, width, height, stride):
size = width * height * int(stride / width)
if self.size != size:
self.set_size_request(width, height)
self.size = size
anon_file = AnonymousFile(self.size)
anon_file.open()
fd = anon_file.fd
self.screencopy_data = mmap.mmap(fileno=fd, length=self.size, access=(mmap.ACCESS_READ | mmap.ACCESS_WRITE), offset=0)
pool = self.wl_shm.create_pool(fd, self.size)
self.wl_buffer = pool.create_buffer(0, width, height, stride, pixel_format)
pool.destroy()
anon_file.close()
self.buffer_width = width
self.buffer_height = height
self.frame.copy(self.wl_buffer)
def frame_ready(self, frame, tv_sec_hi, tv_sec_low, tv_nsec):
self.screencopy_data.seek(0)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture_id)
GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, self.buffer_width, self.buffer_height, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, self.screencopy_data.read(self.size))
self.request_next_frame()
def request_next_frame(self):
if self.frame:
self.frame.destroy()
self.queue_render()
wl_output = self.window_list.get_live_preview_output()
if not wl_output:
return
self.frame = self.screencopy_manager.capture_output(1, wl_output)
self.frame.dispatcher["buffer"] = self.frame_handle_buffer
self.frame.dispatcher["ready"] = self.frame_ready
def on_render(self, gl_area, gl_context):
Gdk.GLContext.make_current(gl_context)
GL.glClear(GL.GL_COLOR_BUFFER_BIT)
GL.glUseProgram(self.shader_program)
GL.glBindTexture(GL.GL_TEXTURE_2D, self.texture_id)
GL.glEnableVertexAttribArray(0);
GL.glVertexAttribPointer(0, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, self.vertices);
GL.glEnableVertexAttribArray(1);
GL.glVertexAttribPointer(1, 2, GL.GL_FLOAT, GL.GL_FALSE, 0, self.uv_coords);
GL.glDrawArrays(GL.GL_TRIANGLE_FAN, 0, 4);
GL.glDisableVertexAttribArray(0);
GL.glDisableVertexAttribArray(1);
GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
gl_area.queue_draw()
def on_tick(self, widget, frame_clock):
self.request_next_frame()
return GLib.SOURCE_CONTINUE
class WFWindowList(panorama_panel.Applet):
name = _("Wayfire window list")
@@ -352,9 +517,14 @@ class WFWindowList(panorama_panel.Applet):
self.add_controller(self.drop_target)
# Make a Wayfire socket for workspace handling
self.live_preview_tooltips = False
try:
import wayfire
from wayfire.core.template import get_msg_template
self.wf_socket = wayfire.WayfireSocket()
self.get_msg_template = get_msg_template
if "live_previews/request_stream" in self.wf_socket.list_methods():
self.live_preview_tooltips = True
self.wf_socket.watch(["view-workspace-changed", "wset-workspace-changed"])
fd = self.wf_socket.client.fileno()
GLib.io_add_watch(GLib.IOChannel.unix_new(fd), GLib.IO_IN, self.on_wf_event, priority=GLib.PRIORITY_HIGH)
@@ -479,7 +649,11 @@ class WFWindowList(panorama_panel.Applet):
self.my_output = monitor.output_proxy
# End hack
self.output = None
self.wl_shm = None
self.manager = None
self.screencopy_manager = None
self.live_preview_output_name = None
self.registry = self.display.get_registry()
self.registry.dispatcher["global"] = self.on_global
self.display.roundtrip()
@@ -487,6 +661,8 @@ class WFWindowList(panorama_panel.Applet):
print("Could not load wf-window-list. Is foreign-toplevel protocol advertised by the compositor?")
print("(Wayfire requires enabling foreign-toplevel plugin)")
return
if self.screencopy_manager is None:
print("Protocol zwlr_screencopy_manager_v1 not found, live (minimized) window previews will not work!")
self.manager.dispatcher["toplevel"] = self.on_new_toplevel
self.manager.dispatcher["finished"] = lambda *a: print("Toplevel manager finished")
self.display.roundtrip()
@@ -501,13 +677,39 @@ class WFWindowList(panorama_panel.Applet):
self.display.dispatch(queue=self.event_queue)
return True
def set_live_preview_output(self, output):
self.output = output
def get_live_preview_output(self):
return self.output
def set_live_preview_output_name(self, name):
self.live_preview_output_name = name
def get_live_preview_output_name(self):
return self.live_preview_output_name
def wl_output_handle_name(self, output, name):
if name != self.get_live_preview_output_name():
output.release()
output.destroy()
return
self.set_live_preview_output(output)
def on_global(self, registry, name, interface, version):
if interface == "zwlr_foreign_toplevel_manager_v1":
self.print_log("Interface registered")
self.manager = registry.bind(name, ZwlrForeignToplevelManagerV1, version)
elif interface == "wl_output":
if self.live_preview_output_name is None:
return
wl_output = registry.bind(name, WlOutput, version)
wl_output.dispatcher["name"] = self.wl_output_handle_name
elif interface == "wl_seat":
self.print_log("Seat found")
self.seat = registry.bind(name, WlSeat, version)
elif interface == "wl_shm":
self.wl_shm = registry.bind(name, WlShm, version)
elif interface == "wl_compositor":
self.compositor = registry.bind(name, WlCompositor, version)
wl_surface_ptr = gtk.gdk_wayland_surface_get_wl_surface(
@@ -515,6 +717,8 @@ class WFWindowList(panorama_panel.Applet):
self.get_root().get_native().get_surface().__gpointer__, None)))
self.wl_surface = WlSurface()
self.wl_surface._ptr = wl_surface_ptr
elif interface == "zwlr_screencopy_manager_v1":
self.screencopy_manager = registry.bind(name, ZwlrScreencopyManagerV1, version)
def on_new_toplevel(self, manager: ZwlrForeignToplevelManagerV1,
handle: ZwlrForeignToplevelHandleV1):
@@ -524,9 +728,10 @@ class WFWindowList(panorama_panel.Applet):
handle.dispatcher["output_leave"] = self.on_output_left
handle.dispatcher["state"] = self.on_state_changed
handle.dispatcher["closed"] = self.on_closed
self.display.roundtrip()
def on_output_entered(self, handle, output):
button = WindowButton(handle, handle.title)
button = WindowButton(handle, handle.title, self)
button.icon.set_pixel_size(self.icon_size)
button.output = None
@@ -553,7 +758,8 @@ class WFWindowList(panorama_panel.Applet):
def set_title(self, button, title):
button.window_title = title
button.set_tooltip_text(title)
if not self.live_preview_tooltips:
button.set_tooltip_text(title)
def on_title_changed(self, handle, title):
handle.title = title
@@ -572,12 +778,13 @@ class WFWindowList(panorama_panel.Applet):
def on_button_click(self, button: WindowButton):
# Set a rectangle for animation
button.window_id.set_rectangle(self.wl_surface, *get_widget_rect(button))
if button.window_state.focused:
# Already pressed in, so minimise the focused window
button.window_id.set_minimized()
else:
button.window_id.unset_minimized()
if button.window_state.minimised:
button.window_id.activate(self.seat)
else:
if button.window_state.focused:
button.window_id.set_minimized()
else:
button.window_id.activate(self.seat)
def on_state_changed(self, handle, states):
if handle in self.toplevel_buttons:
@@ -592,6 +799,7 @@ class WFWindowList(panorama_panel.Applet):
self.set_all_rectangles()
def set_app_id(self, button, app_id):
button.app_id = app_id
button.set_icon_from_app_id(app_id)
app_ids = app_id.split()
for app_id in app_ids: