From 97433d3aa151b609f315ffa732aea522f5e35271 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 17 Dec 2023 20:38:52 -0800 Subject: [PATCH] macos: close all windows Fixes #1052 This implements a `close_all_windows` binding in the core and implements it for macOS specifically. This will ask for close confirmation if any surface in any of the windows requires confirmation. This is bound by default to option+shift+command+w to match Safari. The binding is generall option+command+w but users may expect this to also mean "Close All Other Tabs" which is the changed behavior if any tabs are present in a standard macOS application. So I chose to follow Safari instead. This doesn't implement this feature for GTK, that's left as an exercise for a contributor. --- macos/Sources/AppDelegate.swift | 7 +++ .../Features/About/AboutController.swift | 7 ++- .../Features/Terminal/TerminalManager.swift | 46 +++++++++++++++++++ macos/Sources/MainMenu.xib | 7 +++ src/Surface.zig | 6 +++ src/config/Config.zig | 5 ++ src/input/Binding.zig | 8 +++- 7 files changed, 83 insertions(+), 3 deletions(-) diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index 4b0398767..44e11ef28 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -21,6 +21,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti @IBOutlet private var menuSplitVertical: NSMenuItem? @IBOutlet private var menuClose: NSMenuItem? @IBOutlet private var menuCloseWindow: NSMenuItem? + @IBOutlet private var menuCloseAllWindows: NSMenuItem? @IBOutlet private var menuCopy: NSMenuItem? @IBOutlet private var menuPaste: NSMenuItem? @@ -218,6 +219,7 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti syncMenuShortcut(action: "new_tab", menuItem: self.menuNewTab) syncMenuShortcut(action: "close_surface", menuItem: self.menuClose) syncMenuShortcut(action: "close_window", menuItem: self.menuCloseWindow) + syncMenuShortcut(action: "close_all_windows", menuItem: self.menuCloseAllWindows) syncMenuShortcut(action: "new_split:right", menuItem: self.menuSplitHorizontal) syncMenuShortcut(action: "new_split:down", menuItem: self.menuSplitVertical) @@ -360,6 +362,11 @@ class AppDelegate: NSObject, ObservableObject, NSApplicationDelegate, UNUserNoti NSApp.activate(ignoringOtherApps: true) } + @IBAction func closeAllWindows(_ sender: Any?) { + terminalManager.closeAllWindows() + AboutController.shared.hide() + } + @IBAction func showAbout(_ sender: Any?) { AboutController.shared.show() } diff --git a/macos/Sources/Features/About/AboutController.swift b/macos/Sources/Features/About/AboutController.swift index 2c6882eea..897e2918b 100644 --- a/macos/Sources/Features/About/AboutController.swift +++ b/macos/Sources/Features/About/AboutController.swift @@ -16,8 +16,11 @@ class AboutController: NSWindowController, NSWindowDelegate { // MARK: - Functions func show() { - guard let window = window else { return } - window.makeKeyAndOrderFront(nil) + window?.makeKeyAndOrderFront(nil) + } + + func hide() { + window?.close() } //MARK: - First Responder diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index c532285ef..24e742971 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -189,6 +189,52 @@ class TerminalManager { } } + /// Close all windows, asking for confirmation if necessary. + func closeAllWindows() { + var needsConfirm: Bool = false + for w in self.windows { + if (w.controller.surfaceTree?.needsConfirmQuit() ?? false) { + needsConfirm = true + break + } + } + + if (!needsConfirm) { + for w in self.windows { + w.controller.close() + } + + return + } + + // If we don't have a main window, we just close all windows because + // we have no window to show the modal on top of. I'm sure there's a way + // to do an app-level alert but I don't know how and this case should never + // really happen. + guard let alertWindow = mainWindow?.controller.window else { + for w in self.windows { + w.controller.close() + } + + return + } + + // If we need confirmation by any, show one confirmation for all windows + let alert = NSAlert() + alert.messageText = "Close All Windows?" + alert.informativeText = "All terminal sessions will be terminated." + alert.addButton(withTitle: "Close All Windows") + alert.addButton(withTitle: "Cancel") + alert.alertStyle = .warning + alert.beginSheetModal(for: alertWindow, completionHandler: { response in + if (response == .alertFirstButtonReturn) { + for w in self.windows { + w.controller.close() + } + } + }) + } + /// Relabels all the tabs with the proper keyboard shortcut. func relabelAllTabs() { for w in windows { diff --git a/macos/Sources/MainMenu.xib b/macos/Sources/MainMenu.xib index 69ee9fe38..1b6c88fa3 100644 --- a/macos/Sources/MainMenu.xib +++ b/macos/Sources/MainMenu.xib @@ -15,6 +15,7 @@ + @@ -134,6 +135,12 @@ + + + + + + diff --git a/src/Surface.zig b/src/Surface.zig index f79818738..cb781fe0a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2885,6 +2885,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .close_window => try self.app.closeSurface(self), + .close_all_windows => { + if (@hasDecl(apprt.Surface, "closeAllWindows")) { + self.rt_surface.closeAllWindows(); + } else log.warn("runtime doesn't implement closeAllWindows", .{}); + }, + .quit => try self.app.setQuit(), } diff --git a/src/config/Config.zig b/src/config/Config.zig index 1275c8a82..5c4109223 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1183,6 +1183,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { .{ .key = .w, .mods = .{ .super = true, .shift = true } }, .{ .close_window = {} }, ); + try result.keybind.set.put( + alloc, + .{ .key = .w, .mods = .{ .super = true, .shift = true, .alt = true } }, + .{ .close_all_windows = {} }, + ); try result.keybind.set.put( alloc, .{ .key = .t, .mods = .{ .super = true } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 19bdcdc27..95449b60a 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -221,12 +221,18 @@ pub const Action = union(enum) { reload_config: void, /// Close the current "surface", whether that is a window, tab, split, - /// etc. This only closes ONE surface. + /// etc. This only closes ONE surface. This will trigger close confirmation + /// as configured. close_surface: void, /// Close the window, regardless of how many tabs or splits there may be. + /// This will trigger close confirmation as configured. close_window: void, + /// Close all windows. This will trigger close confirmation as configured. + /// This only works for macOS currently. + close_all_windows: void, + /// Toggle fullscreen mode of window. toggle_fullscreen: void,