Merge branch 'main' into issue-151-ssh-remote-port-proxying
This commit is contained in:
commit
d67090994e
61 changed files with 11220 additions and 614 deletions
|
|
@ -3555,7 +3555,14 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
private var pendingPaneClosePanelIds: [UUID: [UUID]] = [:]
|
||||
private var pendingClosedBrowserRestoreSnapshots: [TabID: ClosedBrowserPanelRestoreSnapshot] = [:]
|
||||
private var isApplyingTabSelection = false
|
||||
private var pendingTabSelection: (tabId: TabID, pane: PaneID)?
|
||||
private struct PendingTabSelectionRequest {
|
||||
let tabId: TabID
|
||||
let pane: PaneID
|
||||
let reassertAppKitFocus: Bool
|
||||
let focusIntent: PanelFocusIntent?
|
||||
let previousTerminalHostedView: GhosttySurfaceScrollView?
|
||||
}
|
||||
private var pendingTabSelection: PendingTabSelectionRequest?
|
||||
private var isReconcilingFocusState = false
|
||||
private var focusReconcileScheduled = false
|
||||
#if DEBUG
|
||||
|
|
@ -5604,6 +5611,19 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}()
|
||||
let shouldSuppressReentrantRefocus = trigger == .terminalFirstResponder && selectionAlreadyConverged
|
||||
#if DEBUG
|
||||
let targetPaneShort = targetPaneId.map { String($0.id.uuidString.prefix(5)) } ?? "nil"
|
||||
let focusedPaneShort = bonsplitController.focusedPaneId.map { String($0.id.uuidString.prefix(5)) } ?? "nil"
|
||||
let selectedTabShort = bonsplitController.focusedPaneId
|
||||
.flatMap { bonsplitController.selectedTab(inPane: $0)?.id }
|
||||
.map { String($0.uuid.uuidString.prefix(5)) } ?? "nil"
|
||||
let currentPanelShort = currentlyFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"focus.panel.begin workspace=\(id.uuidString.prefix(5)) " +
|
||||
"panel=\(panelId.uuidString.prefix(5)) trigger=\(String(describing: trigger)) " +
|
||||
"targetPane=\(targetPaneShort) focusedPane=\(focusedPaneShort) selectedTab=\(selectedTabShort) " +
|
||||
"converged=\(selectionAlreadyConverged ? 1 : 0) " +
|
||||
"currentPanel=\(currentPanelShort)"
|
||||
)
|
||||
if shouldSuppressReentrantRefocus {
|
||||
dlog(
|
||||
"focus.panel.skipReentrant panel=\(panelId.uuidString.prefix(5)) " +
|
||||
|
|
@ -5613,34 +5633,65 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
#endif
|
||||
|
||||
if let targetPaneId, !selectionAlreadyConverged {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"focus.panel.focusPane workspace=\(id.uuidString.prefix(5)) " +
|
||||
"panel=\(panelId.uuidString.prefix(5)) pane=\(targetPaneId.id.uuidString.prefix(5))"
|
||||
)
|
||||
#endif
|
||||
bonsplitController.focusPane(targetPaneId)
|
||||
}
|
||||
|
||||
if !selectionAlreadyConverged {
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"focus.panel.selectTab workspace=\(id.uuidString.prefix(5)) " +
|
||||
"panel=\(panelId.uuidString.prefix(5)) tab=\(tabId.uuid.uuidString.prefix(5))"
|
||||
)
|
||||
#endif
|
||||
bonsplitController.selectTab(tabId)
|
||||
}
|
||||
|
||||
// Also focus the underlying panel
|
||||
if let panel = panels[panelId] {
|
||||
if (currentlyFocusedPanelId != panelId || !selectionAlreadyConverged) && !shouldSuppressReentrantRefocus {
|
||||
panel.focus()
|
||||
}
|
||||
|
||||
if !shouldSuppressReentrantRefocus, let terminalPanel = panel as? TerminalPanel {
|
||||
// Avoid re-entrant focus loops when focus was initiated by AppKit first-responder
|
||||
// (becomeFirstResponder -> onFocus -> focusPanel).
|
||||
if !terminalPanel.hostedView.isSurfaceViewFirstResponder() {
|
||||
terminalPanel.hostedView.moveFocus(from: previousTerminalHostedView)
|
||||
}
|
||||
}
|
||||
}
|
||||
if let targetPaneId, !shouldSuppressReentrantRefocus {
|
||||
applyTabSelection(tabId: tabId, inPane: targetPaneId)
|
||||
if let targetPaneId {
|
||||
let activationIntent = panels[panelId]?.preferredFocusIntentForActivation()
|
||||
applyTabSelection(
|
||||
tabId: tabId,
|
||||
inPane: targetPaneId,
|
||||
reassertAppKitFocus: !shouldSuppressReentrantRefocus,
|
||||
focusIntent: activationIntent,
|
||||
previousTerminalHostedView: previousTerminalHostedView
|
||||
)
|
||||
}
|
||||
|
||||
if let browserPanel = panels[panelId] as? BrowserPanel {
|
||||
maybeAutoFocusBrowserAddressBarOnPanelFocus(browserPanel, trigger: trigger)
|
||||
}
|
||||
|
||||
if trigger == .terminalFirstResponder,
|
||||
panels[panelId] is TerminalPanel {
|
||||
scheduleTerminalFirstResponderReassert(panelId: panelId)
|
||||
}
|
||||
}
|
||||
|
||||
/// A terminal click can arrive while AppKit and bonsplit already look converged, which takes
|
||||
/// the re-entrant focus path and skips the normal explicit `ensureFocus` call. Re-assert focus
|
||||
/// on the next couple of turns so stale callbacks from split churn can't leave keyboard input
|
||||
/// attached to the wrong surface (#1147).
|
||||
private func scheduleTerminalFirstResponderReassert(panelId: UUID, remainingPasses: Int = 2) {
|
||||
guard remainingPasses > 0 else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self,
|
||||
self.focusedPanelId == panelId,
|
||||
let terminalPanel = self.terminalPanel(for: panelId) else {
|
||||
return
|
||||
}
|
||||
|
||||
terminalPanel.hostedView.ensureFocus(for: self.id, surfaceId: panelId)
|
||||
self.scheduleTerminalFirstResponderReassert(
|
||||
panelId: panelId,
|
||||
remainingPasses: remainingPasses - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func maybeAutoFocusBrowserAddressBarOnPanelFocus(
|
||||
|
|
@ -5753,9 +5804,28 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
|
||||
@discardableResult
|
||||
func toggleSplitZoom(panelId: UUID) -> Bool {
|
||||
let wasSplitZoomed = bonsplitController.isSplitZoomed
|
||||
guard let paneId = paneId(forPanelId: panelId) else { return false }
|
||||
guard bonsplitController.togglePaneZoom(inPane: paneId) else { return false }
|
||||
focusPanel(panelId)
|
||||
reconcileTerminalPortalVisibilityForCurrentRenderedLayout()
|
||||
reconcileBrowserPortalVisibilityForCurrentRenderedLayout(reason: "workspace.toggleSplitZoom")
|
||||
scheduleTerminalPortalVisibilityReconcileAfterSplitZoom(remainingPasses: 4)
|
||||
scheduleBrowserPortalVisibilityReconcileAfterSplitZoom(
|
||||
remainingPasses: 4,
|
||||
reason: "workspace.toggleSplitZoom"
|
||||
)
|
||||
scheduleTerminalGeometryReconcile()
|
||||
if let browserPanel = browserPanel(for: panelId) {
|
||||
browserPanel.preparePortalHostReplacementForNextDistinctClaim(
|
||||
inPane: paneId,
|
||||
reason: "workspace.toggleSplitZoom"
|
||||
)
|
||||
scheduleBrowserPortalReconcileAfterSplitZoom(panelId: panelId, remainingPasses: 4)
|
||||
if wasSplitZoomed && !bonsplitController.isSplitZoomed {
|
||||
scheduleBrowserSplitZoomExitFocusReassert(panelId: panelId, remainingPasses: 4)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -6012,6 +6082,248 @@ final class Workspace: Identifiable, ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func renderedVisiblePanelIdsForCurrentLayout() -> Set<UUID> {
|
||||
let renderedPaneIds = bonsplitController.zoomedPaneId.map { [$0] } ?? bonsplitController.allPaneIds
|
||||
var visiblePanelIds: Set<UUID> = []
|
||||
|
||||
for paneId in renderedPaneIds {
|
||||
let selectedTab = bonsplitController.selectedTab(inPane: paneId) ?? bonsplitController.tabs(inPane: paneId).first
|
||||
guard let selectedTab,
|
||||
let panelId = panelIdFromSurfaceId(selectedTab.id),
|
||||
panels[panelId] != nil else {
|
||||
continue
|
||||
}
|
||||
visiblePanelIds.insert(panelId)
|
||||
}
|
||||
|
||||
if let focusedPanelId,
|
||||
panels[focusedPanelId] != nil,
|
||||
let focusedPaneId = paneId(forPanelId: focusedPanelId),
|
||||
renderedPaneIds.contains(where: { $0.id == focusedPaneId.id }) {
|
||||
visiblePanelIds.insert(focusedPanelId)
|
||||
}
|
||||
|
||||
return visiblePanelIds
|
||||
}
|
||||
|
||||
private func reconcileTerminalPortalVisibilityForCurrentRenderedLayout() {
|
||||
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
|
||||
|
||||
for panel in panels.values {
|
||||
guard let terminalPanel = panel as? TerminalPanel else { continue }
|
||||
let shouldBeVisible = visiblePanelIds.contains(terminalPanel.id)
|
||||
terminalPanel.hostedView.setVisibleInUI(shouldBeVisible)
|
||||
terminalPanel.hostedView.setActive(shouldBeVisible && focusedPanelId == terminalPanel.id)
|
||||
TerminalWindowPortalRegistry.updateEntryVisibility(
|
||||
for: terminalPanel.hostedView,
|
||||
visibleInUI: shouldBeVisible
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func terminalPortalVisibilityNeedsFollowUp() -> Bool {
|
||||
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
|
||||
|
||||
for panel in panels.values {
|
||||
guard let terminalPanel = panel as? TerminalPanel else { continue }
|
||||
let shouldBeVisible = visiblePanelIds.contains(terminalPanel.id)
|
||||
let hostedView = terminalPanel.hostedView
|
||||
|
||||
if shouldBeVisible {
|
||||
if hostedView.isHidden || hostedView.window == nil || hostedView.superview == nil {
|
||||
return true
|
||||
}
|
||||
} else if !hostedView.isHidden {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func scheduleTerminalPortalVisibilityReconcileAfterSplitZoom(remainingPasses: Int) {
|
||||
guard remainingPasses > 0 else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
for window in NSApp.windows {
|
||||
window.contentView?.layoutSubtreeIfNeeded()
|
||||
window.contentView?.displayIfNeeded()
|
||||
}
|
||||
|
||||
self.reconcileTerminalPortalVisibilityForCurrentRenderedLayout()
|
||||
|
||||
if self.terminalPortalVisibilityNeedsFollowUp(), remainingPasses > 1 {
|
||||
self.scheduleTerminalPortalVisibilityReconcileAfterSplitZoom(
|
||||
remainingPasses: remainingPasses - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reconcileBrowserPortalVisibilityForCurrentRenderedLayout(reason: String) {
|
||||
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
|
||||
|
||||
for panel in panels.values {
|
||||
guard let browserPanel = panel as? BrowserPanel else { continue }
|
||||
let shouldBeVisible = visiblePanelIds.contains(browserPanel.id)
|
||||
if shouldBeVisible {
|
||||
BrowserWindowPortalRegistry.updateEntryVisibility(
|
||||
for: browserPanel.webView,
|
||||
visibleInUI: true,
|
||||
zPriority: 2
|
||||
)
|
||||
let anchorView = browserPanel.portalAnchorView
|
||||
let anchorReady =
|
||||
anchorView.window != nil &&
|
||||
anchorView.superview != nil &&
|
||||
anchorView.bounds.width > 1 &&
|
||||
anchorView.bounds.height > 1
|
||||
if anchorReady {
|
||||
BrowserWindowPortalRegistry.synchronizeForAnchor(anchorView)
|
||||
BrowserWindowPortalRegistry.refresh(
|
||||
webView: browserPanel.webView,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
} else {
|
||||
BrowserWindowPortalRegistry.updateEntryVisibility(
|
||||
for: browserPanel.webView,
|
||||
visibleInUI: false,
|
||||
zPriority: 0
|
||||
)
|
||||
BrowserWindowPortalRegistry.hide(
|
||||
webView: browserPanel.webView,
|
||||
source: reason
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func browserPortalVisibilityNeedsFollowUp() -> Bool {
|
||||
let visiblePanelIds = renderedVisiblePanelIdsForCurrentLayout()
|
||||
|
||||
for panel in panels.values {
|
||||
guard let browserPanel = panel as? BrowserPanel else { continue }
|
||||
guard visiblePanelIds.contains(browserPanel.id) else { continue }
|
||||
let anchorView = browserPanel.portalAnchorView
|
||||
let anchorReady =
|
||||
anchorView.window != nil &&
|
||||
anchorView.superview != nil &&
|
||||
anchorView.bounds.width > 1 &&
|
||||
anchorView.bounds.height > 1
|
||||
if !anchorReady ||
|
||||
browserPanel.webView.window == nil ||
|
||||
browserPanel.webView.superview == nil ||
|
||||
!BrowserWindowPortalRegistry.isWebView(browserPanel.webView, boundTo: anchorView) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func scheduleBrowserPortalVisibilityReconcileAfterSplitZoom(
|
||||
remainingPasses: Int,
|
||||
reason: String
|
||||
) {
|
||||
guard remainingPasses > 0 else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
for window in NSApp.windows {
|
||||
window.contentView?.layoutSubtreeIfNeeded()
|
||||
window.contentView?.displayIfNeeded()
|
||||
}
|
||||
|
||||
self.reconcileBrowserPortalVisibilityForCurrentRenderedLayout(reason: reason)
|
||||
|
||||
if self.browserPortalVisibilityNeedsFollowUp(), remainingPasses > 1 {
|
||||
self.scheduleBrowserPortalVisibilityReconcileAfterSplitZoom(
|
||||
remainingPasses: remainingPasses - 1,
|
||||
reason: reason
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Browser panes host WKWebView in the window portal. After pane zoom toggles,
|
||||
// force a few post-layout sync passes so the portal does not outlive the omnibar chrome.
|
||||
private func scheduleBrowserPortalReconcileAfterSplitZoom(panelId: UUID, remainingPasses: Int) {
|
||||
guard remainingPasses > 0 else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, let browserPanel = self.browserPanel(for: panelId) else { return }
|
||||
|
||||
for window in NSApp.windows {
|
||||
window.contentView?.layoutSubtreeIfNeeded()
|
||||
window.contentView?.displayIfNeeded()
|
||||
}
|
||||
|
||||
let anchorView = browserPanel.portalAnchorView
|
||||
let anchorReady =
|
||||
anchorView.window != nil &&
|
||||
anchorView.superview != nil &&
|
||||
anchorView.bounds.width > 1 &&
|
||||
anchorView.bounds.height > 1
|
||||
|
||||
if anchorReady {
|
||||
BrowserWindowPortalRegistry.synchronizeForAnchor(anchorView)
|
||||
BrowserWindowPortalRegistry.refresh(
|
||||
webView: browserPanel.webView,
|
||||
reason: "workspace.toggleSplitZoom"
|
||||
)
|
||||
}
|
||||
|
||||
let portalNeedsFollowUpPass =
|
||||
!anchorReady ||
|
||||
browserPanel.webView.window == nil ||
|
||||
browserPanel.webView.superview == nil
|
||||
if portalNeedsFollowUpPass {
|
||||
self.scheduleBrowserPortalReconcileAfterSplitZoom(
|
||||
panelId: panelId,
|
||||
remainingPasses: remainingPasses - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Browser panes can briefly keep the portal-hosted WKWebView visible while Bonsplit is
|
||||
// still rebuilding the unzoomed pane host. Reassert pane/tab selection after layout settles
|
||||
// so the SwiftUI chrome does not remain hidden until another browser focus command runs.
|
||||
private func scheduleBrowserSplitZoomExitFocusReassert(panelId: UUID, remainingPasses: Int) {
|
||||
guard remainingPasses > 0 else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, self.browserPanel(for: panelId) != nil else { return }
|
||||
guard let paneId = self.paneId(forPanelId: panelId),
|
||||
let tabId = self.surfaceIdFromPanelId(panelId) else { return }
|
||||
|
||||
let selectionConverged =
|
||||
self.bonsplitController.focusedPaneId == paneId &&
|
||||
self.bonsplitController.selectedTab(inPane: paneId)?.id == tabId
|
||||
let anchorReady: Bool = {
|
||||
guard let browserPanel = self.browserPanel(for: panelId) else { return false }
|
||||
let anchorView = browserPanel.portalAnchorView
|
||||
return
|
||||
anchorView.window != nil &&
|
||||
anchorView.superview != nil &&
|
||||
anchorView.bounds.width > 1 &&
|
||||
anchorView.bounds.height > 1
|
||||
}()
|
||||
|
||||
if !selectionConverged {
|
||||
self.focusPanel(panelId)
|
||||
self.scheduleFocusReconcile()
|
||||
}
|
||||
|
||||
if !selectionConverged || !anchorReady {
|
||||
self.scheduleBrowserSplitZoomExitFocusReassert(
|
||||
panelId: panelId,
|
||||
remainingPasses: remainingPasses - 1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleMovedTerminalRefresh(panelId: UUID) {
|
||||
guard terminalPanel(for: panelId) != nil else { return }
|
||||
|
||||
|
|
@ -6280,8 +6592,20 @@ extension Workspace: BonsplitDelegate {
|
|||
|
||||
/// Apply the side-effects of selecting a tab (unfocus others, focus this panel, update state).
|
||||
/// bonsplit doesn't always emit didSelectTab for programmatic selection paths (e.g. createTab).
|
||||
private func applyTabSelection(tabId: TabID, inPane pane: PaneID) {
|
||||
pendingTabSelection = (tabId: tabId, pane: pane)
|
||||
private func applyTabSelection(
|
||||
tabId: TabID,
|
||||
inPane pane: PaneID,
|
||||
reassertAppKitFocus: Bool = true,
|
||||
focusIntent: PanelFocusIntent? = nil,
|
||||
previousTerminalHostedView: GhosttySurfaceScrollView? = nil
|
||||
) {
|
||||
pendingTabSelection = PendingTabSelectionRequest(
|
||||
tabId: tabId,
|
||||
pane: pane,
|
||||
reassertAppKitFocus: reassertAppKitFocus,
|
||||
focusIntent: focusIntent,
|
||||
previousTerminalHostedView: previousTerminalHostedView
|
||||
)
|
||||
guard !isApplyingTabSelection else { return }
|
||||
isApplyingTabSelection = true
|
||||
defer {
|
||||
|
|
@ -6294,12 +6618,36 @@ extension Workspace: BonsplitDelegate {
|
|||
pendingTabSelection = nil
|
||||
iterations += 1
|
||||
if iterations > 8 { break }
|
||||
applyTabSelectionNow(tabId: request.tabId, inPane: request.pane)
|
||||
applyTabSelectionNow(
|
||||
tabId: request.tabId,
|
||||
inPane: request.pane,
|
||||
reassertAppKitFocus: request.reassertAppKitFocus,
|
||||
focusIntent: request.focusIntent,
|
||||
previousTerminalHostedView: request.previousTerminalHostedView
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func applyTabSelectionNow(tabId: TabID, inPane pane: PaneID) {
|
||||
private func applyTabSelectionNow(
|
||||
tabId: TabID,
|
||||
inPane pane: PaneID,
|
||||
reassertAppKitFocus: Bool,
|
||||
focusIntent: PanelFocusIntent?,
|
||||
previousTerminalHostedView: GhosttySurfaceScrollView?
|
||||
) {
|
||||
let previousFocusedPanelId = focusedPanelId
|
||||
#if DEBUG
|
||||
let focusedPaneBefore = bonsplitController.focusedPaneId.map { String($0.id.uuidString.prefix(5)) } ?? "nil"
|
||||
let selectedTabBefore = bonsplitController.focusedPaneId
|
||||
.flatMap { bonsplitController.selectedTab(inPane: $0)?.id }
|
||||
.map { String($0.uuid.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"focus.split.apply.begin workspace=\(id.uuidString.prefix(5)) " +
|
||||
"pane=\(pane.id.uuidString.prefix(5)) tab=\(tabId.uuid.uuidString.prefix(5)) " +
|
||||
"focusedPane=\(focusedPaneBefore) selectedTab=\(selectedTabBefore) " +
|
||||
"reassert=\(reassertAppKitFocus ? 1 : 0)"
|
||||
)
|
||||
#endif
|
||||
if bonsplitController.allPaneIds.contains(pane) {
|
||||
if bonsplitController.focusedPaneId != pane {
|
||||
bonsplitController.focusPane(pane)
|
||||
|
|
@ -6334,6 +6682,8 @@ extension Workspace: BonsplitDelegate {
|
|||
if shouldTreatCurrentEventAsExplicitFocusIntent() {
|
||||
markExplicitFocusIntent(on: panelId)
|
||||
}
|
||||
let activationIntent = focusIntent ?? panel.preferredFocusIntentForActivation()
|
||||
panel.prepareFocusIntentForActivation(activationIntent)
|
||||
|
||||
syncPinnedStateForTab(selectedTabId, panelId: panelId)
|
||||
syncUnreadBadgeStateForPanel(panelId)
|
||||
|
|
@ -6343,11 +6693,24 @@ extension Workspace: BonsplitDelegate {
|
|||
p.unfocus()
|
||||
}
|
||||
|
||||
panel.focus()
|
||||
if let focusWindow = activationWindow(for: panel) {
|
||||
yieldForeignOwnedFocusIfNeeded(
|
||||
in: focusWindow,
|
||||
targetPanelId: panelId,
|
||||
targetIntent: activationIntent
|
||||
)
|
||||
}
|
||||
|
||||
activatePanel(
|
||||
panel,
|
||||
focusIntent: activationIntent,
|
||||
reassertAppKitFocus: reassertAppKitFocus
|
||||
)
|
||||
let focusIntentAllowsBrowserOmnibarAutofocus =
|
||||
shouldTreatCurrentEventAsExplicitFocusIntent() ||
|
||||
TerminalController.socketCommandAllowsInAppFocusMutations()
|
||||
if let browserPanel = panel as? BrowserPanel,
|
||||
shouldAllowBrowserOmnibarAutofocus(for: activationIntent),
|
||||
previousFocusedPanelId != panelId || focusIntentAllowsBrowserOmnibarAutofocus {
|
||||
maybeAutoFocusBrowserAddressBarOnPanelFocus(browserPanel, trigger: .standard)
|
||||
}
|
||||
|
|
@ -6375,10 +6738,33 @@ extension Workspace: BonsplitDelegate {
|
|||
|
||||
// Converge AppKit first responder with bonsplit's selected tab in the focused pane.
|
||||
// Without this, keyboard input can remain on a different terminal than the blue tab indicator.
|
||||
if let terminalPanel = panel as? TerminalPanel {
|
||||
if reassertAppKitFocus, let terminalPanel = panel as? TerminalPanel {
|
||||
if shouldMoveTerminalSurfaceFocus(for: activationIntent),
|
||||
!terminalPanel.hostedView.isSurfaceViewFirstResponder() {
|
||||
#if DEBUG
|
||||
let previousExists = previousTerminalHostedView != nil ? 1 : 0
|
||||
dlog(
|
||||
"focus.split.moveFocus workspace=\(id.uuidString.prefix(5)) " +
|
||||
"panel=\(panelId.uuidString.prefix(5)) previousExists=\(previousExists) " +
|
||||
"to=\(panelId.uuidString.prefix(5))"
|
||||
)
|
||||
#endif
|
||||
terminalPanel.hostedView.moveFocus(from: previousTerminalHostedView)
|
||||
}
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"focus.split.ensureFocus workspace=\(id.uuidString.prefix(5)) " +
|
||||
"panel=\(panelId.uuidString.prefix(5)) pane=\(focusedPane.id.uuidString.prefix(5)) " +
|
||||
"tab=\(selectedTabId.uuid.uuidString.prefix(5)) intent=\(String(describing: activationIntent))"
|
||||
)
|
||||
#endif
|
||||
terminalPanel.hostedView.ensureFocus(for: id, surfaceId: panelId)
|
||||
}
|
||||
|
||||
if shouldRestoreFocusIntentAfterActivation(activationIntent) {
|
||||
_ = panel.restoreFocusIntent(activationIntent)
|
||||
}
|
||||
|
||||
// Update current directory if this is a terminal
|
||||
if let dir = panelDirectories[panelId] {
|
||||
currentDirectory = dir
|
||||
|
|
@ -6395,6 +6781,108 @@ extension Workspace: BonsplitDelegate {
|
|||
GhosttyNotificationKey.surfaceId: panelId
|
||||
]
|
||||
)
|
||||
#if DEBUG
|
||||
let prevPanelShort = previousFocusedPanelId.map { String($0.uuidString.prefix(5)) } ?? "nil"
|
||||
dlog(
|
||||
"focus.split.apply.end workspace=\(id.uuidString.prefix(5)) " +
|
||||
"panel=\(panelId.uuidString.prefix(5)) type=\(String(describing: type(of: panel))) " +
|
||||
"focusedPane=\(focusedPane.id.uuidString.prefix(5)) selectedTab=\(selectedTabId.uuid.uuidString.prefix(5)) " +
|
||||
"prevPanel=\(prevPanelShort)"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func activatePanel(
|
||||
_ panel: any Panel,
|
||||
focusIntent: PanelFocusIntent,
|
||||
reassertAppKitFocus: Bool
|
||||
) {
|
||||
if let terminalPanel = panel as? TerminalPanel {
|
||||
let shouldFocusTerminalSurface = shouldMoveTerminalSurfaceFocus(for: focusIntent)
|
||||
terminalPanel.surface.setFocus(shouldFocusTerminalSurface)
|
||||
terminalPanel.hostedView.setActive(true)
|
||||
if reassertAppKitFocus && shouldFocusTerminalSurface {
|
||||
terminalPanel.focus()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if let browserPanel = panel as? BrowserPanel {
|
||||
guard shouldFocusBrowserWebView(for: focusIntent) else { return }
|
||||
browserPanel.focus()
|
||||
return
|
||||
}
|
||||
|
||||
if reassertAppKitFocus {
|
||||
panel.focus()
|
||||
}
|
||||
}
|
||||
|
||||
private func activationWindow(for panel: any Panel) -> NSWindow? {
|
||||
if let terminalPanel = panel as? TerminalPanel {
|
||||
return terminalPanel.hostedView.window ?? NSApp.keyWindow ?? NSApp.mainWindow
|
||||
}
|
||||
if let browserPanel = panel as? BrowserPanel {
|
||||
return browserPanel.webView.window ?? browserPanel.portalAnchorView.window ?? NSApp.keyWindow ?? NSApp.mainWindow
|
||||
}
|
||||
return NSApp.keyWindow ?? NSApp.mainWindow
|
||||
}
|
||||
|
||||
private func yieldForeignOwnedFocusIfNeeded(
|
||||
in window: NSWindow,
|
||||
targetPanelId: UUID,
|
||||
targetIntent: PanelFocusIntent
|
||||
) {
|
||||
guard let firstResponder = window.firstResponder else { return }
|
||||
|
||||
for (panelId, panel) in panels where panelId != targetPanelId {
|
||||
guard let ownedIntent = panel.ownedFocusIntent(for: firstResponder, in: window) else { continue }
|
||||
#if DEBUG
|
||||
dlog(
|
||||
"focus.handoff.begin workspace=\(id.uuidString.prefix(5)) " +
|
||||
"fromPanel=\(panelId.uuidString.prefix(5)) toPanel=\(targetPanelId.uuidString.prefix(5)) " +
|
||||
"fromIntent=\(String(describing: ownedIntent)) toIntent=\(String(describing: targetIntent))"
|
||||
)
|
||||
#endif
|
||||
_ = panel.yieldFocusIntent(ownedIntent, in: window)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldMoveTerminalSurfaceFocus(for intent: PanelFocusIntent) -> Bool {
|
||||
switch intent {
|
||||
case .terminal(.findField):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldFocusBrowserWebView(for intent: PanelFocusIntent) -> Bool {
|
||||
switch intent {
|
||||
case .browser(.addressBar), .browser(.findField):
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldAllowBrowserOmnibarAutofocus(for intent: PanelFocusIntent) -> Bool {
|
||||
switch intent {
|
||||
case .browser(.webView), .panel:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldRestoreFocusIntentAfterActivation(_ intent: PanelFocusIntent) -> Bool {
|
||||
switch intent {
|
||||
case .browser(.addressBar), .browser(.findField), .terminal(.findField):
|
||||
return true
|
||||
case .panel, .browser(.webView), .terminal(.surface):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func beginNonFocusSplitFocusReassert(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue