162 lines
6.1 KiB
Swift
162 lines
6.1 KiB
Swift
import SwiftUI
|
|
|
|
struct WorkspaceSwitcherView: View {
|
|
@ObservedObject var screen: ScreenContext
|
|
let orchestrator: NotchOrchestrator
|
|
@ObservedObject private var screenRegistry = ScreenRegistry.shared
|
|
@ObservedObject private var workspaceRegistry = WorkspaceRegistry.shared
|
|
|
|
@State private var isRenameAlertPresented = false
|
|
@State private var isDeleteConfirmationPresented = false
|
|
@State private var renameDraft = ""
|
|
|
|
private var currentWorkspaceSummary: WorkspaceSummary {
|
|
workspaceRegistry.summary(for: screen.workspaceID)
|
|
?? workspaceRegistry.allWorkspaceSummaries().first
|
|
?? WorkspaceSummary(id: screen.workspaceID, name: "Workspace")
|
|
}
|
|
|
|
private var deletionFallbackSummary: WorkspaceSummary? {
|
|
guard let fallbackWorkspaceID = workspaceRegistry.deletionFallbackWorkspaceID(
|
|
forDeleting: screen.workspaceID,
|
|
preferredFallback: workspaceRegistry.defaultWorkspaceID
|
|
) else {
|
|
return nil
|
|
}
|
|
|
|
return workspaceRegistry.summary(for: fallbackWorkspaceID)
|
|
}
|
|
|
|
private var assignedScreenCount: Int {
|
|
screenRegistry.assignedScreenCount(to: screen.workspaceID)
|
|
}
|
|
|
|
var body: some View {
|
|
Menu {
|
|
ForEach(workspaceRegistry.workspaceSummaries) { summary in
|
|
Button {
|
|
selectWorkspace(summary.id)
|
|
} label: {
|
|
if summary.id == screen.workspaceID {
|
|
Label(summary.name, systemImage: "checkmark")
|
|
} else {
|
|
Text(summary.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
Divider()
|
|
|
|
Button("New Workspace") {
|
|
let workspaceID = workspaceRegistry.createWorkspace()
|
|
selectWorkspace(workspaceID)
|
|
}
|
|
|
|
Button("Rename Current Workspace") {
|
|
renameDraft = currentWorkspaceSummary.name
|
|
syncFocusLossSuppression(renamePresented: true, deletePresented: isDeleteConfirmationPresented)
|
|
isRenameAlertPresented = true
|
|
}
|
|
|
|
Button("Delete Current Workspace", role: .destructive) {
|
|
syncFocusLossSuppression(renamePresented: isRenameAlertPresented, deletePresented: true)
|
|
isDeleteConfirmationPresented = true
|
|
}
|
|
.disabled(!workspaceRegistry.canDeleteWorkspace(id: screen.workspaceID))
|
|
} label: {
|
|
switcherLabel
|
|
}
|
|
.menuStyle(.borderlessButton)
|
|
.accessibilityIdentifier("notch.workspace-switcher")
|
|
.accessibilityLabel("Workspace Switcher")
|
|
.accessibilityValue(currentWorkspaceSummary.name)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
.help("Switch workspace for this screen")
|
|
.alert("Rename Workspace", isPresented: $isRenameAlertPresented) {
|
|
TextField("Workspace name", text: $renameDraft)
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Save") {
|
|
workspaceRegistry.renameWorkspace(id: screen.workspaceID, to: renameDraft)
|
|
}
|
|
} message: {
|
|
Text("This only renames the shared workspace. Screens assigned to it keep following the new name.")
|
|
}
|
|
.confirmationDialog("Delete Workspace", isPresented: $isDeleteConfirmationPresented, titleVisibility: .visible) {
|
|
Button("Delete Workspace", role: .destructive) {
|
|
deleteCurrentWorkspace()
|
|
}
|
|
} message: {
|
|
Text(deleteMessage)
|
|
}
|
|
.onChange(of: isRenameAlertPresented) { _, isPresented in
|
|
syncFocusLossSuppression(renamePresented: isPresented, deletePresented: isDeleteConfirmationPresented)
|
|
}
|
|
.onChange(of: isDeleteConfirmationPresented) { _, isPresented in
|
|
syncFocusLossSuppression(renamePresented: isRenameAlertPresented, deletePresented: isPresented)
|
|
}
|
|
.onDisappear {
|
|
screen.setCloseOnFocusLossSuppressed(false)
|
|
}
|
|
}
|
|
|
|
private var deleteMessage: String {
|
|
if let fallback = deletionFallbackSummary {
|
|
return "This reassigns \(assignedScreenCount) screen\(assignedScreenCount == 1 ? "" : "s") to \(fallback.name) and closes this workspace."
|
|
}
|
|
|
|
return "At least one workspace must remain."
|
|
}
|
|
|
|
private var switcherLabel: some View {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "rectangle.3.group")
|
|
.font(.system(size: 11, weight: .medium))
|
|
Text(currentWorkspaceSummary.name)
|
|
.font(.system(size: 11, weight: .medium))
|
|
.lineLimit(1)
|
|
Image(systemName: "chevron.down")
|
|
.font(.system(size: 9, weight: .semibold))
|
|
}
|
|
.foregroundStyle(.white.opacity(0.7))
|
|
.padding(.horizontal, 8)
|
|
.padding(.vertical, 4)
|
|
.background(
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(Color.white.opacity(0.08))
|
|
)
|
|
.contentShape(Rectangle())
|
|
.accessibilityElement(children: .ignore)
|
|
.accessibilityLabel("Workspace Switcher")
|
|
.accessibilityValue(currentWorkspaceSummary.name)
|
|
.accessibilityIdentifier("notch.workspace-switcher")
|
|
}
|
|
|
|
private func selectWorkspace(_ workspaceID: WorkspaceID) {
|
|
screenRegistry.assignWorkspace(workspaceID, to: screen.id)
|
|
|
|
if screen.notchState == .open {
|
|
orchestrator.open(screenID: screen.id)
|
|
}
|
|
}
|
|
|
|
private func deleteCurrentWorkspace() {
|
|
guard let fallback = screenRegistry.deleteWorkspace(
|
|
screen.workspaceID,
|
|
preferredFallback: workspaceRegistry.defaultWorkspaceID
|
|
) else {
|
|
return
|
|
}
|
|
|
|
screenRegistry.assignWorkspace(fallback, to: screen.id)
|
|
if screen.notchState == .open {
|
|
orchestrator.open(screenID: screen.id)
|
|
} else {
|
|
screen.requestTerminalFocus?()
|
|
}
|
|
}
|
|
|
|
private func syncFocusLossSuppression(renamePresented: Bool, deletePresented: Bool) {
|
|
screen.setCloseOnFocusLossSuppressed(renamePresented || deletePresented)
|
|
}
|
|
}
|