diff --git a/src/App.zig b/src/App.zig index 6a4a7a546..599265a44 100644 --- a/src/App.zig +++ b/src/App.zig @@ -288,6 +288,9 @@ pub fn setQuit(self: *App) !void { /// This is separate from surface focus events. See the `focused` /// field for more information. pub fn focusEvent(self: *App, focused: bool) void { + // Prevent redundant focus events + if (self.focused == focused) return; + log.debug("focus event focused={}", .{focused}); self.focused = focused; } diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 2e0bbad84..3bed756d0 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -97,7 +97,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { c.gtk_get_minor_version(), c.gtk_get_micro_version(), }); - + if (version.atLeast(4, 16, 0)) { // From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE _ = internal_os.setenv("GDK_DISABLE", "gles-api"); @@ -235,6 +235,24 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { c.G_CONNECT_DEFAULT, ); + // Other signals + _ = c.g_signal_connect_data( + app, + "window-added", + c.G_CALLBACK(>kWindowAdded), + core_app, + null, + c.G_CONNECT_DEFAULT, + ); + _ = c.g_signal_connect_data( + app, + "window-removed", + c.G_CALLBACK(>kWindowRemoved), + core_app, + null, + c.G_CONNECT_DEFAULT, + ); + // We don't use g_application_run, we want to manually control the // loop so we have to do the same things the run function does: // https://github.com/GNOME/glib/blob/a8e8b742e7926e33eb635a8edceac74cf239d6ed/gio/gapplication.c#L2533 @@ -1065,6 +1083,72 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void { }, .{ .forever = {} }); } +fn gtkWindowAdded( + _: *c.GtkApplication, + window: *c.GtkWindow, + ud: ?*anyopaque, +) callconv(.C) void { + const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); + + // Request the is-active property change so we can detect + // when our app loses focus. + _ = c.g_signal_connect_data( + window, + "notify::is-active", + c.G_CALLBACK(>kWindowIsActive), + core_app, + null, + c.G_CONNECT_DEFAULT, + ); +} + +fn gtkWindowRemoved( + _: *c.GtkApplication, + _: *c.GtkWindow, + ud: ?*anyopaque, +) callconv(.C) void { + const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); + + // Recheck if we are focused + gtkWindowIsActive(null, undefined, core_app); +} + +fn gtkWindowIsActive( + window: ?*c.GtkWindow, + _: *c.GParamSpec, + ud: ?*anyopaque, +) callconv(.C) void { + const core_app: *CoreApp = @ptrCast(@alignCast(ud orelse return)); + + // If our window is active, then we can tell the app + // that we are focused. + if (window) |w| { + if (c.gtk_window_is_active(w) == 1) { + core_app.focusEvent(true); + return; + } + } + + // If the window becomes inactive, we need to check if any + // other windows are active. If not, then we are no longer + // focused. + if (c.gtk_window_list_toplevels()) |list| { + defer c.g_list_free(list); + var current: ?*c.GList = list; + while (current) |elem| : (current = elem.next) { + // If the window is active then we are still focused. + // This is another window since we did our check above. + // That window should trigger its own is-active + // callback so we don't need to call it here. + const w: *c.GtkWindow = @alignCast(@ptrCast(elem.data)); + if (c.gtk_window_is_active(w) == 1) return; + } + } + + // We are not focused + core_app.focusEvent(false); +} + /// Call a D-Bus method to determine the current color scheme. If there /// is any error at any point we'll log the error and return "light" pub fn getColorScheme(self: *App) apprt.ColorScheme {