mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-05 19:08:17 +00:00
macos: make sure we're not registering unnecessary undos
This commit is contained in:
@@ -64,6 +64,7 @@
|
||||
A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; };
|
||||
A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366E2DF25D8300E04A10 /* Duration+Extension.swift */; };
|
||||
A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */; };
|
||||
A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636722DF4813000E04A10 /* UndoManager+Extension.swift */; };
|
||||
A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; };
|
||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; };
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; };
|
||||
@@ -171,6 +172,7 @@
|
||||
A586366A2DF0A98900E04A10 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = "<group>"; };
|
||||
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extension.swift"; sourceTree = "<group>"; };
|
||||
A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringUndoManager.swift; sourceTree = "<group>"; };
|
||||
A58636722DF4813000E04A10 /* UndoManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UndoManager+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = "<group>"; };
|
||||
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = "<group>"; };
|
||||
A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
@@ -447,6 +449,7 @@
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */,
|
||||
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
||||
A58636722DF4813000E04A10 /* UndoManager+Extension.swift */,
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
@@ -683,6 +686,7 @@
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||
A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
|
@@ -260,8 +260,8 @@ class BaseTerminalController: NSWindowController,
|
||||
self.alert = alert
|
||||
}
|
||||
|
||||
// MARK: Focus Management
|
||||
|
||||
// MARK: Split Tree Management
|
||||
|
||||
/// Find the next surface to focus when a node is being closed.
|
||||
/// Goes to previous split unless we're the leftmost leaf, then goes to next.
|
||||
private func findNextFocusTargetAfterClosing(node: SplitTree<Ghostty.SurfaceView>.Node) -> Ghostty.SurfaceView? {
|
||||
@@ -282,45 +282,63 @@ class BaseTerminalController: NSWindowController,
|
||||
///
|
||||
/// This does no confirmation and assumes confirmation is already done.
|
||||
private func removeSurfaceNode(_ node: SplitTree<Ghostty.SurfaceView>.Node) {
|
||||
let nextTarget = findNextFocusTargetAfterClosing(node: node)
|
||||
let oldFocused = focusedSurface
|
||||
let focused = node.contains { $0 == focusedSurface }
|
||||
|
||||
// Keep track of the old tree for undo management.
|
||||
let oldTree = surfaceTree
|
||||
|
||||
// Remove the node from the tree
|
||||
surfaceTree = surfaceTree.remove(node)
|
||||
|
||||
// Move focus if the closed surface was focused and we have a next target
|
||||
if let nextTarget, focused {
|
||||
let nextFocus: Ghostty.SurfaceView? = if node.contains(
|
||||
where: { $0 == focusedSurface }
|
||||
) {
|
||||
findNextFocusTargetAfterClosing(node: node)
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
replaceSurfaceTree(
|
||||
surfaceTree.remove(node),
|
||||
moveFocusTo: nextFocus,
|
||||
moveFocusFrom: focusedSurface,
|
||||
undoAction: "Close Terminal"
|
||||
)
|
||||
}
|
||||
|
||||
private func replaceSurfaceTree(
|
||||
_ newTree: SplitTree<Ghostty.SurfaceView>,
|
||||
moveFocusTo newView: Ghostty.SurfaceView? = nil,
|
||||
moveFocusFrom oldView: Ghostty.SurfaceView? = nil,
|
||||
undoAction: String? = nil
|
||||
) {
|
||||
// Setup our new split tree
|
||||
let oldTree = surfaceTree
|
||||
surfaceTree = newTree
|
||||
if let newView {
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: nextTarget, from: oldFocused)
|
||||
Ghostty.moveFocus(to: newView, from: oldView)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup our undo
|
||||
if let undoManager {
|
||||
undoManager.setActionName("Close Terminal")
|
||||
if let undoAction {
|
||||
undoManager.setActionName(undoAction)
|
||||
}
|
||||
undoManager.registerUndo(
|
||||
withTarget: self,
|
||||
expiresAfter: undoExpiration) { target in
|
||||
expiresAfter: undoExpiration
|
||||
) { target in
|
||||
target.surfaceTree = oldTree
|
||||
if let oldFocused {
|
||||
if let oldView {
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: oldFocused, from: target.focusedSurface)
|
||||
Ghostty.moveFocus(to: oldView, from: target.focusedSurface)
|
||||
}
|
||||
}
|
||||
|
||||
undoManager.registerUndo(
|
||||
withTarget: target,
|
||||
expiresAfter: target.undoExpiration) { target in
|
||||
target.closeSurfaceNode(
|
||||
node,
|
||||
withConfirmation: node.contains {
|
||||
$0.needsConfirmQuit
|
||||
}
|
||||
)
|
||||
expiresAfter: target.undoExpiration
|
||||
) { target in
|
||||
target.replaceSurfaceTree(
|
||||
newTree,
|
||||
moveFocusTo: newView,
|
||||
moveFocusFrom: target.focusedSurface,
|
||||
undoAction: undoAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -478,36 +496,11 @@ class BaseTerminalController: NSWindowController,
|
||||
return
|
||||
}
|
||||
|
||||
// Keep track of the old tree for undo
|
||||
let oldTree = surfaceTree
|
||||
|
||||
// Setup our new split tree
|
||||
surfaceTree = newTree
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: newView, from: oldView)
|
||||
}
|
||||
|
||||
// Setup our undo
|
||||
if let undoManager {
|
||||
undoManager.setActionName("New Split")
|
||||
undoManager.registerUndo(
|
||||
withTarget: self,
|
||||
expiresAfter: undoExpiration) { target in
|
||||
target.surfaceTree = oldTree
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: oldView, from: target.focusedSurface)
|
||||
}
|
||||
|
||||
undoManager.registerUndo(
|
||||
withTarget: target,
|
||||
expiresAfter: target.undoExpiration) { target in
|
||||
target.surfaceTree = newTree
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: newView, from: target.focusedSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
replaceSurfaceTree(
|
||||
newTree,
|
||||
moveFocusTo: newView,
|
||||
moveFocusFrom: oldView,
|
||||
undoAction: "New Split")
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidEqualizeSplits(_ notification: Notification) {
|
||||
|
@@ -210,14 +210,18 @@ class TerminalController: BaseTerminalController {
|
||||
undoManager.setActionName("New Window")
|
||||
undoManager.registerUndo(
|
||||
withTarget: c,
|
||||
expiresAfter: c.undoExpiration) { target in
|
||||
expiresAfter: c.undoExpiration
|
||||
) { target in
|
||||
// Close the window when undoing
|
||||
target.closeWindow(nil)
|
||||
undoManager.disableUndoRegistration {
|
||||
target.closeWindow(nil)
|
||||
}
|
||||
|
||||
// Register redo action
|
||||
undoManager.registerUndo(
|
||||
withTarget: ghostty,
|
||||
expiresAfter: target.undoExpiration) { ghostty in
|
||||
expiresAfter: target.undoExpiration
|
||||
) { ghostty in
|
||||
_ = TerminalController.newWindow(
|
||||
ghostty,
|
||||
withBaseConfig: baseConfig,
|
||||
@@ -314,14 +318,18 @@ class TerminalController: BaseTerminalController {
|
||||
undoManager.setActionName("New Tab")
|
||||
undoManager.registerUndo(
|
||||
withTarget: controller,
|
||||
expiresAfter: controller.undoExpiration) { target in
|
||||
expiresAfter: controller.undoExpiration
|
||||
) { target in
|
||||
// Close the tab when undoing
|
||||
target.closeTab(nil)
|
||||
|
||||
undoManager.disableUndoRegistration {
|
||||
target.closeTab(nil)
|
||||
}
|
||||
|
||||
// Register redo action
|
||||
undoManager.registerUndo(
|
||||
withTarget: ghostty,
|
||||
expiresAfter: target.undoExpiration) { ghostty in
|
||||
expiresAfter: target.undoExpiration
|
||||
) { ghostty in
|
||||
_ = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: parent,
|
||||
@@ -617,14 +625,16 @@ class TerminalController: BaseTerminalController {
|
||||
undoManager.setActionName("Close Tab")
|
||||
undoManager.registerUndo(
|
||||
withTarget: ghostty,
|
||||
expiresAfter: undoExpiration) { ghostty in
|
||||
expiresAfter: undoExpiration
|
||||
) { ghostty in
|
||||
let newController = TerminalController(ghostty, with: undoState)
|
||||
|
||||
// Register redo action
|
||||
undoManager.registerUndo(
|
||||
withTarget: newController,
|
||||
expiresAfter: newController.undoExpiration) { target in
|
||||
target.closeTab(nil)
|
||||
expiresAfter: newController.undoExpiration
|
||||
) { target in
|
||||
target.closeTabImmediately()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,7 +664,7 @@ class TerminalController: BaseTerminalController {
|
||||
undoManager.registerUndo(
|
||||
withTarget: newController,
|
||||
expiresAfter: newController.undoExpiration) { target in
|
||||
target.closeWindow(nil)
|
||||
target.closeWindowImmediately()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,11 @@ class ExpiringUndoManager: UndoManager {
|
||||
// Ignore instantly expiring undos
|
||||
guard duration.timeInterval > 0 else { return }
|
||||
|
||||
// Ignore when undo registration is disabled. UndoManager still lets
|
||||
// registration happen then cancels later but I was seeing some
|
||||
// weird behavior with this so let's just guard on it.
|
||||
guard self.isUndoRegistrationEnabled else { return }
|
||||
|
||||
let expiringTarget = ExpiringTarget(
|
||||
target,
|
||||
expiresAfter: duration,
|
||||
@@ -64,7 +69,10 @@ class ExpiringUndoManager: UndoManager {
|
||||
// Call super to handle standard removal
|
||||
super.removeAllActions(withTarget: target)
|
||||
|
||||
if !(target is ExpiringTarget) {
|
||||
// If the target is an expiring target, remove it.
|
||||
if let expiring = target as? ExpiringTarget {
|
||||
expiringTargets.remove(expiring)
|
||||
} else {
|
||||
// Find and remove any ExpiringTarget instances that wrap this target.
|
||||
expiringTargets
|
||||
.filter { $0.target == nil || $0.target === (target as AnyObject) }
|
||||
|
20
macos/Sources/Helpers/Extensions/UndoManager+Extension.swift
Normal file
20
macos/Sources/Helpers/Extensions/UndoManager+Extension.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
extension UndoManager {
|
||||
/// A Boolean value that indicates whether the undo manager is currently performing
|
||||
/// either an undo or redo operation.
|
||||
var isUndoingOrRedoing: Bool {
|
||||
isUndoing || isRedoing
|
||||
}
|
||||
|
||||
/// Temporarily disables undo registration while executing the provided handler.
|
||||
///
|
||||
/// This method provides a convenient way to perform operations without recording them
|
||||
/// in the undo stack. It ensures that undo registration is properly re-enabled even
|
||||
/// if the handler throws an error.
|
||||
func disableUndoRegistration(handler: () -> Void) {
|
||||
disableUndoRegistration()
|
||||
handler()
|
||||
enableUndoRegistration()
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user