Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Sources/ContentView+SidebarResizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ extension ContentView {
if isResizerBandActive { isResizerBandActive = false }
isSidebarResizerCursorActive = false
stopSidebarResizerCursorStabilizer()
scheduleSidebarResizerCursorRelease(force: true)
// Release synchronously. A deferred work item held in @State captures this view, and
// once the view has left the hierarchy the state write that clears it is dropped, so a
// pending item would pin the window's TabManager.
sidebarResizerCursorReleaseWorkItem?.cancel()
sidebarResizerCursorReleaseWorkItem = nil
releaseSidebarResizerCursorIfNeeded(force: true)
}

private func sidebarResizerHandleOverlay(
Expand Down
32 changes: 18 additions & 14 deletions Sources/ProgramaApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -966,13 +966,17 @@ struct programaApp: App {
let workspace = manager.selectedWorkspace
let workspaceIndex = workspace.flatMap { selectedWorkspaceIndex(in: manager, workspaceId: $0.id) }
let windowMoveTargets = selectedWorkspaceWindowMoveTargets(in: manager)
let hasWorkspace = workspace != nil

// Action closures resolve the active manager when invoked. Capturing `manager` or
// `workspace` would pin the window's TabManager in the main menu after that window closes.

Button(
workspace?.isPinned == true
? String(localized: "contextMenu.unpinWorkspace", defaultValue: "Unpin Workspace")
: String(localized: "contextMenu.pinWorkspace", defaultValue: "Pin Workspace")
) {
toggleSelectedWorkspacePinned(in: manager)
toggleSelectedWorkspacePinned(in: activeTabManager)
}
.disabled(workspace == nil)

Expand All @@ -988,30 +992,30 @@ struct programaApp: App {

if workspace?.hasCustomTitle == true {
Button(String(localized: "contextMenu.removeCustomWorkspaceName", defaultValue: "Remove Custom Workspace Name")) {
clearSelectedWorkspaceCustomName(in: manager)
clearSelectedWorkspaceCustomName(in: activeTabManager)
}
}

Divider()

Button(String(localized: "contextMenu.moveUp", defaultValue: "Move Up")) {
moveSelectedWorkspace(in: manager, by: -1)
moveSelectedWorkspace(in: activeTabManager, by: -1)
}
.disabled(workspaceIndex == nil || workspaceIndex == 0)

Button(String(localized: "contextMenu.moveDown", defaultValue: "Move Down")) {
moveSelectedWorkspace(in: manager, by: 1)
moveSelectedWorkspace(in: activeTabManager, by: 1)
}
.disabled(workspaceIndex == nil || workspaceIndex == manager.tabs.count - 1)

Button(String(localized: "contextMenu.moveToTop", defaultValue: "Move to Top")) {
moveSelectedWorkspaceToTop(in: manager)
moveSelectedWorkspaceToTop(in: activeTabManager)
}
.disabled(workspace == nil || workspaceIndex == 0)

Menu(String(localized: "contextMenu.moveWorkspaceToWindow", defaultValue: "Move Workspace to Window")) {
Button(String(localized: "contextMenu.newWindow", defaultValue: "New Window")) {
moveSelectedWorkspaceToNewWindow(in: manager)
moveSelectedWorkspaceToNewWindow(in: activeTabManager)
}
.disabled(workspace == nil)

Expand All @@ -1021,44 +1025,44 @@ struct programaApp: App {

ForEach(windowMoveTargets) { target in
Button(target.label) {
moveSelectedWorkspace(in: manager, toWindow: target.windowId)
moveSelectedWorkspace(in: activeTabManager, toWindow: target.windowId)
}
.disabled(target.isCurrentWindow || workspace == nil)
.disabled(target.isCurrentWindow || !hasWorkspace)
}
}
.disabled(workspace == nil)

Divider()

Button(String(localized: "menu.file.closeWorkspace", defaultValue: "Close Workspace")) {
manager.closeCurrentWorkspaceWithConfirmation()
activeTabManager.closeCurrentWorkspaceWithConfirmation()
}
.disabled(workspace == nil)

Button(String(localized: "contextMenu.closeOtherWorkspaces", defaultValue: "Close Other Workspaces")) {
closeOtherSelectedWorkspacePeers(in: manager)
closeOtherSelectedWorkspacePeers(in: activeTabManager)
}
.disabled(workspace == nil || manager.tabs.count <= 1)

Button(String(localized: "contextMenu.closeWorkspacesBelow", defaultValue: "Close Workspaces Below")) {
closeSelectedWorkspacesBelow(in: manager)
closeSelectedWorkspacesBelow(in: activeTabManager)
}
.disabled(workspaceIndex == nil || workspaceIndex == manager.tabs.count - 1)

Button(String(localized: "contextMenu.closeWorkspacesAbove", defaultValue: "Close Workspaces Above")) {
closeSelectedWorkspacesAbove(in: manager)
closeSelectedWorkspacesAbove(in: activeTabManager)
}
.disabled(workspaceIndex == nil || workspaceIndex == 0)

Divider()

Button(String(localized: "contextMenu.markWorkspaceRead", defaultValue: "Mark Workspace as Read")) {
markSelectedWorkspaceRead(in: manager)
markSelectedWorkspaceRead(in: activeTabManager)
}
.disabled(!selectedWorkspaceHasUnreadNotifications(in: manager))

Button(String(localized: "contextMenu.markWorkspaceUnread", defaultValue: "Mark Workspace as Unread")) {
markSelectedWorkspaceUnread(in: manager)
markSelectedWorkspaceUnread(in: activeTabManager)
}
.disabled(!selectedWorkspaceHasReadNotifications(in: manager))
}
Expand Down
14 changes: 8 additions & 6 deletions Sources/ProgramaConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,17 @@ final class ProgramaConfigStore: ObservableObject {
func wireDirectoryTracking(tabManager: TabManager) {
cancellables.removeAll()

// The pipeline keeps only the workspace id and its directory publisher. Operators such as
// removeDuplicates retain their last value, and holding the Workspace itself would pin a
// closed window's workspace until the next wiring.
tabManager.$selectedTabId
.compactMap { [weak tabManager] tabId -> Workspace? in
guard let tabId, let tabManager else { return nil }
return tabManager.tabs.first(where: { $0.id == tabId })
.compactMap { [weak tabManager] tabId -> (id: UUID, directory: AnyPublisher<String, Never>)? in
guard let tabId,
let workspace = tabManager?.tabs.first(where: { $0.id == tabId }) else { return nil }
return (workspace.id, workspace.$currentDirectory.eraseToAnyPublisher())
}
.removeDuplicates(by: { $0.id == $1.id })
.map { workspace -> AnyPublisher<String, Never> in
workspace.$currentDirectory.eraseToAnyPublisher()
}
.map(\.directory)
.switchToLatest()
.removeDuplicates()
.receive(on: DispatchQueue.main)
Expand Down
Loading