| @@ -42,54 +42,57 @@ struct ContentView: View { |
| 42 | @State private var selectedFile: String? |
42 | @State private var selectedFile: String? |
| 43 | /// A file that isn't text, shown as a preview instead of the open document. |
43 | /// A file that isn't text, shown as a preview instead of the open document. |
| 44 | @State private var previewPath: String? |
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 | @State private var query = "" |
48 | @State private var query = "" |
| 46 | @FocusState private var searchFocused: Bool |
49 | @FocusState private var searchFocused: Bool |
| 47 | @Environment(\.openWindow) private var openWindow |
50 | @Environment(\.openWindow) private var openWindow |
| 48 | @Environment(\.openSettings) private var openSettings |
51 | @Environment(\.openSettings) private var openSettings |
| 49 | |
52 | |
| 50 | private var main: some View { |
53 | /// Whether the outline pane shows: remembered for org files, hidden by default for |
| 51 | NavigationSplitView { |
54 | /// other files, and always shown while search results are up. |
| 52 | Sidebar(workspace: workspace, selection: $selectedFile) { commands.showingFolderPicker = true } |
55 | private var outlineShown: Bool { |
| 53 | .navigationSplitViewColumnWidth(min: 200, ideal: 260) |
56 | !query.isEmpty || (previewPath == nil && (session.isOrg ? orgOutlineShown : plainOutlineShown)) |
| 54 | } content: { |
57 | } |
| 55 | Group { |
58 | |
| 56 | if query.isEmpty { |
59 | private func toggleOutline() { |
| 57 | OutlineColumn(session: session) |
60 | if session.isOrg { orgOutlineShown.toggle() } else { plainOutlineShown.toggle() } |
| 58 | } else { |
61 | } |
| 59 | SearchResults(results: workspace.search(query, overlay: workspace.overlay(path: session.path, document: session.document))) { location in |
62 | |
| 60 | guard location.path == session.path || UnsavedChanges.resolve(session) else { return } |
63 | @ViewBuilder |
| 61 | selectedFile = location.path |
64 | private var detail: some View { |
| 62 | session.show(location) |
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 | } |
75 | } else { |
| 66 | .navigationSplitViewColumnWidth(min: 160, ideal: 220, max: 360) |
76 | ContentUnavailableView( |
| 67 | } detail: { |
77 | workspace.roots.isEmpty ? "Add a folder of org files" : "Choose a file", |
| 68 | if let previewPath { |
78 | systemImage: "doc.text", |
| 69 | FilePreview(path: previewPath) |
79 | description: Text(workspace.roots.isEmpty ? "File ▸ Add Folder… (⇧⌘O)" : "Or press ⌘P to open one by name.") |
| 70 | } else if session.path != nil { |
80 | ) |
| 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 | } |
| |
| 85 | } |
81 | } |
| 86 | .navigationTitle((previewPath ?? session.path).map { ($0 as NSString).lastPathComponent } ?? "Orgstar") |
82 | } |
| 87 | .navigationSubtitle(session.isReadOnly ? "Read-only: not UTF-8" : "") |
83 | |
| 88 | .searchable(text: $query, placement: .toolbar, prompt: "Search notes") |
84 | @ToolbarContentBuilder |
| 89 | .searchFocused($searchFocused) |
85 | private var toolbarItems: some ToolbarContent { |
| 90 | .toolbar { |
86 | ToolbarItem(placement: .navigation) { |
| 91 | if workspace.isIndexing { ProgressView().controlSize(.small) } |
87 | Button(outlineShown ? "Hide Outline" : "Show Outline", systemImage: "list.bullet.indent") { toggleOutline() } |
| 92 | if let running = clock.running { |
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 | Menu { |
96 | Menu { |
| 94 | Button("Clock Out") { perform("app.clock.out") } |
97 | Button("Clock Out") { perform("app.clock.out") } |
| 95 | Button("Cancel Clock") { perform("app.clock.cancel") } |
98 | Button("Cancel Clock") { perform("app.clock.cancel") } |
| @@ -101,11 +104,45 @@ struct ContentView: View { |
| 101 | } |
104 | } |
| 102 | .help("Clocked in since \(running.startedAt.formatted(date: .omitted, time: .shortened))") |
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 | Button("Conflict", systemImage: "exclamationmark.triangle") { commands.showingConflict = true } |
110 | Button("Conflict", systemImage: "exclamationmark.triangle") { commands.showingConflict = true } |
| 106 | .help("The file changed on disk, and the changes conflict with yours") |
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 | .onChange(of: selectedFile) { old, path in |
146 | .onChange(of: selectedFile) { old, path in |
| 110 | guard let path else { return } |
147 | guard let path else { return } |
| 111 | if !DocumentSession.isOrgPath(path), !FileTypes.isText(path) { |
148 | if !DocumentSession.isOrgPath(path), !FileTypes.isText(path) { |
| @@ -344,6 +381,7 @@ struct ContentView: View { |
| 344 | case "app.save": |
381 | case "app.save": |
| 345 | if !session.save(), session.conflict != nil { commands.showingConflict = true } |
382 | if !session.save(), session.conflict != nil { commands.showingConflict = true } |
| 346 | case "app.quit": NSApp.terminate(nil) |
383 | case "app.quit": NSApp.terminate(nil) |
| |
384 | case "app.toggle-outline": toggleOutline() |
| 347 | case "app.quick-open": commands.showingQuickOpen = true |
385 | case "app.quick-open": commands.showingQuickOpen = true |
| 348 | case "app.palette": commands.showingPalette = true |
386 | case "app.palette": commands.showingPalette = true |
| 349 | case "app.search": commands.focusSearch = true |
387 | case "app.search": commands.focusSearch = true |