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 mmapimport locale import typing from pathlib import Path from pywayland.utils import AnonymousFilefrom pywayland.client import Display, EventQueue from pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutputfrom pywayland.protocol.wayland import WlRegistry, WlSeat, WlSurface, WlCompositor, WlOutput, WlBuffer, WlShm, WlShmPoolfrom 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 GLimport 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_listself.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 = Falsetry: import wayfire from wayfire.core.template import get_msg_templateself.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 = Trueself.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 = Noneself.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_nameelif 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 windowbutton.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_idbutton.set_icon_from_app_id(app_id) app_ids = app_id.split() for app_id in app_ids: