apprt/gtk-ng: hook up window close confirmation

This commit is contained in:
Mitchell Hashimoto
2025-07-25 15:06:21 -07:00
parent a25a0011ea
commit a8d0a84530
4 changed files with 65 additions and 29 deletions

View File

@ -418,10 +418,16 @@ pub const Application = extern struct {
return;
}
// Get the parent for our dialog
const parent: ?*gtk.Widget = parent: {
const list = gtk.Window.listToplevels();
defer list.free();
const focused = list.findCustom(null, findActiveWindow);
break :parent @ptrCast(@alignCast(focused.f_data));
};
// Show a confirmation dialog
const dialog: *CloseConfirmationDialog = .new(.app);
// Connect to the reload signal so we know to reload our config.
_ = CloseConfirmationDialog.signals.@"close-request".connect(
dialog,
*Application,
@ -431,7 +437,7 @@ pub const Application = extern struct {
);
// Show it
dialog.present();
dialog.present(parent);
}
fn quitNow(self: *Self) void {
@ -1359,3 +1365,12 @@ fn setGtkEnv(config: *const CoreConfig) error{NoSpaceLeft}!void {
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
}
}
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
// Confusingly, `isActive` returns 1 when active,
// but we want to return 0 to indicate equality.
// Abusing integers to be enums and booleans is a terrible idea, C.
return if (window.isActive() != 0) 0 else -1;
}

View File

@ -79,9 +79,8 @@ pub const CloseConfirmationDialog = extern struct {
self.as(Dialog.Parent).setBody(priv.target.body());
}
pub fn present(self: *Self) void {
const priv = self.private();
self.as(Dialog).present(priv.target.dialogParent());
pub fn present(self: *Self, parent: ?*gtk.Widget) void {
self.as(Dialog).present(parent);
}
pub fn close(self: *Self) void {
@ -159,28 +158,19 @@ pub const CloseConfirmationDialog = extern struct {
/// together into one struct that is the sole source of truth.
pub const Target = enum(c_int) {
app,
window,
pub fn title(self: Target) [*:0]const u8 {
return switch (self) {
.app => i18n._("Quit Ghostty?"),
.window => i18n._("Close Window?"),
};
}
pub fn body(self: Target) [*:0]const u8 {
return switch (self) {
.app => i18n._("All terminal sessions will be terminated."),
};
}
pub fn dialogParent(self: Target) ?*gtk.Widget {
return switch (self) {
.app => {
// Find the currently focused window.
const list = gtk.Window.listToplevels();
defer list.free();
const focused = list.findCustom(null, findActiveWindow);
return @ptrCast(@alignCast(focused.f_data));
},
.window => i18n._("All terminal sessions in this window will be terminated."),
};
}
@ -189,12 +179,3 @@ pub const Target = enum(c_int) {
.{ .name = "GhosttyCloseConfirmationDialogTarget" },
);
};
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
// Confusingly, `isActive` returns 1 when active,
// but we want to return 0 to indicate equality.
// Abusing integers to be enums and booleans is a terrible idea, C.
return if (window.isActive() != 0) 0 else -1;
}

View File

@ -16,6 +16,7 @@ const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const Surface = @import("surface.zig").Surface;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
@ -337,6 +338,44 @@ pub const Window = extern struct {
//---------------------------------------------------------------
// Signal handlers
fn windowCloseRequest(
_: *gtk.Window,
self: *Self,
) callconv(.c) c_int {
// If our surface needs confirmation then we show confirmation.
// This will have to be expanded to a list when we have tabs
// or splits.
confirm: {
const surface = self.getActiveSurface() orelse break :confirm;
const core_surface = surface.core() orelse break :confirm;
if (!core_surface.needsConfirmQuit()) break :confirm;
// Show a confirmation dialog
const dialog: *CloseConfirmationDialog = .new(.app);
_ = CloseConfirmationDialog.signals.@"close-request".connect(
dialog,
*Self,
closeConfirmationClose,
self,
.{},
);
// Show it
dialog.present(self.as(gtk.Widget));
return @intFromBool(true);
}
self.as(gtk.Window).destroy();
return @intFromBool(false);
}
fn closeConfirmationClose(
_: *CloseConfirmationDialog,
self: *Self,
) callconv(.c) void {
self.as(gtk.Window).destroy();
}
fn surfaceCloseRequest(
surface: *Surface,
scope: *const Surface.CloseScope,
@ -426,8 +465,7 @@ pub const Window = extern struct {
_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
// TODO: Confirmation
self.as(gtk.Window).destroy();
self.as(gtk.Window).close();
}
fn actionCopy(
@ -497,6 +535,7 @@ pub const Window = extern struct {
class.bindTemplateChildPrivate("surface", .{});
// Template Callbacks
class.bindTemplateCallback("close_request", &windowCloseRequest);
class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest);
class.bindTemplateCallback("surface_toggle_fullscreen", &surfaceToggleFullscreen);
class.bindTemplateCallback("surface_toggle_maximize", &surfaceToggleMaximize);

View File

@ -6,6 +6,7 @@ template $GhosttyWindow: Adw.ApplicationWindow {
"window",
]
close-request => $close_request();
notify::config => $notify_config();
notify::fullscreened => $notify_fullscreened();
notify::maximized => $notify_maximized();