292 lines
12 KiB
Swift
292 lines
12 KiB
Swift
import SwiftUI
|
|
|
|
struct WorkspacesSettingsView: View {
|
|
@ObservedObject private var workspaceRegistry = WorkspaceRegistry.shared
|
|
@ObservedObject private var screenRegistry = ScreenRegistry.shared
|
|
|
|
@State private var selectedWorkspaceID: WorkspaceID?
|
|
@State private var renameDraft = ""
|
|
@State private var isDeleteAlertPresented = false
|
|
|
|
private var effectiveSelectedWorkspaceID: WorkspaceID? {
|
|
selectedWorkspaceID ?? workspaceRegistry.workspaceSummaries.first?.id
|
|
}
|
|
|
|
private var selectedSummary: WorkspaceSummary? {
|
|
guard let effectiveSelectedWorkspaceID else { return nil }
|
|
return workspaceRegistry.summary(for: effectiveSelectedWorkspaceID)
|
|
}
|
|
|
|
private var selectedController: WorkspaceController? {
|
|
guard let effectiveSelectedWorkspaceID else { return nil }
|
|
return workspaceRegistry.controller(for: effectiveSelectedWorkspaceID)
|
|
}
|
|
|
|
private var selectedAssignedScreenIDs: [ScreenID] {
|
|
guard let effectiveSelectedWorkspaceID else { return [] }
|
|
return screenRegistry.assignedScreenIDs(to: effectiveSelectedWorkspaceID)
|
|
}
|
|
|
|
private var connectedScreenSummaries: [ConnectedScreenSummary] {
|
|
screenRegistry.connectedScreenSummaries()
|
|
}
|
|
|
|
private var activeConnectedScreenSummary: ConnectedScreenSummary? {
|
|
connectedScreenSummaries.first(where: \.isActive)
|
|
}
|
|
|
|
private var deletionFallbackSummary: WorkspaceSummary? {
|
|
guard let effectiveSelectedWorkspaceID,
|
|
let fallbackID = workspaceRegistry.deletionFallbackWorkspaceID(
|
|
forDeleting: effectiveSelectedWorkspaceID,
|
|
preferredFallback: workspaceRegistry.defaultWorkspaceID
|
|
) else {
|
|
return nil
|
|
}
|
|
|
|
return workspaceRegistry.summary(for: fallbackID)
|
|
}
|
|
|
|
var body: some View {
|
|
HStack(spacing: 20) {
|
|
List(selection: $selectedWorkspaceID) {
|
|
ForEach(workspaceRegistry.workspaceSummaries) { summary in
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(summary.name)
|
|
.font(.headline)
|
|
|
|
Text(usageDescription(for: summary))
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.tag(summary.id)
|
|
.accessibilityIdentifier("settings.workspace.row.\(summary.id.uuidString)")
|
|
}
|
|
}
|
|
.accessibilityIdentifier("settings.workspaces.list")
|
|
.frame(minWidth: 220, idealWidth: 240, maxWidth: 260, maxHeight: .infinity)
|
|
|
|
if let summary = selectedSummary {
|
|
Form {
|
|
Section("Identity") {
|
|
TextField("Workspace name", text: $renameDraft)
|
|
.accessibilityIdentifier("settings.workspaces.name-field")
|
|
.onSubmit {
|
|
renameSelectedWorkspace()
|
|
}
|
|
|
|
OptionalHotkeyRecorderView(
|
|
label: "Jump Hotkey",
|
|
binding: workspaceHotkeyBinding(for: summary.id)
|
|
)
|
|
|
|
HStack {
|
|
Button("Save Name") {
|
|
renameSelectedWorkspace()
|
|
}
|
|
.accessibilityIdentifier("settings.workspaces.save-name")
|
|
|
|
Button("New Workspace") {
|
|
createWorkspace()
|
|
}
|
|
.accessibilityIdentifier("settings.workspaces.new")
|
|
}
|
|
|
|
Text("Workspace jump hotkeys are active when the notch is open and switch the current screen to this workspace.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("Usage") {
|
|
LabeledContent("Assigned screens") {
|
|
Text("\(selectedAssignedScreenIDs.count)")
|
|
.accessibilityIdentifier("settings.workspaces.assigned-count")
|
|
}
|
|
|
|
LabeledContent("Open tabs") {
|
|
Text("\(selectedController?.tabs.count ?? 0)")
|
|
}
|
|
|
|
if selectedAssignedScreenIDs.isEmpty {
|
|
Text("No screens are currently assigned to this workspace.")
|
|
.foregroundStyle(.secondary)
|
|
} else {
|
|
ForEach(selectedAssignedScreenIDs, id: \.self) { screenID in
|
|
LabeledContent("Screen") {
|
|
Text(screenID)
|
|
.font(.caption.monospaced())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Shared Workspace Rules") {
|
|
Text(sharedWorkspaceDescription(for: selectedAssignedScreenIDs.count))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Section("Connected Screens") {
|
|
if let activeScreen = activeConnectedScreenSummary {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(activeScreen.displayName)
|
|
Text(activeScreen.id)
|
|
.font(.caption.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button(activeScreen.assignedWorkspaceID == summary.id ? "Assigned Here" : "Assign Current Screen") {
|
|
screenRegistry.assignWorkspace(summary.id, to: activeScreen.id)
|
|
}
|
|
.accessibilityIdentifier("settings.workspaces.assign-current")
|
|
.disabled(activeScreen.assignedWorkspaceID == summary.id)
|
|
}
|
|
} else {
|
|
Text("No connected screens are currently available.")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
ForEach(connectedScreenSummaries.filter { !$0.isActive }) { screen in
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(screen.displayName)
|
|
Text(screen.id)
|
|
.font(.caption.monospaced())
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Button(screen.assignedWorkspaceID == summary.id ? "Assigned Here" : "Assign Here") {
|
|
screenRegistry.assignWorkspace(summary.id, to: screen.id)
|
|
}
|
|
.accessibilityIdentifier("settings.workspaces.assign.\(screen.id)")
|
|
.disabled(screen.assignedWorkspaceID == summary.id)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("Danger Zone") {
|
|
Button("Delete Workspace", role: .destructive) {
|
|
isDeleteAlertPresented = true
|
|
}
|
|
.accessibilityIdentifier("settings.workspaces.delete")
|
|
.disabled(!workspaceRegistry.canDeleteWorkspace(id: summary.id))
|
|
|
|
if !workspaceRegistry.canDeleteWorkspace(id: summary.id) {
|
|
Text("At least one workspace must remain.")
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
} else {
|
|
ContentUnavailableView(
|
|
"No Workspaces",
|
|
systemImage: "rectangle.3.group",
|
|
description: Text("Create a workspace to start grouping tabs across screens.")
|
|
)
|
|
}
|
|
}
|
|
.onAppear {
|
|
selectInitialWorkspaceIfNeeded()
|
|
}
|
|
.onChange(of: workspaceRegistry.workspaceSummaries) { _, _ in
|
|
synchronizeSelectionWithRegistry()
|
|
}
|
|
.onChange(of: selectedWorkspaceID) { _, _ in
|
|
renameDraft = selectedSummary?.name ?? ""
|
|
}
|
|
.alert("Delete Workspace", isPresented: $isDeleteAlertPresented) {
|
|
Button("Cancel", role: .cancel) {}
|
|
Button("Delete", role: .destructive) {
|
|
deleteSelectedWorkspace()
|
|
}
|
|
} message: {
|
|
if let summary = selectedSummary, let fallback = deletionFallbackSummary {
|
|
Text(
|
|
"Deleting \(summary.name) reassigns its screens to \(fallback.name) and closes the workspace."
|
|
)
|
|
} else {
|
|
Text("At least one workspace must remain.")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func usageDescription(for summary: WorkspaceSummary) -> String {
|
|
let screenCount = screenRegistry.assignedScreenCount(to: summary.id)
|
|
let tabCount = workspaceRegistry.controller(for: summary.id)?.tabs.count ?? 0
|
|
return "\(screenCount) screen\(screenCount == 1 ? "" : "s") · \(tabCount) tab\(tabCount == 1 ? "" : "s")"
|
|
}
|
|
|
|
private func sharedWorkspaceDescription(for screenCount: Int) -> String {
|
|
if screenCount > 1 {
|
|
return "This workspace is shared across \(screenCount) screens. Tab changes stay in sync across each assigned screen."
|
|
}
|
|
|
|
if screenCount == 1 {
|
|
return "This workspace is assigned to one screen. You can assign additional screens to share the same tabs."
|
|
}
|
|
|
|
return "Unassigned workspaces keep their tabs and can be attached to any screen later."
|
|
}
|
|
|
|
private func selectInitialWorkspaceIfNeeded() {
|
|
if selectedWorkspaceID == nil {
|
|
selectedWorkspaceID = workspaceRegistry.workspaceSummaries.first?.id
|
|
}
|
|
renameDraft = selectedSummary?.name ?? ""
|
|
}
|
|
|
|
private func synchronizeSelectionWithRegistry() {
|
|
guard let selectedWorkspaceID else {
|
|
selectInitialWorkspaceIfNeeded()
|
|
return
|
|
}
|
|
|
|
if workspaceRegistry.summary(for: selectedWorkspaceID) == nil {
|
|
self.selectedWorkspaceID = workspaceRegistry.workspaceSummaries.first?.id
|
|
}
|
|
|
|
renameDraft = selectedSummary?.name ?? ""
|
|
}
|
|
|
|
private func renameSelectedWorkspace() {
|
|
guard let effectiveSelectedWorkspaceID else { return }
|
|
workspaceRegistry.renameWorkspace(id: effectiveSelectedWorkspaceID, to: renameDraft)
|
|
renameDraft = selectedSummary?.name ?? renameDraft
|
|
}
|
|
|
|
private func createWorkspace() {
|
|
let workspaceID = workspaceRegistry.createWorkspace()
|
|
selectedWorkspaceID = workspaceID
|
|
renameDraft = workspaceRegistry.summary(for: workspaceID)?.name ?? ""
|
|
}
|
|
|
|
private func workspaceHotkeyBinding(for workspaceID: WorkspaceID) -> Binding<HotkeyBinding?> {
|
|
Binding(
|
|
get: {
|
|
workspaceRegistry.summary(for: workspaceID)?.hotkey
|
|
},
|
|
set: { newValue in
|
|
workspaceRegistry.updateWorkspaceHotkey(id: workspaceID, to: newValue)
|
|
}
|
|
)
|
|
}
|
|
|
|
private func deleteSelectedWorkspace() {
|
|
guard let effectiveSelectedWorkspaceID,
|
|
let fallbackWorkspaceID = screenRegistry.deleteWorkspace(
|
|
effectiveSelectedWorkspaceID,
|
|
preferredFallback: workspaceRegistry.defaultWorkspaceID
|
|
) else {
|
|
return
|
|
}
|
|
|
|
self.selectedWorkspaceID = fallbackWorkspaceID
|
|
renameDraft = workspaceRegistry.summary(for: fallbackWorkspaceID)?.name ?? ""
|
|
}
|
|
}
|