mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
gtk: use AdwAlertDialog for close dialogs, fix incorrect close dialogs (#5741)
AdwAlertDialog is the recommended way to do alert/message dialogs starting from libadwaita 1.5, and is much easier to manage than GtkMessageDialog. (The latter is also deprecated since GTK 4.10, but this PR does not migrate it to use GtkAlertDialog, mostly because of its obtuse interface and that we'll remove the GtkMessageDialog code anyway in 1.2 when we remove non-Adwaita builds.) We also had two bugs where tabs with only one split would display the "close surface" confirmation dialog, and windows would do the same when closed via the "Close Window" menu item or by the `close_window` keybind action. (The "close window" dialog only appears when the user clicks on the close button on the titlebar.) Initially I was very confused by this, but it turns out that we don't have any apprt action related to closing a window, and it was simply closing surfaces...
This commit is contained in:
@ -600,6 +600,7 @@ typedef enum {
|
||||
GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
GHOSTTY_ACTION_RELOAD_CONFIG,
|
||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||
GHOSTTY_ACTION_CLOSE_WINDOW,
|
||||
} ghostty_action_tag_e;
|
||||
|
||||
typedef union {
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-02-28 22:12+0100\n"
|
||||
"POT-Creation-Date: 2025-03-06 20:10+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -26,7 +26,7 @@ msgid "Leave blank to restore the default title."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
@ -91,7 +91,7 @@ msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
|
||||
#: src/apprt/gtk/Window.zig:239
|
||||
#: src/apprt/gtk/Window.zig:246
|
||||
msgid "New Tab"
|
||||
msgstr ""
|
||||
|
||||
@ -133,7 +133,7 @@ msgid "Terminal Inspector"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102
|
||||
#: src/apprt/gtk/Window.zig:923
|
||||
#: src/apprt/gtk/Window.zig:933
|
||||
msgid "About Ghostty"
|
||||
msgstr ""
|
||||
|
||||
@ -178,27 +178,63 @@ msgid ""
|
||||
"commands may be executed."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:192
|
||||
#: src/apprt/gtk/Window.zig:199
|
||||
msgid "Main Menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:212
|
||||
#: src/apprt/gtk/Window.zig:219
|
||||
msgid "View Open Tabs"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:257
|
||||
#: src/apprt/gtk/Window.zig:265
|
||||
msgid ""
|
||||
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:660
|
||||
#: src/apprt/gtk/Window.zig:681
|
||||
msgid "Reloaded the configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Window.zig:904
|
||||
#: src/apprt/gtk/Window.zig:914
|
||||
msgid "Ghostty Developers"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1236
|
||||
#: src/apprt/gtk/CloseDialog.zig:47
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:87
|
||||
msgid "Quit Ghostty?"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:88
|
||||
msgid "Close Window?"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:89
|
||||
msgid "Close Tab?"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:90
|
||||
msgid "Close Split?"
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:96
|
||||
msgid "All terminal sessions will be terminated."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:97
|
||||
msgid "All terminal sessions in this window will be terminated."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:98
|
||||
msgid "All terminal sessions in this tab will be terminated."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:99
|
||||
msgid "The currently running process in this split will be terminated."
|
||||
msgstr ""
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1128
|
||||
msgid "Copied to clipboard"
|
||||
msgstr ""
|
||||
|
@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-02-28 22:12+0100\n"
|
||||
"POT-Creation-Date: 2025-03-06 20:10+0100\n"
|
||||
"PO-Revision-Date: 2025-02-27 09:16+0100\n"
|
||||
"Last-Translator: Leah <hi@pluie.me>\n"
|
||||
"Language-Team: Chinese (simplified) <i18n-zh@googlegroups.com>\n"
|
||||
@ -26,7 +26,7 @@ msgid "Leave blank to restore the default title."
|
||||
msgstr "留空以重置至默认标题。"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
@ -91,7 +91,7 @@ msgstr "标签页"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30
|
||||
#: src/apprt/gtk/Window.zig:239
|
||||
#: src/apprt/gtk/Window.zig:246
|
||||
msgid "New Tab"
|
||||
msgstr "新建标签页"
|
||||
|
||||
@ -133,7 +133,7 @@ msgid "Terminal Inspector"
|
||||
msgstr "终端检视器"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102
|
||||
#: src/apprt/gtk/Window.zig:923
|
||||
#: src/apprt/gtk/Window.zig:933
|
||||
msgid "About Ghostty"
|
||||
msgstr "关于 Ghostty"
|
||||
|
||||
@ -178,27 +178,63 @@ msgid ""
|
||||
"commands may be executed."
|
||||
msgstr "将以下内容粘贴至终端内将可能执行有害命令。"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:192
|
||||
#: src/apprt/gtk/Window.zig:199
|
||||
msgid "Main Menu"
|
||||
msgstr "主菜单"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:212
|
||||
#: src/apprt/gtk/Window.zig:219
|
||||
msgid "View Open Tabs"
|
||||
msgstr "浏览标签页"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:257
|
||||
#: src/apprt/gtk/Window.zig:265
|
||||
msgid ""
|
||||
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
|
||||
msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:660
|
||||
#: src/apprt/gtk/Window.zig:681
|
||||
msgid "Reloaded the configuration"
|
||||
msgstr "已重新加载设置"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:904
|
||||
#: src/apprt/gtk/Window.zig:914
|
||||
msgid "Ghostty Developers"
|
||||
msgstr "Ghostty 开发团队"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1236
|
||||
#: src/apprt/gtk/CloseDialog.zig:47
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:87
|
||||
msgid "Quit Ghostty?"
|
||||
msgstr "退出 Ghostty 吗?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:88
|
||||
msgid "Close Window?"
|
||||
msgstr "关闭窗口吗?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:89
|
||||
msgid "Close Tab?"
|
||||
msgstr "关闭标签页吗?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:90
|
||||
msgid "Close Split?"
|
||||
msgstr "关闭分屏吗?"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:96
|
||||
msgid "All terminal sessions will be terminated."
|
||||
msgstr "终端内所有运行中的进程将被终止。"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:97
|
||||
msgid "All terminal sessions in this window will be terminated."
|
||||
msgstr "窗口内所有运行中的进程将被终止。"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:98
|
||||
msgid "All terminal sessions in this tab will be terminated."
|
||||
msgstr "标签页内所有运行中的进程将被终止。"
|
||||
|
||||
#: src/apprt/gtk/CloseDialog.zig:99
|
||||
msgid "The currently running process in this split will be terminated."
|
||||
msgstr "分屏内正在运行中的进程将被终止。"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1128
|
||||
msgid "Copied to clipboard"
|
||||
msgstr "已复制至剪切板"
|
||||
|
@ -4292,7 +4292,11 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
|
||||
.close_surface => self.close(),
|
||||
|
||||
.close_window => self.app.closeSurface(self),
|
||||
.close_window => return try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.close_window,
|
||||
{},
|
||||
),
|
||||
|
||||
.crash => |location| switch (location) {
|
||||
.main => @panic("crash binding action, crashing intentionally"),
|
||||
|
@ -241,6 +241,9 @@ pub const Action = union(Key) {
|
||||
/// for changes.
|
||||
config_change: ConfigChange,
|
||||
|
||||
/// Closes the currently focused window.
|
||||
close_window,
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
@ -283,6 +286,7 @@ pub const Action = union(Key) {
|
||||
color_change,
|
||||
reload_config,
|
||||
config_change,
|
||||
close_window,
|
||||
};
|
||||
|
||||
/// Sync with: ghostty_action_u
|
||||
|
@ -219,6 +219,7 @@ pub const App = struct {
|
||||
.toggle_split_zoom,
|
||||
.present_terminal,
|
||||
.close_all_windows,
|
||||
.close_window,
|
||||
.close_tab,
|
||||
.toggle_tab_overview,
|
||||
.toggle_window_decorations,
|
||||
|
@ -37,6 +37,7 @@ const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const i18n = @import("i18n.zig");
|
||||
@ -45,6 +46,7 @@ const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const testing = std.testing;
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@ -479,10 +481,11 @@ pub fn performAction(
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
.close_window => return try self.closeWindow(target),
|
||||
.toggle_maximize => self.toggleMaximize(target),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
.new_tab => try self.newTab(target),
|
||||
.close_tab => try self.closeTab(target),
|
||||
.close_tab => return try self.closeTab(target),
|
||||
.goto_tab => return self.gotoTab(target, value),
|
||||
.move_tab => self.moveTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
@ -547,19 +550,20 @@ fn newTab(_: *App, target: apprt.Target) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn closeTab(_: *App, target: apprt.Target) !void {
|
||||
fn closeTab(_: *App, target: apprt.Target) !bool {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
const tab = v.rt_surface.container.tab() orelse {
|
||||
log.info(
|
||||
"close_tab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
tab.closeWithConfirmation();
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -1421,65 +1425,34 @@ fn setSecureInput(_: *App, target: apprt.Target, value: apprt.action.SecureInput
|
||||
}
|
||||
}
|
||||
|
||||
fn closeWindow(_: *App, target: apprt.action.Target) !bool {
|
||||
switch (target) {
|
||||
.app => return false,
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse return false;
|
||||
window.closeWithConfirmation();
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(self: *App) void {
|
||||
// If we're already not running, do nothing.
|
||||
if (!self.running) return;
|
||||
|
||||
// If we have no toplevel windows, then we're done.
|
||||
const list = c.gtk_window_list_toplevels();
|
||||
if (list == null) {
|
||||
self.running = false;
|
||||
return;
|
||||
}
|
||||
c.g_list_free(list);
|
||||
|
||||
// If the app says we don't need to confirm, then we can quit now.
|
||||
if (!self.core_app.needsConfirmQuit()) {
|
||||
self.quitNow();
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have windows, then we want to confirm that we want to exit.
|
||||
const alert = c.gtk_message_dialog_new(
|
||||
null,
|
||||
c.GTK_DIALOG_MODAL,
|
||||
c.GTK_MESSAGE_QUESTION,
|
||||
c.GTK_BUTTONS_YES_NO,
|
||||
"Quit Ghostty?",
|
||||
);
|
||||
c.gtk_message_dialog_format_secondary_text(
|
||||
@ptrCast(alert),
|
||||
"All active terminal sessions 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(>kQuitConfirmation),
|
||||
self,
|
||||
null,
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
|
||||
c.gtk_widget_show(alert);
|
||||
CloseDialog.show(.{ .app = self }) catch |err| {
|
||||
log.err("failed to open close dialog={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// This immediately destroys all windows, forcing the application to quit.
|
||||
fn quitNow(self: *App) void {
|
||||
_ = self;
|
||||
pub fn quitNow(self: *App) void {
|
||||
const list = c.gtk_window_list_toplevels();
|
||||
defer c.g_list_free(list);
|
||||
c.g_list_foreach(list, struct {
|
||||
@ -1490,23 +1463,8 @@ fn quitNow(self: *App) void {
|
||||
c.gtk_window_destroy(window);
|
||||
}
|
||||
}.callback, null);
|
||||
}
|
||||
|
||||
fn gtkQuitConfirmation(
|
||||
alert: *c.GtkMessageDialog,
|
||||
response: c.gint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
|
||||
// Close the alert window
|
||||
c.gtk_window_destroy(@ptrCast(alert));
|
||||
|
||||
// If we didn't confirm then we're done
|
||||
if (response != c.GTK_RESPONSE_YES) return;
|
||||
|
||||
// Force close all open windows
|
||||
self.quitNow();
|
||||
self.running = false;
|
||||
}
|
||||
|
||||
/// This is called by the `activate` signal. This is sent on program startup and
|
||||
|
151
src/apprt/gtk/CloseDialog.zig
Normal file
151
src/apprt/gtk/CloseDialog.zig
Normal file
@ -0,0 +1,151 @@
|
||||
const CloseDialog = @This();
|
||||
const std = @import("std");
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
const adw = @import("adw");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
const i18n = @import("i18n.zig");
|
||||
|
||||
const log = std.log.scoped(.close_dialog);
|
||||
|
||||
// We don't fall back to the GTK Message/AlertDialogs since
|
||||
// we don't plan to support libadw < 1.2 as of time of writing
|
||||
// TODO: Switch to just adw.AlertDialog when we drop Debian 12 support
|
||||
const DialogType = if (adwaita.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
/// Open the dialog when the user requests to close a window/tab/split/etc.
|
||||
/// but there's still one or more running processes inside the target that
|
||||
/// cannot be closed automatically. We then ask the user whether they want
|
||||
/// to terminate existing processes.
|
||||
pub fn show(target: Target) !void {
|
||||
// If we don't have a possible window to ask the user,
|
||||
// in most situations (e.g. when a split isn't attached to a window)
|
||||
// we should just close unconditionally.
|
||||
const dialog_window = target.dialogWindow() orelse {
|
||||
target.close();
|
||||
return;
|
||||
};
|
||||
|
||||
const dialog = switch (DialogType) {
|
||||
adw.AlertDialog => adw.AlertDialog.new(target.title(), target.body()),
|
||||
adw.MessageDialog => adw.MessageDialog.new(dialog_window, target.title(), target.body()),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// AlertDialog and MessageDialog have essentially the same API,
|
||||
// so we can cheat a little here
|
||||
dialog.addResponse("cancel", i18n._("Cancel"));
|
||||
dialog.setCloseResponse("cancel");
|
||||
|
||||
dialog.addResponse("close", i18n._("Close"));
|
||||
dialog.setResponseAppearance("close", .destructive);
|
||||
|
||||
// Need a stable pointer
|
||||
const target_ptr = try target.allocator().create(Target);
|
||||
target_ptr.* = target;
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *Target, responseCallback, target_ptr, .{});
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => dialog.as(adw.Dialog).present(dialog_window.as(gtk.Widget)),
|
||||
adw.MessageDialog => dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn responseCallback(
|
||||
_: *DialogType,
|
||||
response: [*:0]const u8,
|
||||
target: *Target,
|
||||
) callconv(.C) void {
|
||||
const alloc = target.allocator();
|
||||
defer alloc.destroy(target);
|
||||
|
||||
if (std.mem.orderZ(u8, response, "close") == .eq) target.close();
|
||||
}
|
||||
|
||||
/// The target of a close dialog.
|
||||
///
|
||||
/// This is here so that we can consolidate all logic related to
|
||||
/// prompting the user and closing windows/tabs/surfaces/etc.
|
||||
/// together into one struct that is the sole source of truth.
|
||||
pub const Target = union(enum) {
|
||||
app: *App,
|
||||
window: *Window,
|
||||
tab: *Tab,
|
||||
surface: *Surface,
|
||||
|
||||
pub fn title(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("Quit Ghostty?"),
|
||||
.window => i18n._("Close Window?"),
|
||||
.tab => i18n._("Close Tab?"),
|
||||
.surface => i18n._("Close Split?"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn body(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("All terminal sessions will be terminated."),
|
||||
.window => i18n._("All terminal sessions in this window will be terminated."),
|
||||
.tab => i18n._("All terminal sessions in this tab will be terminated."),
|
||||
.surface => i18n._("The currently running process in this split will be terminated."),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dialogWindow(self: Target) ?*gtk.Window {
|
||||
return switch (self) {
|
||||
.app => {
|
||||
// Find the currently focused window. We don't store this
|
||||
// anywhere inside the App structure for some reason, so
|
||||
// we have to query every single open window and see which
|
||||
// one is active (focused and receiving keyboard input)
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
return @ptrCast(@alignCast(focused.f_data));
|
||||
},
|
||||
.window => |v| @ptrCast(v.window),
|
||||
.tab => |v| @ptrCast(v.window.window),
|
||||
.surface => |v| surface: {
|
||||
const window_ = v.container.window() orelse return null;
|
||||
break :surface @ptrCast(window_.window);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn allocator(self: Target) std.mem.Allocator {
|
||||
return switch (self) {
|
||||
.app => |v| v.core_app.alloc,
|
||||
.window => |v| v.app.core_app.alloc,
|
||||
.tab => |v| v.window.app.core_app.alloc,
|
||||
.surface => |v| v.app.core_app.alloc,
|
||||
};
|
||||
}
|
||||
|
||||
fn close(self: Target) void {
|
||||
switch (self) {
|
||||
.app => |v| v.quitNow(),
|
||||
.window => |v| gtk.Window.destroy(@ptrCast(v.window)),
|
||||
.tab => |v| v.remove(),
|
||||
.surface => |v| v.container.remove(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
@ -30,6 +30,7 @@ const Menu = @import("menu.zig").Menu;
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const ResizeOverlay = @import("ResizeOverlay.zig");
|
||||
const URLWidget = @import("URLWidget.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const c = @import("c.zig").c;
|
||||
@ -663,54 +664,22 @@ pub fn redraw(self: *Surface) void {
|
||||
}
|
||||
|
||||
/// Close this surface.
|
||||
pub fn close(self: *Surface, processActive: bool) void {
|
||||
pub fn close(self: *Surface, process_active: bool) void {
|
||||
self.closeWithConfirmation(process_active, .{ .surface = self });
|
||||
}
|
||||
|
||||
/// Close this surface.
|
||||
pub fn closeWithConfirmation(self: *Surface, process_active: bool, target: CloseDialog.Target) void {
|
||||
self.setSplitZoom(false);
|
||||
|
||||
// If we're not part of a window hierarchy, we never confirm
|
||||
// so we can just directly remove ourselves and exit.
|
||||
const window = self.container.window() orelse {
|
||||
self.container.remove();
|
||||
return;
|
||||
};
|
||||
|
||||
// If we have no process active we can just exit immediately.
|
||||
if (!processActive) {
|
||||
if (!process_active) {
|
||||
self.container.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup our basic message
|
||||
const alert = c.gtk_message_dialog_new(
|
||||
window.window,
|
||||
c.GTK_DIALOG_MODAL,
|
||||
c.GTK_MESSAGE_QUESTION,
|
||||
c.GTK_BUTTONS_YES_NO,
|
||||
"Close this terminal?",
|
||||
);
|
||||
c.gtk_message_dialog_format_secondary_text(
|
||||
@ptrCast(alert),
|
||||
"There is still a running process in the terminal. " ++
|
||||
"Closing the terminal will kill this process. " ++
|
||||
"Are you sure you want to close the terminal?\n\n" ++
|
||||
"Click 'No' to cancel and return to your terminal.",
|
||||
);
|
||||
|
||||
// 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(>kCloseConfirmation), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
c.gtk_widget_show(alert);
|
||||
CloseDialog.show(target) catch |err| {
|
||||
log.err("failed to open close dialog={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn controlInspector(
|
||||
@ -2081,18 +2050,6 @@ pub fn dimSurface(self: *Surface) void {
|
||||
c.gtk_overlay_add_overlay(self.overlay, self.unfocused_widget.?);
|
||||
}
|
||||
|
||||
fn gtkCloseConfirmation(
|
||||
alert: *c.GtkMessageDialog,
|
||||
response: c.gint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
c.gtk_window_destroy(@ptrCast(alert));
|
||||
if (response == c.GTK_RESPONSE_YES) {
|
||||
const self = userdataSelf(ud.?);
|
||||
self.container.remove();
|
||||
}
|
||||
}
|
||||
|
||||
fn userdataSelf(ud: *anyopaque) *Surface {
|
||||
return @ptrCast(@alignCast(ud));
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ const CoreSurface = @import("../../Surface.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const adwaita = @import("adwaita.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@ -132,54 +134,23 @@ fn needsConfirm(elem: Surface.Container.Elem) bool {
|
||||
/// Close the tab, asking for confirmation if any surface requests it.
|
||||
pub fn closeWithConfirmation(tab: *Tab) void {
|
||||
switch (tab.elem) {
|
||||
.surface => |s| s.close(s.core_surface.needsConfirmQuit()),
|
||||
.surface => |s| s.closeWithConfirmation(
|
||||
s.core_surface.needsConfirmQuit(),
|
||||
.{ .tab = tab },
|
||||
),
|
||||
.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);
|
||||
if (!needsConfirm(s.top_left) and !needsConfirm(s.bottom_right)) {
|
||||
tab.remove();
|
||||
return;
|
||||
}
|
||||
tab.remove();
|
||||
|
||||
CloseDialog.show(.{ .tab = tab }) catch |err| {
|
||||
log.err("failed to open close dialog={}", .{err});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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) return;
|
||||
tab.remove();
|
||||
}
|
||||
|
||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
_ = v;
|
||||
log.debug("tab box destroy", .{});
|
||||
|
@ -252,6 +252,7 @@ fn adwClosePage(
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
|
||||
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
|
||||
if (!self.forcing_close) tab.closeWithConfirmation();
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ const adwaita = @import("adwaita.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const TabView = @import("TabView.zig");
|
||||
const HeaderBar = @import("headerbar.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
const version = @import("version.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const i18n = @import("i18n.zig");
|
||||
@ -824,11 +825,7 @@ pub fn close(self: *Window) void {
|
||||
window.destroy();
|
||||
}
|
||||
|
||||
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
_ = v;
|
||||
log.debug("window close request", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
pub fn closeWithConfirmation(self: *Window) void {
|
||||
// If none of our surfaces need confirmation, we can just exit.
|
||||
for (self.app.core_app.surfaces.items) |surface| {
|
||||
if (surface.container.window()) |window| {
|
||||
@ -837,51 +834,21 @@ fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
}
|
||||
} else {
|
||||
self.close();
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup our basic message
|
||||
const alert = c.gtk_message_dialog_new(
|
||||
self.window,
|
||||
c.GTK_DIALOG_MODAL,
|
||||
c.GTK_MESSAGE_QUESTION,
|
||||
c.GTK_BUTTONS_YES_NO,
|
||||
"Close this window?",
|
||||
);
|
||||
c.gtk_message_dialog_format_secondary_text(
|
||||
@ptrCast(alert),
|
||||
"All terminal sessions in this window 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(>kCloseConfirmation), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
c.gtk_widget_show(alert);
|
||||
return true;
|
||||
CloseDialog.show(.{ .window = self }) catch |err| {
|
||||
log.err("failed to open close dialog={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkCloseConfirmation(
|
||||
alert: *c.GtkMessageDialog,
|
||||
response: c.gint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
c.gtk_window_destroy(@ptrCast(alert));
|
||||
if (response == c.GTK_RESPONSE_YES) {
|
||||
const self = userdataSelf(ud.?);
|
||||
self.close();
|
||||
}
|
||||
fn gtkCloseRequest(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
_ = v;
|
||||
log.debug("window close request", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
self.closeWithConfirmation();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// "destroy" signal for the window
|
||||
@ -978,7 +945,7 @@ fn gtkActionClose(
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
self.close();
|
||||
self.closeWithConfirmation();
|
||||
}
|
||||
|
||||
fn gtkActionNewWindow(
|
||||
|
@ -56,3 +56,8 @@ test "versionAtLeast" {
|
||||
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
|
||||
}
|
||||
|
||||
// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
|
||||
pub fn supportsDialogs() bool {
|
||||
return versionAtLeast(1, 5, 0);
|
||||
}
|
||||
|
Reference in New Issue
Block a user