diff --git a/src/apprt/gtk/class/window.zig b/src/apprt/gtk/class/window.zig index 0c8dfaa7c..2a1a5435a 100644 --- a/src/apprt/gtk/class/window.zig +++ b/src/apprt/gtk/class/window.zig @@ -220,6 +220,9 @@ pub const Window = extern struct { /// behaves slightly differently under certain scenarios. quick_terminal: bool = false, + /// Timeout source to react to this window becoming (in)active. + handle_active_state_source: ?c_uint = null, + /// The window decoration override. If this is not set then we'll /// inherit whatever the config has. This allows overriding the /// config on a per-window basis. @@ -855,6 +858,38 @@ pub const Window = extern struct { } } + /// Callback to handle this window becoming active or inactive. + /// Triggered by propIsActive with a timeout to debounce temporary + /// changes in active state. + fn handleActiveState(ud: ?*anyopaque) callconv(.c) c_int { + const self: *Self = @ptrCast(@alignCast(ud orelse return 0)); + const priv = self.private(); + priv.handle_active_state_source = null; + + // Hide quick-terminal if set to autohide + if (self.isQuickTerminal()) { + if (self.getConfig()) |cfg| { + if (cfg.get().@"quick-terminal-autohide" and + self.as(gtk.Window).isActive() == 0 and + self.as(gtk.Widget).isVisible() == 1) + { + self.toggleVisibility(); + } + } + } + + // Don't change urgency if we're not the active window. + if (self.as(gtk.Window).isActive() == 0) return 0; + + self.winproto().setUrgent(false) catch |err| { + log.warn( + "winproto failed to reset urgency={}", + .{err}, + ); + }; + return 0; + } + //--------------------------------------------------------------- // Properties @@ -1076,27 +1111,34 @@ pub const Window = extern struct { _: *gobject.ParamSpec, self: *Self, ) callconv(.c) void { - // Hide quick-terminal if set to autohide - if (self.isQuickTerminal()) { - if (self.getConfig()) |cfg| { - if (cfg.get().@"quick-terminal-autohide" and - self.as(gtk.Window).isActive() == 0 and - self.as(gtk.Widget).isVisible() == 1) - { - self.toggleVisibility(); - } - } - } + const priv = self.private(); - // Don't change urgency if we're not the active window. - if (self.as(gtk.Window).isActive() == 0) return; - - self.winproto().setUrgent(false) catch |err| { - log.warn( - "winproto failed to reset urgency={}", - .{err}, + // Use a timeout callback to wait for focus state to settle, + // because depending on the windowing backend the window might + // become inactive and immediately active again. This happens + // e.g. on Wayland when opening a context menu or a submenu + // inside a context menu. + if (priv.handle_active_state_source == null) { + priv.handle_active_state_source = glib.timeoutAddFull( + // Use priority of an idle callback instead of the higher + // default timeout priority. This allows us to use a shorter + // timeout duration. + glib.PRIORITY_DEFAULT_IDLE, + // 50ms was chosen to be conservative. From testing we know + // that, depending on the backend and system performance, a + // shorter timeout or just an idle callback can be enough for + // the focus to settle. On the other hand a delay of e.g. 10ms + // does not work reliably on some slow systems. The downside + // of a high value is that some operations in handleActiveState, + // e.g. hiding the quick-terminal, will be visibly delayed. + // However, 50ms should barely be noticeable. We can change + // this in the future if necessary. + 50, + handleActiveState, + self, + null, ); - }; + } } fn propGdkSurfaceDims( @@ -1215,6 +1257,13 @@ pub const Window = extern struct { fn dispose(self: *Self) callconv(.c) void { const priv = self.private(); + if (priv.handle_active_state_source) |v| { + if (glib.Source.remove(v) == 0) { + log.warn("unable to remove handle active state source", .{}); + } + priv.handle_active_state_source = null; + } + priv.command_palette.set(null); if (priv.config) |v| {