gtk: request user attention on bell (#7482)

I'm not sure if this should be enabled by default like the tab
animation, but on KDE at least this is unintrusive enough for me to
always enable by default. Alacritty appears to agree with me as well.

Fixes #7124
This commit is contained in:
Leah Amelia Chen
2025-05-30 23:33:12 +02:00
committed by GitHub
7 changed files with 95 additions and 24 deletions

View File

@@ -2447,6 +2447,11 @@ pub fn ringBell(self: *Surface) !void {
// Need attention if we're not the currently selected tab // Need attention if we're not the currently selected tab
if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true)); if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true));
} }
// Request user attention
window.winproto.setUrgent(true) catch |err| {
log.err("failed to request user attention={}", .{err});
};
} }
/// Handle a stream that is in an error state. /// Handle a stream that is in an error state.

View File

@@ -816,11 +816,15 @@ fn gtkWindowNotifyIsActive(
_: *gobject.ParamSpec, _: *gobject.ParamSpec,
self: *Window, self: *Window,
) callconv(.c) void { ) callconv(.c) void {
if (!self.isQuickTerminal()) return; self.winproto.setUrgent(false) catch |err| {
log.err("failed to unrequest user attention={}", .{err});
};
// Hide when we're unfocused if (self.isQuickTerminal()) {
if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) { // Hide when we're unfocused
self.toggleVisibility(); if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) {
self.toggleVisibility();
}
} }
} }

View File

@@ -146,4 +146,10 @@ pub const Window = union(Protocol) {
inline else => |*v| try v.addSubprocessEnv(env), inline else => |*v| try v.addSubprocessEnv(env),
} }
} }
pub fn setUrgent(self: *Window, urgent: bool) !void {
switch (self.*) {
inline else => |*v| try v.setUrgent(urgent),
}
}
}; };

View File

@@ -70,4 +70,6 @@ pub const Window = struct {
} }
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {} pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
pub fn setUrgent(_: *Window, _: bool) !void {}
}; };

View File

@@ -16,6 +16,7 @@ const ApprtWindow = @import("../Window.zig");
const wl = wayland.client.wl; const wl = wayland.client.wl;
const org = wayland.client.org; const org = wayland.client.org;
const xdg = wayland.client.xdg;
const log = std.log.scoped(.winproto_wayland); const log = std.log.scoped(.winproto_wayland);
@@ -34,6 +35,8 @@ pub const App = struct {
kde_slide_manager: ?*org.KdeKwinSlideManager = null, kde_slide_manager: ?*org.KdeKwinSlideManager = null,
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
xdg_activation: ?*xdg.ActivationV1 = null,
}; };
pub fn init( pub fn init(
@@ -150,6 +153,15 @@ pub const App = struct {
context.kde_slide_manager = slide_manager; context.kde_slide_manager = slide_manager;
return; return;
} }
if (registryBind(
xdg.ActivationV1,
registry,
global,
)) |activation| {
context.xdg_activation = activation;
return;
}
}, },
// We don't handle removal events // We don't handle removal events
@@ -207,15 +219,19 @@ pub const Window = struct {
app_context: *App.Context, app_context: *App.Context,
/// A token that, when present, indicates that the window is blurred. /// A token that, when present, indicates that the window is blurred.
blur_token: ?*org.KdeKwinBlur, blur_token: ?*org.KdeKwinBlur = null,
/// Object that controls the decoration mode (client/server/auto) /// Object that controls the decoration mode (client/server/auto)
/// of the window. /// of the window.
decoration: ?*org.KdeKwinServerDecoration, decoration: ?*org.KdeKwinServerDecoration = null,
/// Object that controls the slide-in/slide-out animations of the /// Object that controls the slide-in/slide-out animations of the
/// quick terminal. Always null for windows other than the quick terminal. /// quick terminal. Always null for windows other than the quick terminal.
slide: ?*org.KdeKwinSlide, slide: ?*org.KdeKwinSlide = null,
/// Object that, when present, denotes that the window is currently
/// requesting attention from the user.
activation_token: ?*xdg.ActivationTokenV1 = null,
pub fn init( pub fn init(
alloc: Allocator, alloc: Allocator,
@@ -268,9 +284,7 @@ pub const Window = struct {
.apprt_window = apprt_window, .apprt_window = apprt_window,
.surface = wl_surface, .surface = wl_surface,
.app_context = app.context, .app_context = app.context,
.blur_token = null,
.decoration = deco, .decoration = deco,
.slide = null,
}; };
} }
@@ -315,6 +329,21 @@ pub const Window = struct {
_ = env; _ = env;
} }
pub fn setUrgent(self: *Window, urgent: bool) !void {
const activation = self.app_context.xdg_activation orelse return;
// If there already is a token, destroy and unset it
if (self.activation_token) |token| token.destroy();
self.activation_token = if (urgent) token: {
const token = try activation.getActivationToken();
token.setSurface(self.surface);
token.setListener(*Window, onActivationTokenEvent, self);
token.commit();
break :token token;
} else null;
}
/// Update the blur state of the window. /// Update the blur state of the window.
fn syncBlur(self: *Window) !void { fn syncBlur(self: *Window) !void {
const manager = self.app_context.kde_blur_manager orelse return; const manager = self.app_context.kde_blur_manager orelse return;
@@ -440,4 +469,26 @@ pub const Window = struct {
window.setDefaultSize(@intCast(dims.width), @intCast(dims.height)); window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
} }
fn onActivationTokenEvent(
token: *xdg.ActivationTokenV1,
event: xdg.ActivationTokenV1.Event,
self: *Window,
) void {
const activation = self.app_context.xdg_activation orelse return;
const current_token = self.activation_token orelse return;
if (token.getId() != current_token.getId()) {
log.warn("received event for unknown activation token; ignoring", .{});
return;
}
switch (event) {
.done => |done| {
activation.activate(done.token, self.surface);
token.destroy();
self.activation_token = null;
},
}
}
}; };

View File

@@ -176,8 +176,8 @@ pub const App = struct {
pub const Window = struct { pub const Window = struct {
app: *App, app: *App,
config: *const ApprtWindow.DerivedConfig, config: *const ApprtWindow.DerivedConfig,
window: xlib.Window,
gtk_window: *adw.ApplicationWindow, gtk_window: *adw.ApplicationWindow,
x11_surface: *gdk_x11.X11Surface,
blur_region: Region = .{}, blur_region: Region = .{},
@@ -192,13 +192,6 @@ pub const Window = struct {
gtk.Native, gtk.Native,
).getSurface() orelse return error.NotX11Surface; ).getSurface() orelse return error.NotX11Surface;
// Check if we're actually on X11
if (gobject.typeCheckInstanceIsA(
surface.as(gobject.TypeInstance),
gdk_x11.X11Surface.getGObjectType(),
) == 0)
return error.NotX11Surface;
const x11_surface = gobject.ext.cast( const x11_surface = gobject.ext.cast(
gdk_x11.X11Surface, gdk_x11.X11Surface,
surface, surface,
@@ -207,8 +200,8 @@ pub const Window = struct {
return .{ return .{
.app = app, .app = app,
.config = &apprt_window.config, .config = &apprt_window.config,
.window = x11_surface.getXid(),
.gtk_window = apprt_window.window, .gtk_window = apprt_window.window,
.x11_surface = x11_surface,
}; };
} }
@@ -279,7 +272,7 @@ pub const Window = struct {
const blur = self.config.background_blur; const blur = self.config.background_blur;
log.debug("set blur={}, window xid={}, region={}", .{ log.debug("set blur={}, window xid={}, region={}", .{
blur, blur,
self.window, self.x11_surface.getXid(),
self.blur_region, self.blur_region,
}); });
@@ -335,11 +328,19 @@ pub const Window = struct {
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void { pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
var buf: [64]u8 = undefined; var buf: [64]u8 = undefined;
const window_id = try std.fmt.bufPrint(&buf, "{}", .{self.window}); const window_id = try std.fmt.bufPrint(
&buf,
"{}",
.{self.x11_surface.getXid()},
);
try env.put("WINDOWID", window_id); try env.put("WINDOWID", window_id);
} }
pub fn setUrgent(self: *Window, urgent: bool) !void {
self.x11_surface.setUrgencyHint(@intFromBool(urgent));
}
fn getWindowProperty( fn getWindowProperty(
self: *Window, self: *Window,
comptime T: type, comptime T: type,
@@ -363,7 +364,7 @@ pub const Window = struct {
const code = c.XGetWindowProperty( const code = c.XGetWindowProperty(
@ptrCast(@alignCast(self.app.display)), @ptrCast(@alignCast(self.app.display)),
self.window, self.x11_surface.getXid(),
name, name,
options.offset, options.offset,
options.length, options.length,
@@ -401,7 +402,7 @@ pub const Window = struct {
const status = c.XChangeProperty( const status = c.XChangeProperty(
@ptrCast(@alignCast(self.app.display)), @ptrCast(@alignCast(self.app.display)),
self.window, self.x11_surface.getXid(),
name, name,
typ, typ,
@intFromEnum(format), @intFromEnum(format),
@@ -419,7 +420,7 @@ pub const Window = struct {
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void { fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
const status = c.XDeleteProperty( const status = c.XDeleteProperty(
@ptrCast(@alignCast(self.app.display)), @ptrCast(@alignCast(self.app.display)),
self.window, self.x11_surface.getXid(),
name, name,
); );
if (status == 0) return error.RequestFailed; if (status == 0) return error.RequestFailed;

View File

@@ -609,21 +609,23 @@ fn addGTK(
.wayland_protocols = wayland_protocols_dep.path(""), .wayland_protocols = wayland_protocols_dep.path(""),
}); });
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
scanner.addCustomProtocol( scanner.addCustomProtocol(
plasma_wayland_protocols_dep.path("src/protocols/blur.xml"), plasma_wayland_protocols_dep.path("src/protocols/blur.xml"),
); );
// FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
scanner.addCustomProtocol( scanner.addCustomProtocol(
plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"), plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"),
); );
scanner.addCustomProtocol( scanner.addCustomProtocol(
plasma_wayland_protocols_dep.path("src/protocols/slide.xml"), plasma_wayland_protocols_dep.path("src/protocols/slide.xml"),
); );
scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml");
scanner.generate("wl_compositor", 1); scanner.generate("wl_compositor", 1);
scanner.generate("org_kde_kwin_blur_manager", 1); scanner.generate("org_kde_kwin_blur_manager", 1);
scanner.generate("org_kde_kwin_server_decoration_manager", 1); scanner.generate("org_kde_kwin_server_decoration_manager", 1);
scanner.generate("org_kde_kwin_slide_manager", 1); scanner.generate("org_kde_kwin_slide_manager", 1);
scanner.generate("xdg_activation_v1", 1);
step.root_module.addImport("wayland", b.createModule(.{ step.root_module.addImport("wayland", b.createModule(.{
.root_source_file = scanner.result, .root_source_file = scanner.result,