macOS: quick terminal should retain menu if not frontmost

This is a bug I noticed in the following scenario:

  1. Open Ghostty
  2. Fullscreen normal terminal window (native fullscreen)
  3. Open quick terminal
  4. Move spaces, QT follows
  5. Fullscreen the quick terminal

The result was that the menu bar would not disappear since our app is
not frontmost but we set the fullscreen frame such that we expected it.
pull/7091/head
Mitchell Hashimoto 2025-04-14 10:50:30 -07:00
parent 453e6590e8
commit d1c15dbf07
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 51 additions and 8 deletions

View File

@ -45,7 +45,7 @@ class QuickTerminalController: BaseTerminalController {
object: nil) object: nil)
center.addObserver( center.addObserver(
self, self,
selector: #selector(onToggleFullscreen), selector: #selector(onToggleFullscreen(notification:)),
name: Ghostty.Notification.ghosttyToggleFullscreen, name: Ghostty.Notification.ghosttyToggleFullscreen,
object: nil) object: nil)
center.addObserver( center.addObserver(
@ -154,8 +154,18 @@ class QuickTerminalController: BaseTerminalController {
// current space. Animate out. // current space. Animate out.
animateOut() animateOut()
} else { } else {
// We've moved to a different space. Bring the quick terminal back // We've moved to a different space.
// into view.
// If we're fullscreen, we need to exit fullscreen because the visible
// bounds may have changed causing a new behavior.
if let fullscreenStyle, fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
DispatchQueue.main.async {
self.onToggleFullscreen()
}
}
// Make the window visible again on this space
DispatchQueue.main.async { DispatchQueue.main.async {
self.window?.makeKeyAndOrderFront(nil) self.window?.makeKeyAndOrderFront(nil)
} }
@ -479,9 +489,23 @@ class QuickTerminalController: BaseTerminalController {
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) { @objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard target == self.focusedSurface else { return } guard target == self.focusedSurface else { return }
onToggleFullscreen()
}
// We ignore the requested mode and always use non-native for the quick terminal private func onToggleFullscreen() {
toggleFullscreen(mode: .nonNative) // We ignore the configured fullscreen style and always use non-native
// because the way the quick terminal works doesn't support native.
//
// An additional detail is that if the is NOT frontmost, then our
// NSApp.presentationOptions will not take effect so we must always
// do the visible menu mode since we can't get rid of the menu.
let mode: FullscreenMode = if (NSApp.isFrontmost) {
.nonNative
} else {
.nonNativeVisibleMenu
}
toggleFullscreen(mode: mode)
} }
@objc private func ghosttyConfigDidChange(_ notification: Notification) { @objc private func ghosttyConfigDidChange(_ notification: Notification) {

View File

@ -275,7 +275,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// calculate this ourselves. // calculate this ourselves.
var frame = screen.frame var frame = screen.frame
if (!properties.hideMenu) { if (!NSApp.presentationOptions.contains(.autoHideMenuBar) &&
!NSApp.presentationOptions.contains(.hideMenuBar)) {
// We need to subtract the menu height since we're still showing it. // We need to subtract the menu height since we're still showing it.
frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0
@ -358,7 +359,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
// spaces. We do this because fullscreen spaces already hide the // spaces. We do this because fullscreen spaces already hide the
// menu and if we insert/remove this presentation option we get // menu and if we insert/remove this presentation option we get
// issues (see #7075) // issues (see #7075)
self.menu = CGSSpace.list(for: window.cgWindowId).allSatisfy { $0.type != .fullscreen } let activeSpace = CGSSpace.active()
let spaces = CGSSpace.list(for: window.cgWindowId)
if spaces.contains(activeSpace) {
self.menu = activeSpace.type != .fullscreen
} else {
self.menu = spaces.allSatisfy { $0.type != .fullscreen }
}
} }
} }
} }

View File

@ -1,5 +1,7 @@
import Cocoa import Cocoa
// MARK: Presentation Options
extension NSApplication { extension NSApplication {
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:] private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
@ -29,3 +31,13 @@ extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
hasher.combine(rawValue) hasher.combine(rawValue)
} }
} }
// MARK: Frontmost
extension NSApplication {
/// True if the application is frontmost. This isn't exactly the same as isActive because
/// an app can be active but not be frontmost if the window with activity is an NSPanel.
var isFrontmost: Bool {
NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier
}
}

View File

@ -54,7 +54,7 @@ struct CGSSpace: Hashable, CustomStringConvertible {
return .init(rawValue: space) return .init(rawValue: space)
} }
/// List the sapces for the given window. /// List the spaces for the given window.
static func list(for windowID: CGWindowID, mask: CGSSpaceMask = .allSpaces) -> [CGSSpace] { static func list(for windowID: CGWindowID, mask: CGSSpaceMask = .allSpaces) -> [CGSSpace] {
guard let spaces = CGSCopySpacesForWindows( guard let spaces = CGSCopySpacesForWindows(
CGSMainConnectionID(), CGSMainConnectionID(),