mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-16 06:46:09 +00:00
macos: native terminal style works with new subclasses
This commit is contained in:
@@ -402,12 +402,12 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A59630992AEE1C6400D64628 /* Terminal.xib */,
|
A59630992AEE1C6400D64628 /* Terminal.xib */,
|
||||||
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */,
|
|
||||||
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */,
|
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */,
|
||||||
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */,
|
|
||||||
A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */,
|
A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */,
|
||||||
A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */,
|
|
||||||
A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */,
|
A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */,
|
||||||
|
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */,
|
||||||
|
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */,
|
||||||
|
A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */,
|
||||||
A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */,
|
A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */,
|
||||||
);
|
);
|
||||||
path = "Window Styles";
|
path = "Window Styles";
|
||||||
|
@@ -569,6 +569,10 @@ class BaseTerminalController: NSWindowController,
|
|||||||
surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode)
|
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
|
// Ensure focus stays on the target surface. We lose focus when we do
|
||||||
// this so we need to grab it again.
|
// this so we need to grab it again.
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
@@ -14,6 +14,7 @@ class TerminalController: BaseTerminalController {
|
|||||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue }
|
guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue }
|
||||||
let config = appDelegate.ghostty.config
|
let config = appDelegate.ghostty.config
|
||||||
let nib = switch config.macosTitlebarStyle {
|
let nib = switch config.macosTitlebarStyle {
|
||||||
|
case "native": "Terminal"
|
||||||
case "tabs": defaultValue
|
case "tabs": defaultValue
|
||||||
case "hidden": "TerminalHiddenTitlebar"
|
case "hidden": "TerminalHiddenTitlebar"
|
||||||
case "transparent": "TerminalTransparentTitlebar"
|
case "transparent": "TerminalTransparentTitlebar"
|
||||||
@@ -132,6 +133,9 @@ class TerminalController: BaseTerminalController {
|
|||||||
if let window = window as? LegacyTerminalWindow {
|
if let window = window as? LegacyTerminalWindow {
|
||||||
window.surfaceIsZoomed = to.zoomed != nil
|
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 our surface tree is now nil then we close our window.
|
||||||
if (to.isEmpty) {
|
if (to.isEmpty) {
|
||||||
@@ -395,28 +399,44 @@ class TerminalController: BaseTerminalController {
|
|||||||
/// changes, when a window is closed, and when tabs are reordered
|
/// changes, when a window is closed, and when tabs are reordered
|
||||||
/// with the mouse.
|
/// with the mouse.
|
||||||
func relabelTabs() {
|
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,
|
// We only listen for frame changes if we have more than 1 window,
|
||||||
// otherwise the accessory view doesn't matter.
|
// otherwise the accessory view doesn't matter.
|
||||||
tabListenForFrame = windows.count > 1
|
tabListenForFrame = window?.tabbedWindows?.count ?? 0 > 1
|
||||||
|
|
||||||
for (tab, window) in zip(1..., windows) {
|
if let windows = window?.tabbedWindows as? [TerminalWindow] {
|
||||||
// We need to clear any windows beyond this because they have had
|
for (tab, window) in zip(1..., windows) {
|
||||||
// a keyEquivalent set previously.
|
// We need to clear any windows beyond this because they have had
|
||||||
guard tab <= 9 else {
|
// a keyEquivalent set previously.
|
||||||
window.keyEquivalent = ""
|
guard tab <= 9 else {
|
||||||
continue
|
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)"
|
// Legacy
|
||||||
if let equiv = ghostty.config.keyboardShortcut(for: action) {
|
if let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] {
|
||||||
window.keyEquivalent = "\(equiv)"
|
for (tab, window) in zip(1..., windows) {
|
||||||
} else {
|
// We need to clear any windows beyond this because they have had
|
||||||
window.keyEquivalent = ""
|
// 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"/>
|
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="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="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">
|
<view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
<rect key="frame" x="0.0" y="0.0" width="800" height="600"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
import SwiftUI
|
||||||
import GhosttyKit
|
import GhosttyKit
|
||||||
|
|
||||||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||||
@@ -42,6 +43,14 @@ class TerminalWindow: NSWindow {
|
|||||||
hideWindowButtons()
|
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
|
// Get our saved level
|
||||||
level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal
|
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 canBecomeKey: Bool { return true }
|
||||||
override var canBecomeMain: 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
|
// MARK: Positioning And Styling
|
||||||
|
|
||||||
/// This is called by the controller when there is a need to reset the window apperance.
|
/// 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() {
|
override func becomeMain() {
|
||||||
|
super.becomeMain()
|
||||||
|
|
||||||
guard let lastSurfaceConfig else { return }
|
guard let lastSurfaceConfig else { return }
|
||||||
syncAppearance(lastSurfaceConfig)
|
syncAppearance(lastSurfaceConfig)
|
||||||
|
|
||||||
|
@@ -27,6 +27,21 @@ extension NSView {
|
|||||||
return root
|
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.
|
/// Recursively finds and returns the first descendant view that has the given class name.
|
||||||
func firstDescendant(withClassName name: String) -> NSView? {
|
func firstDescendant(withClassName name: String) -> NSView? {
|
||||||
for subview in subviews {
|
for subview in subviews {
|
||||||
|
Reference in New Issue
Block a user