Added a hopeful quit blocking dialog.
This commit is contained in:
+131
-2
@@ -106,6 +106,21 @@ updating: bool = false,
|
||||
/// doesn't try to close a tab we're already destroying.
|
||||
closing: bool = false,
|
||||
|
||||
/// Set once quitting has been settled, so the `close-request` that follows goes
|
||||
/// straight through instead of asking the same question twice. See `quit`.
|
||||
quit_confirmed: bool = false,
|
||||
|
||||
/// Set while the confirmation is on screen.
|
||||
///
|
||||
/// Belt and braces. libadwaita gets to a close-request before this does while
|
||||
/// one of its dialogs is open, and answers it by closing the dialog — so a
|
||||
/// second press of the window-manager binding cancels the question rather than
|
||||
/// reaching here at all. This is what catches it if that ever stops being true,
|
||||
/// because the alternative is a second dialog stacked on the first. It stays in
|
||||
/// step either way: every route out of the dialog emits `response`, including
|
||||
/// the one libadwaita takes.
|
||||
confirming_quit: bool = false,
|
||||
|
||||
/// Saved tab templates, read from the config file at startup.
|
||||
layouts: Layouts,
|
||||
|
||||
@@ -413,6 +428,17 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
|
||||
);
|
||||
window.as(gtk.Widget).addController(keys.as(gtk.EventController));
|
||||
|
||||
// Asking before the window goes away. This has to be `close-request`
|
||||
// rather than `destroy`: it is the one signal that can still say no, and
|
||||
// by the time `destroy` arrives the decision has been made.
|
||||
_ = gtk.Window.signals.close_request.connect(
|
||||
window,
|
||||
*Window,
|
||||
&onCloseRequest,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Free our own state once GTK is done with the window. Doing this on
|
||||
// `destroy` rather than `close-request` means no further events can
|
||||
// arrive for widgets whose user data we're about to free.
|
||||
@@ -1517,8 +1543,10 @@ fn closeTab(self: *Window, tab: *Tab) void {
|
||||
self.discardTab(tab);
|
||||
|
||||
if (self.tabs.items.len == 0) {
|
||||
// Teardown of our own state happens in onDestroy.
|
||||
self.window.as(gtk.Window).close();
|
||||
// Teardown of our own state happens in onDestroy. Without asking: the
|
||||
// last tab closing *is* the answer to the question, and there is
|
||||
// nothing left for a confirmation to offer to keep.
|
||||
self.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1800,6 +1828,107 @@ fn onViewEmpty(ctx: ?*anyopaque) void {
|
||||
tab.window.closeTab(tab);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Quitting
|
||||
//
|
||||
// A window here is not a shell but an arrangement of them — tabs, splits, and
|
||||
// whatever each one is in the middle of — and closing it exits every one of
|
||||
// those at once. That is a lot to hang off a single keystroke, which under a
|
||||
// tiling compositor is exactly where it hangs: niri's close binding is a
|
||||
// modifier away from the ones that move focus between windows, and it does not
|
||||
// ask. So we do.
|
||||
|
||||
/// The window has been asked to close. Returning non-zero keeps it.
|
||||
///
|
||||
/// Three ways through here. Confirmation switched off, or a decision already
|
||||
/// taken, and the close goes ahead. Otherwise the question goes up and the
|
||||
/// window stays until it is answered.
|
||||
///
|
||||
/// Note that a close arriving *while* the question is up does not reach this at
|
||||
/// all — see `confirming_quit`. It dismisses the dialog, which is a cancel, so
|
||||
/// leaning on the binding never costs the window.
|
||||
fn onCloseRequest(_: *adw.ApplicationWindow, self: *Window) callconv(.c) c_int {
|
||||
if (self.quit_confirmed) return 0;
|
||||
if (!Settings.get().confirm_quit) return 0;
|
||||
|
||||
// Already asked, and still waiting for the answer. Unreachable in practice;
|
||||
// see the field.
|
||||
if (self.confirming_quit) return 1;
|
||||
|
||||
self.confirming_quit = true;
|
||||
self.askBeforeQuitting();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// Close the window without asking.
|
||||
///
|
||||
/// For the paths where the question has already been answered — the
|
||||
/// confirmation being accepted, and the last tab closing, which is a decision
|
||||
/// to close this window made one tab at a time.
|
||||
fn quit(self: *Window) void {
|
||||
self.quit_confirmed = true;
|
||||
self.window.as(gtk.Window).close();
|
||||
}
|
||||
|
||||
/// Put the question up: an Adwaita alert dialog over the window it is about.
|
||||
fn askBeforeQuitting(self: *Window) void {
|
||||
const dialog = adw.AlertDialog.new("Quit Playpen?", null);
|
||||
|
||||
// The count is the reason for asking at all. "Quit?" over one shell is a
|
||||
// shrug; over nine tabs of work it is the whole point of the dialog, and
|
||||
// it is also the quickest way to notice you are about to close the wrong
|
||||
// window. The buffer is sized so the format cannot fail, but the fallback
|
||||
// says the same thing without the number rather than nothing at all.
|
||||
var buf: [160]u8 = undefined;
|
||||
const body: [:0]const u8 = std.fmt.bufPrintZ(
|
||||
&buf,
|
||||
"{d} tab{s} will close, and every shell in {s} will exit.",
|
||||
.{
|
||||
self.tabs.items.len,
|
||||
if (self.tabs.items.len == 1) "" else "s",
|
||||
if (self.tabs.items.len == 1) "it" else "them",
|
||||
},
|
||||
) catch "Every tab will close, and every shell in them will exit.";
|
||||
dialog.setBody(body.ptr);
|
||||
|
||||
dialog.addResponse("cancel", "Keep Working");
|
||||
dialog.addResponse("quit", "Quit");
|
||||
|
||||
// Destructive, and *not* the default: Escape and Enter both have to land on
|
||||
// keeping the window, or the dialog is one more keystroke to fumble rather
|
||||
// than a guard against fumbling one.
|
||||
dialog.setResponseAppearance("quit", .destructive);
|
||||
dialog.setDefaultResponse("cancel");
|
||||
dialog.setCloseResponse("cancel");
|
||||
|
||||
_ = adw.AlertDialog.signals.response.connect(
|
||||
dialog,
|
||||
*Window,
|
||||
&onQuitResponse,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
dialog.as(adw.Dialog).present(self.window.as(gtk.Widget));
|
||||
}
|
||||
|
||||
/// The question was answered — by a button, by Escape, or by the dialog being
|
||||
/// dismissed, which `close_response` has already turned into "cancel".
|
||||
fn onQuitResponse(_: *adw.AlertDialog, response: [*:0]u8, self: *Window) callconv(.c) void {
|
||||
self.confirming_quit = false;
|
||||
if (!std.mem.eql(u8, std.mem.span(response), "quit")) return;
|
||||
|
||||
// Not from here: this runs while the dialog is closing, and destroying the
|
||||
// window it is parented to out from under it is how that turns into a
|
||||
// crash. One trip back through the main loop and the dialog is gone.
|
||||
_ = glib.idleAddOnce(&onQuitIdle, self);
|
||||
}
|
||||
|
||||
fn onQuitIdle(data: ?*anyopaque) callconv(.c) void {
|
||||
const self: *Window = @ptrCast(@alignCast(data.?));
|
||||
self.quit();
|
||||
}
|
||||
|
||||
/// GTK has finished with the window: release everything we allocated.
|
||||
fn onDestroy(_: *adw.ApplicationWindow, self: *Window) callconv(.c) void {
|
||||
if (self.closing) return;
|
||||
|
||||
Reference in New Issue
Block a user