mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-15 22:36:14 +00:00
macos: native terminal style works with new subclasses
This commit is contained in:
@@ -402,12 +402,12 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A59630992AEE1C6400D64628 /* Terminal.xib */,
|
||||
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */,
|
||||
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */,
|
||||
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */,
|
||||
A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */,
|
||||
A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */,
|
||||
A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */,
|
||||
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */,
|
||||
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */,
|
||||
A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */,
|
||||
A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */,
|
||||
);
|
||||
path = "Window Styles";
|
||||
|
@@ -569,6 +569,10 @@ class BaseTerminalController: NSWindowController,
|
||||
surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode)
|
||||
}
|
||||
|
||||
// Move focus to our window. Importantly this ensures that if we click the
|
||||
// reset zoom button in a tab bar of an unfocused tab that we become focused.
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
|
||||
// Ensure focus stays on the target surface. We lose focus when we do
|
||||
// this so we need to grab it again.
|
||||
DispatchQueue.main.async {
|
||||
|
@@ -14,6 +14,7 @@ class TerminalController: BaseTerminalController {
|
||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue }
|
||||
let config = appDelegate.ghostty.config
|
||||
let nib = switch config.macosTitlebarStyle {
|
||||
case "native": "Terminal"
|
||||
case "tabs": defaultValue
|
||||
case "hidden": "TerminalHiddenTitlebar"
|
||||
case "transparent": "TerminalTransparentTitlebar"
|
||||
@@ -132,6 +133,9 @@ class TerminalController: BaseTerminalController {
|
||||
if let window = window as? LegacyTerminalWindow {
|
||||
window.surfaceIsZoomed = to.zoomed != nil
|
||||
}
|
||||
if let window = window as? TerminalWindow {
|
||||
window.surfaceIsZoomed2 = to.zoomed != nil
|
||||
}
|
||||
|
||||
// If our surface tree is now nil then we close our window.
|
||||
if (to.isEmpty) {
|
||||
@@ -395,28 +399,44 @@ class TerminalController: BaseTerminalController {
|
||||
/// changes, when a window is closed, and when tabs are reordered
|
||||
/// with the mouse.
|
||||
func relabelTabs() {
|
||||
// Reset this to false. It'll be set back to true later.
|
||||
tabListenForFrame = false
|
||||
|
||||
guard let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] else { return }
|
||||
|
||||
// We only listen for frame changes if we have more than 1 window,
|
||||
// otherwise the accessory view doesn't matter.
|
||||
tabListenForFrame = windows.count > 1
|
||||
tabListenForFrame = window?.tabbedWindows?.count ?? 0 > 1
|
||||
|
||||
for (tab, window) in zip(1..., windows) {
|
||||
// We need to clear any windows beyond this because they have had
|
||||
// a keyEquivalent set previously.
|
||||
guard tab <= 9 else {
|
||||
window.keyEquivalent = ""
|
||||
continue
|
||||
if let windows = window?.tabbedWindows as? [TerminalWindow] {
|
||||
for (tab, window) in zip(1..., windows) {
|
||||
// We need to clear any windows beyond this because they have had
|
||||
// a keyEquivalent set previously.
|
||||
guard tab <= 9 else {
|
||||
window.keyEquivalent2 = ""
|
||||
continue
|
||||
}
|
||||
|
||||
let action = "goto_tab:\(tab)"
|
||||
if let equiv = ghostty.config.keyboardShortcut(for: action) {
|
||||
window.keyEquivalent2 = "\(equiv)"
|
||||
} else {
|
||||
window.keyEquivalent2 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let action = "goto_tab:\(tab)"
|
||||
if let equiv = ghostty.config.keyboardShortcut(for: action) {
|
||||
window.keyEquivalent = "\(equiv)"
|
||||
} else {
|
||||
window.keyEquivalent = ""
|
||||
// Legacy
|
||||
if let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] {
|
||||
for (tab, window) in zip(1..., windows) {
|
||||
// We need to clear any windows beyond this because they have had
|
||||
// a keyEquivalent set previously.
|
||||
guard tab <= 9 else {
|
||||
window.keyEquivalent = ""
|
||||
continue
|
||||
}
|
||||
|
||||
let action = "goto_tab:\(tab)"
|
||||
if let equiv = ghostty.config.keyboardShortcut(for: action) {
|
||||
window.keyEquivalent = "\(equiv)"
|
||||
} else {
|
||||
window.keyEquivalent = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -17,7 +17,7 @@
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1512" height="948"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="3008" height="1661"/>
|
||||
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||
@@ -42,6 +43,14 @@ class TerminalWindow: NSWindow {
|
||||
hideWindowButtons()
|
||||
}
|
||||
|
||||
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
||||
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
|
||||
// where buttons were not clickable.
|
||||
let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton])
|
||||
stackView.setHuggingPriority(.defaultHigh, for: .horizontal)
|
||||
stackView.spacing = 3
|
||||
tab.accessoryView = stackView
|
||||
|
||||
// Get our saved level
|
||||
level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal
|
||||
}
|
||||
@@ -51,6 +60,85 @@ class TerminalWindow: NSWindow {
|
||||
override var canBecomeKey: Bool { return true }
|
||||
override var canBecomeMain: Bool { return true }
|
||||
|
||||
override func becomeKey() {
|
||||
super.becomeKey()
|
||||
resetZoomTabButton.contentTintColor = .controlAccentColor
|
||||
}
|
||||
|
||||
override func resignKey() {
|
||||
super.resignKey()
|
||||
resetZoomTabButton.contentTintColor = .secondaryLabelColor
|
||||
}
|
||||
|
||||
override func mergeAllWindows(_ sender: Any?) {
|
||||
super.mergeAllWindows(sender)
|
||||
|
||||
// It takes an event loop cycle to merge all the windows so we set a
|
||||
// short timer to relabel the tabs (issue #1902)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||
self?.terminalController?.relabelTabs()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Tab Key Equivalents
|
||||
|
||||
// TODO: rename once Legacy window removes
|
||||
var keyEquivalent2: String? = nil {
|
||||
didSet {
|
||||
// When our key equivalent is set, we must update the tab label.
|
||||
guard let keyEquivalent2 else {
|
||||
keyEquivalentLabel.attributedStringValue = NSAttributedString()
|
||||
return
|
||||
}
|
||||
|
||||
keyEquivalentLabel.attributedStringValue = NSAttributedString(
|
||||
string: "\(keyEquivalent2) ",
|
||||
attributes: [
|
||||
.font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize),
|
||||
.foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// The label that has the key equivalent for tab views.
|
||||
private lazy var keyEquivalentLabel: NSTextField = {
|
||||
let label = NSTextField(labelWithAttributedString: NSAttributedString())
|
||||
label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal)
|
||||
label.postsFrameChangedNotifications = true
|
||||
return label
|
||||
}()
|
||||
|
||||
// MARK: Surface Zoom
|
||||
|
||||
/// Set to true if a surface is currently zoomed to show the reset zoom button.
|
||||
var surfaceIsZoomed2: Bool = false {
|
||||
didSet {
|
||||
// Show/hide our reset zoom button depending on if we're zoomed.
|
||||
// We want to show it if we are zoomed.
|
||||
resetZoomTabButton.isHidden = !surfaceIsZoomed2
|
||||
}
|
||||
}
|
||||
|
||||
private lazy var resetZoomTabButton: NSButton = generateResetZoomButton()
|
||||
|
||||
private func generateResetZoomButton() -> NSButton {
|
||||
let button = NSButton()
|
||||
button.isHidden = true
|
||||
button.target = terminalController
|
||||
button.action = #selector(TerminalController.splitZoom(_:))
|
||||
button.isBordered = false
|
||||
button.allowsExpansionToolTips = true
|
||||
button.toolTip = "Reset Zoom"
|
||||
button.contentTintColor = .controlAccentColor
|
||||
button.state = .on
|
||||
button.image = NSImage(named:"ResetZoom")
|
||||
button.frame = NSRect(x: 0, y: 0, width: 20, height: 20)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.widthAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
button.heightAnchor.constraint(equalToConstant: 20).isActive = true
|
||||
return button
|
||||
}
|
||||
|
||||
// MARK: Positioning And Styling
|
||||
|
||||
/// This is called by the controller when there is a need to reset the window apperance.
|
||||
|
@@ -26,6 +26,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
||||
}
|
||||
|
||||
override func becomeMain() {
|
||||
super.becomeMain()
|
||||
|
||||
guard let lastSurfaceConfig else { return }
|
||||
syncAppearance(lastSurfaceConfig)
|
||||
|
||||
|
@@ -27,6 +27,21 @@ extension NSView {
|
||||
return root
|
||||
}
|
||||
|
||||
/// Checks if a view contains another view in its hierarchy.
|
||||
func contains(_ view: NSView) -> Bool {
|
||||
if self == view {
|
||||
return true
|
||||
}
|
||||
|
||||
for subview in subviews {
|
||||
if subview.contains(view) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// Recursively finds and returns the first descendant view that has the given class name.
|
||||
func firstDescendant(withClassName name: String) -> NSView? {
|
||||
for subview in subviews {
|
||||
|
Reference in New Issue
Block a user