Merge remote-tracking branch 'origin/main' into pr-317-session-persistence
# Conflicts: # Sources/AppDelegate.swift
This commit is contained in:
commit
ea33e3adbd
15 changed files with 2401 additions and 344 deletions
|
|
@ -984,14 +984,6 @@ private func commandPaletteWindowOverlayController(for window: NSWindow) -> Wind
|
|||
return controller
|
||||
}
|
||||
|
||||
private struct CommandPaletteRowFramePreferenceKey: PreferenceKey {
|
||||
static var defaultValue: [Int: CGRect] = [:]
|
||||
|
||||
static func reduce(value: inout [Int: CGRect], nextValue: () -> [Int: CGRect]) {
|
||||
value.merge(nextValue(), uniquingKeysWith: { _, rhs in rhs })
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkspaceMountPolicy {
|
||||
// Keep only the selected workspace mounted to minimize layer-tree traversal.
|
||||
static let maxMountedWorkspaces = 1
|
||||
|
|
@ -1127,8 +1119,8 @@ struct ContentView: View {
|
|||
@State private var commandPaletteRenameDraft: String = ""
|
||||
@State private var commandPaletteSelectedResultIndex: Int = 0
|
||||
@State private var commandPaletteHoveredResultIndex: Int?
|
||||
@State private var commandPaletteLastSelectionIndex: Int = 0
|
||||
@State private var commandPaletteRowFrames: [Int: CGRect] = [:]
|
||||
@State private var commandPaletteScrollTargetIndex: Int?
|
||||
@State private var commandPaletteScrollTargetAnchor: UnitPoint?
|
||||
@State private var commandPaletteRestoreFocusTarget: CommandPaletteRestoreFocusTarget?
|
||||
@State private var commandPaletteUsageHistoryByCommandId: [String: CommandPaletteUsageEntry] = [:]
|
||||
@AppStorage(CommandPaletteRenameSelectionSettings.selectAllOnFocusKey)
|
||||
|
|
@ -1204,11 +1196,6 @@ struct ContentView: View {
|
|||
case kind
|
||||
}
|
||||
|
||||
enum CommandPaletteScrollAnchor: Equatable {
|
||||
case top
|
||||
case bottom
|
||||
}
|
||||
|
||||
private struct CommandPaletteTrailingLabel {
|
||||
let text: String
|
||||
let style: CommandPaletteTrailingLabelStyle
|
||||
|
|
@ -1284,6 +1271,10 @@ struct ContentView: View {
|
|||
static let panelHasUnread = "panel.hasUnread"
|
||||
|
||||
static let updateHasAvailable = "update.hasAvailable"
|
||||
|
||||
static func terminalOpenTargetAvailable(_ target: TerminalDirectoryOpenTarget) -> String {
|
||||
"terminal.openTarget.\(target.rawValue).available"
|
||||
}
|
||||
}
|
||||
|
||||
private struct CommandPaletteCommandContribution {
|
||||
|
|
@ -1757,6 +1748,7 @@ struct ContentView: View {
|
|||
WindowDragHandleView()
|
||||
|
||||
TitlebarLeadingInsetReader(inset: $titlebarLeadingInset)
|
||||
.allowsHitTesting(false)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
if isFullScreen && !sidebarState.isVisible {
|
||||
|
|
@ -1772,6 +1764,7 @@ struct ContentView: View {
|
|||
.font(.system(size: 13, weight: .bold))
|
||||
.foregroundColor(fakeTitlebarTextColor)
|
||||
.lineLimit(1)
|
||||
.allowsHitTesting(false)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
|
@ -1784,9 +1777,6 @@ struct ContentView: View {
|
|||
.frame(height: titlebarPadding)
|
||||
.frame(maxWidth: .infinity)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture(count: 2) {
|
||||
NSApp.keyWindow?.zoom(nil)
|
||||
}
|
||||
.background(fakeTitlebarBackground)
|
||||
.overlay(alignment: .bottom) {
|
||||
Rectangle()
|
||||
|
|
@ -2231,6 +2221,9 @@ struct ContentView: View {
|
|||
// Do not make the entire background draggable; it interferes with drag gestures
|
||||
// like sidebar tab reordering in multi-window mode.
|
||||
window.isMovableByWindowBackground = false
|
||||
// Keep the window immovable by default so titlebar controls (like the folder icon)
|
||||
// cannot accidentally initiate native window drags.
|
||||
window.isMovable = false
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
|
||||
// Track this window for fullscreen notifications
|
||||
|
|
@ -2487,7 +2480,7 @@ struct ContentView: View {
|
|||
private var commandPaletteCommandListView: some View {
|
||||
let visibleResults = Array(commandPaletteResults)
|
||||
let selectedIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
let commandPaletteListMaxHeight: CGFloat = 216
|
||||
let commandPaletteListMaxHeight: CGFloat = 450
|
||||
let commandPaletteRowHeight: CGFloat = 24
|
||||
let commandPaletteEmptyStateHeight: CGFloat = 44
|
||||
let commandPaletteListContentHeight = visibleResults.isEmpty
|
||||
|
|
@ -2531,133 +2524,85 @@ struct ContentView: View {
|
|||
|
||||
Divider()
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
if visibleResults.isEmpty {
|
||||
Text(commandPaletteEmptyStateText)
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 12)
|
||||
} else {
|
||||
ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in
|
||||
let isSelected = index == selectedIndex
|
||||
let isHovered = commandPaletteHoveredResultIndex == index
|
||||
let rowBackground: Color = isSelected
|
||||
? Color.accentColor.opacity(0.12)
|
||||
: (isHovered ? Color.primary.opacity(0.08) : .clear)
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
if visibleResults.isEmpty {
|
||||
Text(commandPaletteEmptyStateText)
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 12)
|
||||
} else {
|
||||
ForEach(Array(visibleResults.enumerated()), id: \.element.id) { index, result in
|
||||
let isSelected = index == selectedIndex
|
||||
let isHovered = commandPaletteHoveredResultIndex == index
|
||||
let rowBackground: Color = isSelected
|
||||
? Color.accentColor.opacity(0.12)
|
||||
: (isHovered ? Color.primary.opacity(0.08) : .clear)
|
||||
|
||||
Button {
|
||||
runCommandPaletteCommand(result.command)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
commandPaletteHighlightedTitleText(
|
||||
result.command.title,
|
||||
matchedIndices: result.titleMatchIndices
|
||||
)
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
|
||||
if let trailingLabel = commandPaletteTrailingLabel(for: result.command) {
|
||||
switch trailingLabel.style {
|
||||
case .shortcut:
|
||||
Text(trailingLabel.text)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 1)
|
||||
.background(Color.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 4, style: .continuous))
|
||||
case .kind:
|
||||
Text(trailingLabel.text)
|
||||
.font(.system(size: 11, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(rowBackground)
|
||||
.background(
|
||||
GeometryReader { geometry in
|
||||
Color.clear.preference(
|
||||
key: CommandPaletteRowFramePreferenceKey.self,
|
||||
value: [index: geometry.frame(in: .named("commandPaletteListScroll"))]
|
||||
)
|
||||
}
|
||||
Button {
|
||||
runCommandPaletteCommand(result.command)
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
commandPaletteHighlightedTitleText(
|
||||
result.command.title,
|
||||
matchedIndices: result.titleMatchIndices
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.id(index)
|
||||
.onHover { hovering in
|
||||
if hovering {
|
||||
commandPaletteHoveredResultIndex = index
|
||||
} else if commandPaletteHoveredResultIndex == index {
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
.font(.system(size: 13, weight: .regular))
|
||||
.lineLimit(1)
|
||||
Spacer()
|
||||
|
||||
if let trailingLabel = commandPaletteTrailingLabel(for: result.command) {
|
||||
switch trailingLabel.style {
|
||||
case .shortcut:
|
||||
Text(trailingLabel.text)
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 1)
|
||||
.background(Color.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 4, style: .continuous))
|
||||
case .kind:
|
||||
Text(trailingLabel.text)
|
||||
.font(.system(size: 11, weight: .regular))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 9)
|
||||
.padding(.vertical, 2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(rowBackground)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.id(index)
|
||||
.onHover { hovering in
|
||||
if hovering {
|
||||
commandPaletteHoveredResultIndex = index
|
||||
} else if commandPaletteHoveredResultIndex == index {
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Force a fresh row tree per query so rendered labels/actions stay in lockstep.
|
||||
.id(commandPaletteQuery)
|
||||
}
|
||||
.coordinateSpace(name: "commandPaletteListScroll")
|
||||
.frame(height: commandPaletteListHeight)
|
||||
.onChange(of: commandPaletteSelectedResultIndex) { _ in
|
||||
guard !visibleResults.isEmpty else { return }
|
||||
let index = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
let previousIndex = commandPaletteLastSelectionIndex
|
||||
defer { commandPaletteLastSelectionIndex = index }
|
||||
|
||||
guard let anchorDecision = Self.commandPaletteScrollAnchor(
|
||||
selectedIndex: index,
|
||||
previousIndex: previousIndex,
|
||||
resultCount: visibleResults.count,
|
||||
selectedFrame: commandPaletteRowFrames[index],
|
||||
viewportHeight: commandPaletteListHeight,
|
||||
contentHeight: commandPaletteListContentHeight
|
||||
) else { return }
|
||||
|
||||
let anchor: UnitPoint
|
||||
switch anchorDecision {
|
||||
case .top:
|
||||
anchor = .top
|
||||
case .bottom:
|
||||
anchor = .bottom
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(.easeOut(duration: 0.1)) {
|
||||
proxy.scrollTo(index, anchor: anchor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: visibleResults.count) { _ in
|
||||
commandPaletteLastSelectionIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
}
|
||||
.onPreferenceChange(CommandPaletteRowFramePreferenceKey.self) { frames in
|
||||
commandPaletteRowFrames = frames
|
||||
guard !visibleResults.isEmpty else { return }
|
||||
let index = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
guard let anchorDecision = Self.commandPaletteEdgeVisibilityCorrectionAnchor(
|
||||
selectedIndex: index,
|
||||
resultCount: visibleResults.count,
|
||||
selectedFrame: frames[index],
|
||||
viewportHeight: commandPaletteListHeight,
|
||||
contentHeight: commandPaletteListContentHeight
|
||||
) else { return }
|
||||
let anchor: UnitPoint = anchorDecision == .top ? .top : .bottom
|
||||
DispatchQueue.main.async {
|
||||
withAnimation(.easeOut(duration: 0.08)) {
|
||||
proxy.scrollTo(index, anchor: anchor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollTargetLayout()
|
||||
// Force a fresh row tree per query so rendered labels/actions stay in lockstep.
|
||||
.id(commandPaletteQuery)
|
||||
}
|
||||
.frame(height: commandPaletteListHeight)
|
||||
.scrollPosition(
|
||||
id: Binding(
|
||||
get: { commandPaletteScrollTargetIndex },
|
||||
// Ignore passive readback so manual scrolling doesn't mutate selection-follow state.
|
||||
set: { _ in }
|
||||
),
|
||||
anchor: commandPaletteScrollTargetAnchor
|
||||
)
|
||||
.onChange(of: commandPaletteSelectedResultIndex) { _ in
|
||||
updateCommandPaletteScrollTarget(resultCount: visibleResults.count, animated: true)
|
||||
}
|
||||
|
||||
// Keep Esc-to-close behavior without showing footer controls.
|
||||
|
|
@ -2672,20 +2617,19 @@ struct ContentView: View {
|
|||
}
|
||||
.onAppear {
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteLastSelectionIndex = commandPaletteSelectedResultIndex
|
||||
commandPaletteRowFrames = [:]
|
||||
updateCommandPaletteScrollTarget(resultCount: visibleResults.count, animated: false)
|
||||
resetCommandPaletteSearchFocus()
|
||||
}
|
||||
.onChange(of: commandPaletteQuery) { _ in
|
||||
commandPaletteSelectedResultIndex = 0
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteLastSelectionIndex = 0
|
||||
commandPaletteRowFrames = [:]
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
syncCommandPaletteDebugStateForObservedWindow()
|
||||
}
|
||||
.onChange(of: visibleResults.count) { _ in
|
||||
commandPaletteSelectedResultIndex = commandPaletteSelectedIndex(resultCount: visibleResults.count)
|
||||
commandPaletteLastSelectionIndex = commandPaletteSelectedResultIndex
|
||||
updateCommandPaletteScrollTarget(resultCount: visibleResults.count, animated: false)
|
||||
if let hoveredIndex = commandPaletteHoveredResultIndex, hoveredIndex >= visibleResults.count {
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
}
|
||||
|
|
@ -3288,18 +3232,29 @@ struct ContentView: View {
|
|||
if let panelContext = focusedPanelContext {
|
||||
let workspace = panelContext.workspace
|
||||
let panelId = panelContext.panelId
|
||||
let panelIsTerminal = panelContext.panel.panelType == .terminal
|
||||
snapshot.setBool(CommandPaletteContextKeys.hasFocusedPanel, true)
|
||||
snapshot.setString(
|
||||
CommandPaletteContextKeys.panelName,
|
||||
panelDisplayName(workspace: workspace, panelId: panelId, fallback: panelContext.panel.displayTitle)
|
||||
)
|
||||
snapshot.setBool(CommandPaletteContextKeys.panelIsBrowser, panelContext.panel.panelType == .browser)
|
||||
snapshot.setBool(CommandPaletteContextKeys.panelIsTerminal, panelContext.panel.panelType == .terminal)
|
||||
snapshot.setBool(CommandPaletteContextKeys.panelIsTerminal, panelIsTerminal)
|
||||
snapshot.setBool(CommandPaletteContextKeys.panelHasCustomName, workspace.panelCustomTitles[panelId] != nil)
|
||||
snapshot.setBool(CommandPaletteContextKeys.panelShouldPin, !workspace.isPanelPinned(panelId))
|
||||
let hasUnread = workspace.manualUnreadPanelIds.contains(panelId)
|
||||
|| notificationStore.hasUnreadNotification(forTabId: workspace.id, surfaceId: panelId)
|
||||
snapshot.setBool(CommandPaletteContextKeys.panelHasUnread, hasUnread)
|
||||
|
||||
if panelIsTerminal {
|
||||
let availableTargets = TerminalDirectoryOpenTarget.cachedLiveAvailableTargets
|
||||
for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets {
|
||||
snapshot.setBool(
|
||||
CommandPaletteContextKeys.terminalOpenTargetAvailable(target),
|
||||
availableTargets.contains(target)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if case .updateAvailable = updateViewModel.effectiveState {
|
||||
|
|
@ -3710,15 +3665,20 @@ struct ContentView: View {
|
|||
)
|
||||
)
|
||||
|
||||
contributions.append(
|
||||
CommandPaletteCommandContribution(
|
||||
commandId: "palette.terminalOpenDirectory",
|
||||
title: constant("Open Current Directory in IDE"),
|
||||
subtitle: terminalPanelSubtitle,
|
||||
keywords: ["terminal", "directory", "open", "ide", "code", "default app"],
|
||||
when: { $0.bool(CommandPaletteContextKeys.panelIsTerminal) }
|
||||
for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets {
|
||||
contributions.append(
|
||||
CommandPaletteCommandContribution(
|
||||
commandId: target.commandPaletteCommandId,
|
||||
title: constant(target.commandPaletteTitle),
|
||||
subtitle: terminalPanelSubtitle,
|
||||
keywords: target.commandPaletteKeywords,
|
||||
when: { context in
|
||||
context.bool(CommandPaletteContextKeys.panelIsTerminal)
|
||||
&& context.bool(CommandPaletteContextKeys.terminalOpenTargetAvailable(target))
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
contributions.append(
|
||||
CommandPaletteCommandContribution(
|
||||
commandId: "palette.terminalFind",
|
||||
|
|
@ -3981,9 +3941,11 @@ struct ContentView: View {
|
|||
_ = tabManager.createBrowserSplit(direction: .right, url: url)
|
||||
}
|
||||
|
||||
registry.register(commandId: "palette.terminalOpenDirectory") {
|
||||
if !openFocusedDirectoryInDefaultApp() {
|
||||
NSSound.beep()
|
||||
for target in TerminalDirectoryOpenTarget.commandPaletteShortcutTargets {
|
||||
registry.register(commandId: target.commandPaletteCommandId) {
|
||||
if !openFocusedDirectory(in: target) {
|
||||
NSSound.beep()
|
||||
}
|
||||
}
|
||||
}
|
||||
registry.register(commandId: "palette.terminalFind") {
|
||||
|
|
@ -4047,61 +4009,43 @@ struct ContentView: View {
|
|||
return min(max(commandPaletteSelectedResultIndex, 0), resultCount - 1)
|
||||
}
|
||||
|
||||
static func commandPaletteScrollAnchor(
|
||||
static func commandPaletteScrollPositionAnchor(
|
||||
selectedIndex: Int,
|
||||
previousIndex: Int,
|
||||
resultCount: Int,
|
||||
selectedFrame: CGRect?,
|
||||
viewportHeight: CGFloat,
|
||||
contentHeight: CGFloat,
|
||||
epsilon: CGFloat = 0.5
|
||||
) -> CommandPaletteScrollAnchor? {
|
||||
resultCount: Int
|
||||
) -> UnitPoint? {
|
||||
guard resultCount > 0 else { return nil }
|
||||
guard contentHeight > viewportHeight else { return nil }
|
||||
|
||||
// Always pin edges exactly into view when selection reaches first/last.
|
||||
if selectedIndex <= 0 {
|
||||
return .top
|
||||
return UnitPoint.top
|
||||
}
|
||||
if selectedIndex >= resultCount - 1 {
|
||||
return .bottom
|
||||
return UnitPoint.bottom
|
||||
}
|
||||
|
||||
if let frame = selectedFrame,
|
||||
frame.minY >= (0 - epsilon),
|
||||
frame.maxY <= (viewportHeight + epsilon) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return selectedIndex >= previousIndex ? .bottom : .top
|
||||
return nil
|
||||
}
|
||||
|
||||
static func commandPaletteEdgeVisibilityCorrectionAnchor(
|
||||
selectedIndex: Int,
|
||||
resultCount: Int,
|
||||
selectedFrame: CGRect?,
|
||||
viewportHeight: CGFloat,
|
||||
contentHeight: CGFloat,
|
||||
epsilon: CGFloat = 0.5
|
||||
) -> CommandPaletteScrollAnchor? {
|
||||
guard resultCount > 0 else { return nil }
|
||||
guard contentHeight > viewportHeight else { return nil }
|
||||
|
||||
let isTop = selectedIndex <= 0
|
||||
let isBottom = selectedIndex >= (resultCount - 1)
|
||||
guard isTop || isBottom else { return nil }
|
||||
|
||||
guard let frame = selectedFrame else {
|
||||
return isTop ? .top : .bottom
|
||||
private func updateCommandPaletteScrollTarget(resultCount: Int, animated: Bool) {
|
||||
guard resultCount > 0 else {
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
return
|
||||
}
|
||||
|
||||
if isTop {
|
||||
let topDelta = abs(frame.minY)
|
||||
return topDelta > epsilon ? .top : nil
|
||||
}
|
||||
let selectedIndex = commandPaletteSelectedIndex(resultCount: resultCount)
|
||||
commandPaletteScrollTargetAnchor = Self.commandPaletteScrollPositionAnchor(
|
||||
selectedIndex: selectedIndex,
|
||||
resultCount: resultCount
|
||||
)
|
||||
|
||||
let bottomDelta = abs(frame.maxY - viewportHeight)
|
||||
return bottomDelta > epsilon ? .bottom : nil
|
||||
let assignTarget = {
|
||||
commandPaletteScrollTargetIndex = selectedIndex
|
||||
}
|
||||
if animated {
|
||||
withAnimation(.easeOut(duration: 0.1)) {
|
||||
assignTarget()
|
||||
}
|
||||
} else {
|
||||
assignTarget()
|
||||
}
|
||||
}
|
||||
|
||||
private func moveCommandPaletteSelection(by delta: Int) {
|
||||
|
|
@ -4295,8 +4239,8 @@ struct ContentView: View {
|
|||
commandPaletteRenameDraft = ""
|
||||
commandPaletteSelectedResultIndex = 0
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteLastSelectionIndex = 0
|
||||
commandPaletteRowFrames = [:]
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
resetCommandPaletteSearchFocus()
|
||||
syncCommandPaletteDebugStateForObservedWindow()
|
||||
}
|
||||
|
|
@ -4309,8 +4253,8 @@ struct ContentView: View {
|
|||
commandPaletteRenameDraft = ""
|
||||
commandPaletteSelectedResultIndex = 0
|
||||
commandPaletteHoveredResultIndex = nil
|
||||
commandPaletteLastSelectionIndex = 0
|
||||
commandPaletteRowFrames = [:]
|
||||
commandPaletteScrollTargetIndex = nil
|
||||
commandPaletteScrollTargetAnchor = nil
|
||||
isCommandPaletteSearchFocused = false
|
||||
isCommandPaletteRenameFocused = false
|
||||
commandPaletteRestoreFocusTarget = nil
|
||||
|
|
@ -4537,9 +4481,22 @@ struct ContentView: View {
|
|||
return NSWorkspace.shared.open(url)
|
||||
}
|
||||
|
||||
private func openFocusedDirectoryInDefaultApp() -> Bool {
|
||||
private func openFocusedDirectory(in target: TerminalDirectoryOpenTarget) -> Bool {
|
||||
guard let directoryURL = focusedTerminalDirectoryURL() else { return false }
|
||||
return NSWorkspace.shared.open(directoryURL)
|
||||
return openFocusedDirectory(directoryURL, in: target)
|
||||
}
|
||||
|
||||
private func openFocusedDirectory(_ directoryURL: URL, in target: TerminalDirectoryOpenTarget) -> Bool {
|
||||
switch target {
|
||||
case .finder:
|
||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: directoryURL.path)
|
||||
return true
|
||||
default:
|
||||
guard let applicationURL = target.applicationURL() else { return false }
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
NSWorkspace.shared.open([directoryURL], withApplicationAt: applicationURL, configuration: configuration)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private func focusedTerminalDirectoryURL() -> URL? {
|
||||
|
|
@ -7417,9 +7374,21 @@ private struct DraggableFolderIconRepresentable: NSViewRepresentable {
|
|||
}
|
||||
}
|
||||
|
||||
private final class DraggableFolderNSView: NSView, NSDraggingSource {
|
||||
final class DraggableFolderNSView: NSView, NSDraggingSource {
|
||||
private final class FolderIconImageView: NSImageView {
|
||||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
}
|
||||
|
||||
var directory: String
|
||||
private var imageView: NSImageView!
|
||||
private var imageView: FolderIconImageView!
|
||||
private var previousWindowMovableState: Bool?
|
||||
private weak var suppressedWindow: NSWindow?
|
||||
private var hasActiveDragSession = false
|
||||
private var didArmWindowDragSuppression = false
|
||||
|
||||
private func formatPoint(_ point: NSPoint) -> String {
|
||||
String(format: "(%.1f,%.1f)", point.x, point.y)
|
||||
}
|
||||
|
||||
init(directory: String) {
|
||||
self.directory = directory
|
||||
|
|
@ -7435,8 +7404,10 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
|
|||
NSSize(width: 16, height: 16)
|
||||
}
|
||||
|
||||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
|
||||
private func setupImageView() {
|
||||
imageView = NSImageView()
|
||||
imageView = FolderIconImageView()
|
||||
imageView.imageScaling = .scaleProportionallyDown
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(imageView)
|
||||
|
|
@ -7461,9 +7432,40 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
|
|||
return context == .outsideApplication ? [.copy, .link] : .copy
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) {
|
||||
hasActiveDragSession = false
|
||||
restoreWindowMovableStateIfNeeded()
|
||||
#if DEBUG
|
||||
dlog("folder.dragStart dir=\(directory)")
|
||||
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
|
||||
let windowOrigin = window.map { formatPoint($0.frame.origin) } ?? "nil"
|
||||
dlog("folder.dragEnd dir=\(directory) operation=\(operation.rawValue) screen=\(formatPoint(screenPoint)) nowMovable=\(nowMovable) windowOrigin=\(windowOrigin)")
|
||||
#endif
|
||||
}
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||
guard bounds.contains(point) else { return nil }
|
||||
maybeDisableWindowDraggingEarly(trigger: "hitTest")
|
||||
let hit = super.hitTest(point)
|
||||
#if DEBUG
|
||||
let hitDesc = hit.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
let imageHit = (hit === imageView)
|
||||
let wasMovable = previousWindowMovableState.map(String.init) ?? "nil"
|
||||
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
|
||||
dlog("folder.hitTest point=\(formatPoint(point)) hit=\(hitDesc) imageViewHit=\(imageHit) returning=DraggableFolderNSView wasMovable=\(wasMovable) nowMovable=\(nowMovable)")
|
||||
#endif
|
||||
return self
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
maybeDisableWindowDraggingEarly(trigger: "mouseDown")
|
||||
hasActiveDragSession = false
|
||||
#if DEBUG
|
||||
let localPoint = convert(event.locationInWindow, from: nil)
|
||||
let responderDesc = window?.firstResponder.map { String(describing: type(of: $0)) } ?? "nil"
|
||||
let wasMovable = previousWindowMovableState.map(String.init) ?? "nil"
|
||||
let nowMovable = window.map { String($0.isMovable) } ?? "nil"
|
||||
let windowOrigin = window.map { formatPoint($0.frame.origin) } ?? "nil"
|
||||
dlog("folder.mouseDown dir=\(directory) point=\(formatPoint(localPoint)) firstResponder=\(responderDesc) wasMovable=\(wasMovable) nowMovable=\(nowMovable) windowOrigin=\(windowOrigin)")
|
||||
#endif
|
||||
let fileURL = URL(fileURLWithPath: directory)
|
||||
let draggingItem = NSDraggingItem(pasteboardWriter: fileURL as NSURL)
|
||||
|
|
@ -7472,7 +7474,19 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
|
|||
iconImage.size = NSSize(width: 32, height: 32)
|
||||
draggingItem.setDraggingFrame(bounds, contents: iconImage)
|
||||
|
||||
beginDraggingSession(with: [draggingItem], event: event, source: self)
|
||||
let session = beginDraggingSession(with: [draggingItem], event: event, source: self)
|
||||
hasActiveDragSession = true
|
||||
#if DEBUG
|
||||
let itemCount = session.draggingPasteboard.pasteboardItems?.count ?? 0
|
||||
dlog("folder.dragStart dir=\(directory) pasteboardItems=\(itemCount)")
|
||||
#endif
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
super.mouseUp(with: event)
|
||||
if !hasActiveDragSession {
|
||||
restoreWindowMovableStateIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
override func rightMouseDown(with event: NSEvent) {
|
||||
|
|
@ -7541,6 +7555,59 @@ private final class DraggableFolderNSView: NSView, NSDraggingSource {
|
|||
// Open "Computer" view in Finder (shows all volumes)
|
||||
NSWorkspace.shared.open(URL(fileURLWithPath: "/", isDirectory: true))
|
||||
}
|
||||
|
||||
private func restoreWindowMovableStateIfNeeded() {
|
||||
guard didArmWindowDragSuppression || previousWindowMovableState != nil else { return }
|
||||
let targetWindow = suppressedWindow ?? window
|
||||
let depthAfter = endWindowDragSuppression(window: targetWindow)
|
||||
restoreWindowDragging(window: targetWindow, previousMovableState: previousWindowMovableState)
|
||||
self.previousWindowMovableState = nil
|
||||
self.suppressedWindow = nil
|
||||
self.didArmWindowDragSuppression = false
|
||||
#if DEBUG
|
||||
let nowMovable = targetWindow.map { String($0.isMovable) } ?? "nil"
|
||||
dlog("folder.dragSuppression restore depth=\(depthAfter) nowMovable=\(nowMovable)")
|
||||
#endif
|
||||
}
|
||||
|
||||
private func maybeDisableWindowDraggingEarly(trigger: String) {
|
||||
guard !didArmWindowDragSuppression else { return }
|
||||
guard let eventType = NSApp.currentEvent?.type,
|
||||
eventType == .leftMouseDown || eventType == .leftMouseDragged else {
|
||||
return
|
||||
}
|
||||
guard let currentWindow = window else { return }
|
||||
|
||||
didArmWindowDragSuppression = true
|
||||
suppressedWindow = currentWindow
|
||||
let suppressionDepth = beginWindowDragSuppression(window: currentWindow) ?? 0
|
||||
if currentWindow.isMovable {
|
||||
previousWindowMovableState = temporarilyDisableWindowDragging(window: currentWindow)
|
||||
} else {
|
||||
previousWindowMovableState = nil
|
||||
}
|
||||
#if DEBUG
|
||||
let wasMovable = previousWindowMovableState.map(String.init) ?? "nil"
|
||||
let nowMovable = String(currentWindow.isMovable)
|
||||
dlog(
|
||||
"folder.dragSuppression trigger=\(trigger) event=\(eventType) depth=\(suppressionDepth) wasMovable=\(wasMovable) nowMovable=\(nowMovable)"
|
||||
)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func temporarilyDisableWindowDragging(window: NSWindow?) -> Bool? {
|
||||
guard let window else { return nil }
|
||||
let wasMovable = window.isMovable
|
||||
if wasMovable {
|
||||
window.isMovable = false
|
||||
}
|
||||
return wasMovable
|
||||
}
|
||||
|
||||
func restoreWindowDragging(window: NSWindow?, previousMovableState: Bool?) {
|
||||
guard let window, let previousMovableState else { return }
|
||||
window.isMovable = previousMovableState
|
||||
}
|
||||
|
||||
/// Wrapper view that tries NSGlassEffectView (macOS 26+) when available or requested
|
||||
|
|
@ -7622,11 +7689,16 @@ private struct SidebarVisualEffectBackground: NSViewRepresentable {
|
|||
|
||||
|
||||
/// Reads the leading inset required to clear traffic lights + left titlebar accessories.
|
||||
final class TitlebarLeadingInsetPassthroughView: NSView {
|
||||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
override func hitTest(_ point: NSPoint) -> NSView? { nil }
|
||||
}
|
||||
|
||||
private struct TitlebarLeadingInsetReader: NSViewRepresentable {
|
||||
@Binding var inset: CGFloat
|
||||
|
||||
func makeNSView(context: Context) -> NSView {
|
||||
let view = NSView()
|
||||
let view = TitlebarLeadingInsetPassthroughView()
|
||||
view.setFrameSize(.zero)
|
||||
return view
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue