| @@ -42,54 +42,57 @@ struct ContentView: View { |
| 42 | 42 | @State private var selectedFile: String? |
| 43 | 43 | /// A file that isn't text, shown as a preview instead of the open document. |
| 44 | 44 | @State private var previewPath: String? |
| 45 | @AppStorage("outlineShown") private var orgOutlineShown = true |
| 46 | /// The outline for a file that isn't org, off until asked for. |
| 47 | @State private var plainOutlineShown = false |
| 45 | 48 | @State private var query = "" |
| 46 | 49 | @FocusState private var searchFocused: Bool |
| 47 | 50 | @Environment(\.openWindow) private var openWindow |
| 48 | 51 | @Environment(\.openSettings) private var openSettings |
| 49 | 52 | |
| 50 | | private var main: some View { |
| 51 | | NavigationSplitView { |
| 52 | | Sidebar(workspace: workspace, selection: $selectedFile) { commands.showingFolderPicker = true } |
| 53 | | .navigationSplitViewColumnWidth(min: 200, ideal: 260) |
| 54 | | } content: { |
| 55 | | Group { |
| 56 | | if query.isEmpty { |
| 57 | | OutlineColumn(session: session) |
| 58 | | } else { |
| 59 | | SearchResults(results: workspace.search(query, overlay: workspace.overlay(path: session.path, document: session.document))) { location in |
| 60 | | guard location.path == session.path || UnsavedChanges.resolve(session) else { return } |
| 61 | | selectedFile = location.path |
| 62 | | session.show(location) |
| 53 | /// Whether the outline pane shows: remembered for org files, hidden by default for |
| 54 | /// other files, and always shown while search results are up. |
| 55 | private var outlineShown: Bool { |
| 56 | !query.isEmpty || (previewPath == nil && (session.isOrg ? orgOutlineShown : plainOutlineShown)) |
| 57 | } |
| 58 | |
| 59 | private func toggleOutline() { |
| 60 | if session.isOrg { orgOutlineShown.toggle() } else { plainOutlineShown.toggle() } |
| 61 | } |
| 62 | |
| 63 | @ViewBuilder |
| 64 | private var detail: some View { |
| 65 | if let previewPath { |
| 66 | FilePreview(path: previewPath) |
| 67 | } else if session.path != nil { |
| 68 | EditorView(session: session, commands: commands, keymap: keymap, modal: preset.isModal, optionAsMeta: OptionAsMeta(rawValue: optionAsMeta), options: options, onAppCommand: perform, onExternal: runExternal) |
| 69 | .safeAreaInset(edge: .bottom, spacing: 0) { |
| 70 | VStack(spacing: 0) { |
| 71 | ModeLine(session: session, commands: commands, clock: clock, perform: perform) |
| 72 | EchoArea(commands: commands) |
| 63 | 73 | } |
| 64 | 74 | } |
| 65 | | } |
| 66 | | .navigationSplitViewColumnWidth(min: 160, ideal: 220, max: 360) |
| 67 | | } detail: { |
| 68 | | if let previewPath { |
| 69 | | FilePreview(path: previewPath) |
| 70 | | } else if session.path != nil { |
| 71 | | EditorView(session: session, commands: commands, keymap: keymap, modal: preset.isModal, optionAsMeta: OptionAsMeta(rawValue: optionAsMeta), options: options, onAppCommand: perform, onExternal: runExternal) |
| 72 | | .safeAreaInset(edge: .bottom, spacing: 0) { |
| 73 | | VStack(spacing: 0) { |
| 74 | | ModeLine(session: session, commands: commands, clock: clock, perform: perform) |
| 75 | | EchoArea(commands: commands) |
| 76 | | } |
| 77 | | } |
| 78 | | } else { |
| 79 | | ContentUnavailableView( |
| 80 | | workspace.roots.isEmpty ? "Add a folder of org files" : "Choose a file", |
| 81 | | systemImage: "doc.text", |
| 82 | | description: Text(workspace.roots.isEmpty ? "File ▸ Add Folder… (⇧⌘O)" : "Or press ⌘P to open one by name.") |
| 83 | | ) |
| 84 | | } |
| 75 | } else { |
| 76 | ContentUnavailableView( |
| 77 | workspace.roots.isEmpty ? "Add a folder of org files" : "Choose a file", |
| 78 | systemImage: "doc.text", |
| 79 | description: Text(workspace.roots.isEmpty ? "File ▸ Add Folder… (⇧⌘O)" : "Or press ⌘P to open one by name.") |
| 80 | ) |
| 85 | 81 | } |
| 86 | | .navigationTitle((previewPath ?? session.path).map { ($0 as NSString).lastPathComponent } ?? "Orgstar") |
| 87 | | .navigationSubtitle(session.isReadOnly ? "Read-only: not UTF-8" : "") |
| 88 | | .searchable(text: $query, placement: .toolbar, prompt: "Search notes") |
| 89 | | .searchFocused($searchFocused) |
| 90 | | .toolbar { |
| 91 | | if workspace.isIndexing { ProgressView().controlSize(.small) } |
| 92 | | if let running = clock.running { |
| 82 | } |
| 83 | |
| 84 | @ToolbarContentBuilder |
| 85 | private var toolbarItems: some ToolbarContent { |
| 86 | ToolbarItem(placement: .navigation) { |
| 87 | Button(outlineShown ? "Hide Outline" : "Show Outline", systemImage: "list.bullet.indent") { toggleOutline() } |
| 88 | .help(outlineShown ? "Hide the outline (⌥⌘O)" : "Show the outline (⌥⌘O)") |
| 89 | .disabled(!query.isEmpty || previewPath != nil || session.path == nil) |
| 90 | } |
| 91 | if workspace.isIndexing { |
| 92 | ToolbarItem { ProgressView().controlSize(.small) } |
| 93 | } |
| 94 | if let running = clock.running { |
| 95 | ToolbarItem { |
| 93 | 96 | Menu { |
| 94 | 97 | Button("Clock Out") { perform("app.clock.out") } |
| 95 | 98 | Button("Cancel Clock") { perform("app.clock.cancel") } |
| @@ -101,11 +104,45 @@ struct ContentView: View { |
| 101 | 104 | } |
| 102 | 105 | .help("Clocked in since \(running.startedAt.formatted(date: .omitted, time: .shortened))") |
| 103 | 106 | } |
| 104 | | if session.conflict != nil { |
| 107 | } |
| 108 | if session.conflict != nil { |
| 109 | ToolbarItem { |
| 105 | 110 | Button("Conflict", systemImage: "exclamationmark.triangle") { commands.showingConflict = true } |
| 106 | 111 | .help("The file changed on disk, and the changes conflict with yours") |
| 107 | 112 | } |
| 108 | 113 | } |
| 114 | } |
| 115 | |
| 116 | private var main: some View { |
| 117 | NavigationSplitView { |
| 118 | Sidebar(workspace: workspace, selection: $selectedFile) { commands.showingFolderPicker = true } |
| 119 | .navigationSplitViewColumnWidth(min: 200, ideal: 260) |
| 120 | } detail: { |
| 121 | HSplitView { |
| 122 | if outlineShown { |
| 123 | Group { |
| 124 | if query.isEmpty { |
| 125 | OutlineColumn(session: session) |
| 126 | } else { |
| 127 | SearchResults(results: workspace.search(query, overlay: workspace.overlay(path: session.path, document: session.document))) { location in |
| 128 | guard location.path == session.path || UnsavedChanges.resolve(session) else { return } |
| 129 | selectedFile = location.path |
| 130 | session.show(location) |
| 131 | } |
| 132 | } |
| 133 | } |
| 134 | .frame(minWidth: 160, idealWidth: 220, maxWidth: 360) |
| 135 | } |
| 136 | detail |
| 137 | .frame(minWidth: 300, maxWidth: .infinity) |
| 138 | } |
| 139 | } |
| 140 | .navigationTitle((previewPath ?? session.path).map { ($0 as NSString).lastPathComponent } ?? "Orgstar") |
| 141 | .navigationSubtitle(session.isReadOnly ? "Read-only: not UTF-8" : "") |
| 142 | .searchable(text: $query, placement: .toolbar, prompt: "Search notes") |
| 143 | .searchFocused($searchFocused) |
| 144 | .toolbar { toolbarItems } |
| 145 | .onChange(of: session.path) { plainOutlineShown = false } |
| 109 | 146 | .onChange(of: selectedFile) { old, path in |
| 110 | 147 | guard let path else { return } |
| 111 | 148 | if !DocumentSession.isOrgPath(path), !FileTypes.isText(path) { |
| @@ -344,6 +381,7 @@ struct ContentView: View { |
| 344 | 381 | case "app.save": |
| 345 | 382 | if !session.save(), session.conflict != nil { commands.showingConflict = true } |
| 346 | 383 | case "app.quit": NSApp.terminate(nil) |
| 384 | case "app.toggle-outline": toggleOutline() |
| 347 | 385 | case "app.quick-open": commands.showingQuickOpen = true |
| 348 | 386 | case "app.palette": commands.showingPalette = true |
| 349 | 387 | case "app.search": commands.focusSearch = true |