From c0e7b92e911e403f4c62cef2c584dc5b89ffc156 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 29 Jul 2025 07:30:53 -0700 Subject: [PATCH] apprt/gtk-ng: close tab confirmation --- .../class/close_confirmation_dialog.zig | 42 +++++++--- src/apprt/gtk-ng/class/tab.zig | 8 ++ src/apprt/gtk-ng/class/window.zig | 76 +++++++++++++++++++ src/apprt/gtk-ng/ui/1.5/window.blp | 1 + 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig index cd22f9b31..579f792ce 100644 --- a/src/apprt/gtk-ng/class/close_confirmation_dialog.zig +++ b/src/apprt/gtk-ng/class/close_confirmation_dialog.zig @@ -57,6 +57,17 @@ pub const CloseConfirmationDialog = extern struct { void, ); }; + + pub const cancel = struct { + pub const name = "cancel"; + pub const connect = impl.connect; + const impl = gobject.ext.defineSignal( + name, + Self, + &.{}, + void, + ); + }; }; const Private = struct { @@ -72,14 +83,15 @@ pub const CloseConfirmationDialog = extern struct { fn init(self: *Self, _: *Class) callconv(.C) void { gtk.Widget.initTemplate(self.as(gtk.Widget)); + } + pub fn present(self: *Self, parent: ?*gtk.Widget) void { // Setup our title/body text. const priv = self.private(); self.as(Dialog.Parent).setHeading(priv.target.title()); self.as(Dialog.Parent).setBody(priv.target.body()); - } - pub fn present(self: *Self, parent: ?*gtk.Widget) void { + // Show it self.as(Dialog).present(parent); } @@ -91,13 +103,21 @@ pub const CloseConfirmationDialog = extern struct { self: *Self, response_id: [*:0]const u8, ) callconv(.C) void { - if (std.mem.orderZ(u8, response_id, "close") != .eq) return; - signals.@"close-request".impl.emit( - self, - null, - .{}, - null, - ); + if (std.mem.orderZ(u8, response_id, "close") == .eq) { + signals.@"close-request".impl.emit( + self, + null, + .{}, + null, + ); + } else { + signals.cancel.impl.emit( + self, + null, + .{}, + null, + ); + } } fn dispose(self: *Self) callconv(.C) void { @@ -141,6 +161,7 @@ pub const CloseConfirmationDialog = extern struct { // Signals signals.@"close-request".impl.register(.{}); + signals.cancel.impl.register(.{}); // Virtual methods gobject.Object.virtual_methods.dispose.implement(class, &dispose); @@ -158,11 +179,13 @@ pub const CloseConfirmationDialog = extern struct { /// together into one struct that is the sole source of truth. pub const Target = enum(c_int) { app, + tab, window, pub fn title(self: Target) [*:0]const u8 { return switch (self) { .app => i18n._("Quit Ghostty?"), + .tab => i18n._("Close Tab?"), .window => i18n._("Close Window?"), }; } @@ -170,6 +193,7 @@ pub const Target = enum(c_int) { pub fn body(self: Target) [*:0]const u8 { return switch (self) { .app => i18n._("All terminal sessions will be terminated."), + .tab => i18n._("All terminal sessions in this tab will be terminated."), .window => i18n._("All terminal sessions in this window will be terminated."), }; } diff --git a/src/apprt/gtk-ng/class/tab.zig b/src/apprt/gtk-ng/class/tab.zig index 95ba74a0e..9787e991e 100644 --- a/src/apprt/gtk-ng/class/tab.zig +++ b/src/apprt/gtk-ng/class/tab.zig @@ -172,6 +172,14 @@ pub const Tab = extern struct { return priv.surface; } + /// Returns true if this tab needs confirmation before quitting based + /// on the various Ghostty configurations. + pub fn getNeedsConfirmQuit(self: *Self) bool { + const surface = self.getActiveSurface(); + const core_surface = surface.core() orelse return false; + return core_surface.needsConfirmQuit(); + } + //--------------------------------------------------------------- // Virtual methods diff --git a/src/apprt/gtk-ng/class/window.zig b/src/apprt/gtk-ng/class/window.zig index 18696f32a..c922bfd9a 100644 --- a/src/apprt/gtk-ng/class/window.zig +++ b/src/apprt/gtk-ng/class/window.zig @@ -591,6 +591,81 @@ pub const Window = extern struct { self.as(gtk.Window).destroy(); } + fn closeConfirmationCloseTab( + _: *CloseConfirmationDialog, + page: *adw.TabPage, + ) callconv(.c) void { + const tab_view_widget = page + .getChild() + .as(gtk.Widget) + .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { + log.warn("close confirmation caled for non-existent page", .{}); + return; + }; + const tab_view = gobject.ext.cast( + adw.TabView, + tab_view_widget, + ).?; + tab_view.closePageFinish(page, @intFromBool(true)); + } + + fn closeConfirmationCancelTab( + _: *CloseConfirmationDialog, + page: *adw.TabPage, + ) callconv(.c) void { + const tab_view_widget = page + .getChild() + .as(gtk.Widget) + .getAncestor(gobject.ext.typeFor(adw.TabView)) orelse { + log.warn("close confirmation caled for non-existent page", .{}); + return; + }; + const tab_view = gobject.ext.cast( + adw.TabView, + tab_view_widget, + ).?; + tab_view.closePageFinish(page, @intFromBool(false)); + } + + fn tabViewClosePage( + _: *adw.TabView, + page: *adw.TabPage, + self: *Self, + ) callconv(.c) c_int { + const priv = self.private(); + const child = page.getChild(); + const tab = gobject.ext.cast(Tab, child) orelse + return @intFromBool(false); + + // If the tab says it doesn't need confirmation then we go ahead + // and close immediately. + if (!tab.getNeedsConfirmQuit()) { + priv.tab_view.closePageFinish(page, @intFromBool(true)); + return @intFromBool(true); + } + + // Show a confirmation dialog + const dialog: *CloseConfirmationDialog = .new(.tab); + _ = CloseConfirmationDialog.signals.@"close-request".connect( + dialog, + *adw.TabPage, + closeConfirmationCloseTab, + page, + .{}, + ); + _ = CloseConfirmationDialog.signals.cancel.connect( + dialog, + *adw.TabPage, + closeConfirmationCancelTab, + page, + .{}, + ); + + // Show it + dialog.present(child); + return @intFromBool(true); + } + fn tabViewSelectedPage( _: *adw.TabView, _: *gobject.ParamSpec, @@ -929,6 +1004,7 @@ pub const Window = extern struct { // Template Callbacks class.bindTemplateCallback("close_request", &windowCloseRequest); + class.bindTemplateCallback("close_page", &tabViewClosePage); class.bindTemplateCallback("selected_page", &tabViewSelectedPage); class.bindTemplateCallback("page_attached", &tabViewPageAttached); class.bindTemplateCallback("page_detached", &tabViewPageDetached); diff --git a/src/apprt/gtk-ng/ui/1.5/window.blp b/src/apprt/gtk-ng/ui/1.5/window.blp index 3c03a2751..70974d1c7 100644 --- a/src/apprt/gtk-ng/ui/1.5/window.blp +++ b/src/apprt/gtk-ng/ui/1.5/window.blp @@ -79,6 +79,7 @@ template $GhosttyWindow: Adw.ApplicationWindow { Adw.ToastOverlay toast_overlay { Adw.TabView tab_view { notify::selected-page => $selected_page(); + close-page => $close_page(); page-attached => $page_attached(); page-detached => $page_detached(); }