From b7b5b9bbf5f570f4669c92e61322469a825d0b33 Mon Sep 17 00:00:00 2001 From: Leigh Oliver Date: Tue, 31 Dec 2024 23:40:49 +0000 Subject: [PATCH 1/3] fix(gtk): add close confirmation for tabs --- src/apprt/gtk/Tab.zig | 67 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index ed0804fd3..1a3b44136 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -121,10 +121,71 @@ pub fn remove(self: *Tab) void { self.window.closeTab(self); } +/// Helper function to check if any surface in the split hierarchy needs close confirmation +const needsConfirm = struct { + fn check(elem: Surface.Container.Elem) bool { + return switch (elem) { + .surface => |s| s.core_surface.needsConfirmQuit(), + .split => |s| check(s.top_left) or check(s.bottom_right), + }; + } +}.check; + +/// Close the tab, asking for confirmation if any surface requests it. +fn closeWithConfirmation(tab: *Tab) void { + switch (tab.elem) { + .surface => |s| s.close(s.core_surface.needsConfirmQuit()), + .split => |s| { + if (needsConfirm(s.top_left) or needsConfirm(s.bottom_right)) { + const alert = c.gtk_message_dialog_new( + tab.window.window, + c.GTK_DIALOG_MODAL, + c.GTK_MESSAGE_QUESTION, + c.GTK_BUTTONS_YES_NO, + "Close this tab?", + ); + c.gtk_message_dialog_format_secondary_text( + @ptrCast(alert), + "All terminal sessions in this tab will be terminated.", + ); + + // We want the "yes" to appear destructive. + const yes_widget = c.gtk_dialog_get_widget_for_response( + @ptrCast(alert), + c.GTK_RESPONSE_YES, + ); + c.gtk_widget_add_css_class(yes_widget, "destructive-action"); + + // We want the "no" to be the default action + c.gtk_dialog_set_default_response( + @ptrCast(alert), + c.GTK_RESPONSE_NO, + ); + + _ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kTabCloseConfirmation), tab, null, c.G_CONNECT_DEFAULT); + c.gtk_widget_show(alert); + return; + } + tab.remove(); + }, + } +} + pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { const tab: *Tab = @ptrCast(@alignCast(ud)); - const window = tab.window; - window.closeTab(tab); + tab.closeWithConfirmation(); +} + +fn gtkTabCloseConfirmation( + alert: *c.GtkMessageDialog, + response: c.gint, + ud: ?*anyopaque, +) callconv(.C) void { + c.gtk_window_destroy(@ptrCast(alert)); + if (response == c.GTK_RESPONSE_YES) { + const tab: *Tab = @ptrCast(@alignCast(ud)); + tab.remove(); + } } fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { @@ -146,6 +207,6 @@ pub fn gtkTabClick( const self: *Tab = @ptrCast(@alignCast(ud)); const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture)); if (gtk_button == c.GDK_BUTTON_MIDDLE) { - self.remove(); + self.closeWithConfirmation(); } } From 8c1ad59de761153023fba73913bb168df91c55d5 Mon Sep 17 00:00:00 2001 From: Leigh Oliver Date: Wed, 1 Jan 2025 10:14:16 +0000 Subject: [PATCH 2/3] remove unnecessary struct --- src/apprt/gtk/Tab.zig | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 1a3b44136..6e28b8644 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -122,14 +122,12 @@ pub fn remove(self: *Tab) void { } /// Helper function to check if any surface in the split hierarchy needs close confirmation -const needsConfirm = struct { - fn check(elem: Surface.Container.Elem) bool { - return switch (elem) { - .surface => |s| s.core_surface.needsConfirmQuit(), - .split => |s| check(s.top_left) or check(s.bottom_right), - }; - } -}.check; +fn needsConfirm(elem: Surface.Container.Elem) bool { + return switch (elem) { + .surface => |s| s.core_surface.needsConfirmQuit(), + .split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right), + }; +} /// Close the tab, asking for confirmation if any surface requests it. fn closeWithConfirmation(tab: *Tab) void { From 00137c41895628cc6a068e40445b6670ae3a9012 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 10 Jan 2025 15:32:25 -0800 Subject: [PATCH 3/3] apprt/gtk: adw tab view close confirmation --- src/apprt/gtk/Tab.zig | 28 ++++--------------------- src/apprt/gtk/notebook_adw.zig | 37 ++++++++++++++++++++++++++++++++++ src/apprt/gtk/notebook_gtk.zig | 23 +++++++++++++++++++-- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 6e28b8644..d320daa7c 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -130,7 +130,7 @@ fn needsConfirm(elem: Surface.Container.Elem) bool { } /// Close the tab, asking for confirmation if any surface requests it. -fn closeWithConfirmation(tab: *Tab) void { +pub fn closeWithConfirmation(tab: *Tab) void { switch (tab.elem) { .surface => |s| s.close(s.core_surface.needsConfirmQuit()), .split => |s| { @@ -169,21 +169,15 @@ fn closeWithConfirmation(tab: *Tab) void { } } -pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { - const tab: *Tab = @ptrCast(@alignCast(ud)); - tab.closeWithConfirmation(); -} - fn gtkTabCloseConfirmation( alert: *c.GtkMessageDialog, response: c.gint, ud: ?*anyopaque, ) callconv(.C) void { + const tab: *Tab = @ptrCast(@alignCast(ud)); c.gtk_window_destroy(@ptrCast(alert)); - if (response == c.GTK_RESPONSE_YES) { - const tab: *Tab = @ptrCast(@alignCast(ud)); - tab.remove(); - } + if (response != c.GTK_RESPONSE_YES) return; + tab.remove(); } fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { @@ -194,17 +188,3 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void { const tab: *Tab = @ptrCast(@alignCast(ud)); tab.destroy(tab.window.app.core_app.alloc); } - -pub fn gtkTabClick( - gesture: *c.GtkGestureClick, - _: c.gint, - _: c.gdouble, - _: c.gdouble, - ud: ?*anyopaque, -) callconv(.C) void { - const self: *Tab = @ptrCast(@alignCast(ud)); - const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture)); - if (gtk_button == c.GDK_BUTTON_MIDDLE) { - self.closeWithConfirmation(); - } -} diff --git a/src/apprt/gtk/notebook_adw.zig b/src/apprt/gtk/notebook_adw.zig index 48f005467..649db9be3 100644 --- a/src/apprt/gtk/notebook_adw.zig +++ b/src/apprt/gtk/notebook_adw.zig @@ -17,6 +17,14 @@ pub const NotebookAdw = struct { /// the tab view tab_view: *AdwTabView, + /// Set to true so that the adw close-page handler knows we're forcing + /// and to allow a close to happen with no confirm. This is a bit of a hack + /// because we currently use GTK alerts to confirm tab close and they + /// don't carry with them the ADW state that we are confirming or not. + /// Long term we should move to ADW alerts so we can know if we are + /// confirming or not. + forcing_close: bool = false, + pub fn init(notebook: *Notebook) void { const window: *Window = @fieldParentPtr("notebook", notebook); const app = window.app; @@ -38,6 +46,7 @@ pub const NotebookAdw = struct { }; _ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT); } @@ -112,6 +121,12 @@ pub const NotebookAdw = struct { pub fn closeTab(self: *NotebookAdw, tab: *Tab) void { if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable; + // closeTab always expects to close unconditionally so we mark this + // as true so that the close_page call below doesn't request + // confirmation. + self.forcing_close = true; + defer self.forcing_close = false; + const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return; c.adw_tab_view_close_page(self.tab_view, page); @@ -143,6 +158,28 @@ fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaqu window.focusCurrentTab(); } +fn adwClosePage( + _: *AdwTabView, + page: *c.AdwTabPage, + ud: ?*anyopaque, +) callconv(.C) c.gboolean { + const child = c.adw_tab_page_get_child(page); + const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data( + @ptrCast(child), + Tab.GHOSTTY_TAB, + ) orelse return 0)); + + const window: *Window = @ptrCast(@alignCast(ud.?)); + const notebook = window.notebook.adw; + c.adw_tab_view_close_page_finish( + notebook.tab_view, + page, + @intFromBool(notebook.forcing_close), + ); + if (!notebook.forcing_close) tab.closeWithConfirmation(); + return 1; +} + fn adwTabViewCreateWindow( _: *AdwTabView, ud: ?*anyopaque, diff --git a/src/apprt/gtk/notebook_gtk.zig b/src/apprt/gtk/notebook_gtk.zig index a2c482500..5f145dc84 100644 --- a/src/apprt/gtk/notebook_gtk.zig +++ b/src/apprt/gtk/notebook_gtk.zig @@ -157,8 +157,8 @@ pub const NotebookGtk = struct { c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0); c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click)); - _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT); - _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), tab, null, c.G_CONNECT_DEFAULT); + _ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), tab, null, c.G_CONNECT_DEFAULT); // Tab settings c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1); @@ -283,3 +283,22 @@ fn gtkNotebookCreateWindow( return newWindow.notebook.gtk.notebook; } + +fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void { + const tab: *Tab = @ptrCast(@alignCast(ud)); + tab.closeWithConfirmation(); +} + +fn gtkTabClick( + gesture: *c.GtkGestureClick, + _: c.gint, + _: c.gdouble, + _: c.gdouble, + ud: ?*anyopaque, +) callconv(.C) void { + const self: *Tab = @ptrCast(@alignCast(ud)); + const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture)); + if (gtk_button == c.GDK_BUTTON_MIDDLE) { + self.closeWithConfirmation(); + } +}