diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-06 12:45:37 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-06 12:45:37 +0200 |
| commit | 54f89bcd1a7fb0b92b65d8c0ea3847d635e11133 (patch) | |
| tree | da6c1e3f015c4b21a2e32854cadb09e71f9079e7 /Map/Presentation | |
| parent | 3cd8f1f7200109d505b5e105d5ebe53f0c93d337 (diff) | |
Allow adding and removing templates
Diffstat (limited to 'Map/Presentation')
| -rw-r--r-- | Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift | 1 | ||||
| -rw-r--r-- | Map/Presentation/Base Components/MapTextEditor.swift | 38 | ||||
| -rw-r--r-- | Map/Presentation/MapEditor.swift | 15 | ||||
| -rw-r--r-- | Map/Presentation/Preferences/StagesPreferencesView.swift | 6 | ||||
| -rw-r--r-- | Map/Presentation/Preferences/TemplatesPreferencesView.swift | 255 | ||||
| -rw-r--r-- | Map/Presentation/PreferencesWindow.swift | 10 | ||||
| -rw-r--r-- | Map/Presentation/Theme/Dimensions.swift | 17 |
7 files changed, 206 insertions, 136 deletions
diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift index 9478346..94a87d7 100644 --- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift +++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift @@ -3,7 +3,6 @@ import SwiftUI struct EvolutionPickerMenu: View { @Binding var selectedEvolution: StageType - private func select(_ stageType: StageType) { selectedEvolution = stageType diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift index 18b2fbc..4790a70 100644 --- a/Map/Presentation/Base Components/MapTextEditor.swift +++ b/Map/Presentation/Base Components/MapTextEditor.swift @@ -17,7 +17,8 @@ import SwiftUI class MapTextEditorController: NSViewController { - @Binding var document: MapDocument + @Binding var text: String + var highlightRanges: [Range<String.Index>] { didSet { updateHighlights() @@ -44,10 +45,10 @@ class MapTextEditorController: NSViewController { private let changeDebouncer: Debouncer = Debouncer(seconds: 1) init( - document: Binding<MapDocument>, highlightRanges: [Range<String.Index>], selectedRange: Int, + text: Binding<String>, highlightRanges: [Range<String.Index>], selectedRange: Int, onChange: @escaping () -> Void ) { - self._document = document + self._text = text self.onChange = onChange self.highlightRanges = highlightRanges self.selectedRange = selectedRange @@ -68,7 +69,7 @@ class MapTextEditorController: NSViewController { textView.allowsUndo = true textView.delegate = self textView.textStorage?.delegate = self - textView.string = self.document.text + textView.string = self.text textView.isEditable = true textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) self.view = scrollView @@ -79,12 +80,12 @@ class MapTextEditorController: NSViewController { updateHighlights() } - private var textView: NSTextView? { + var currentTextView: NSTextView? { return (view as? NSScrollView)?.documentView as? NSTextView } private func updateHighlights() { - if let textView { + if let textView = currentTextView { if let textStorage = textView.textStorage { textStorage.removeAttribute( .backgroundColor, range: NSRange(location: 0, length: textStorage.length)) @@ -105,7 +106,7 @@ class MapTextEditorController: NSViewController { } private func focusOnResult() { - if let textView { + if let textView = currentTextView { if let textStorage = textView.textStorage { if selectedRange < highlightRanges.count { let range = highlightRanges[selectedRange] @@ -121,7 +122,7 @@ extension MapTextEditorController: NSTextViewDelegate { func textDidChange(_ obj: Notification) { if let textField = obj.object as? NSTextView { - self.document.text = textField.string + self.text = textField.string changeDebouncer.debounce { DispatchQueue.main.async { @@ -235,16 +236,26 @@ extension MapTextEditorController: NSTextStorageDelegate { struct MapTextEditor: NSViewControllerRepresentable { - @Binding var document: MapDocument + @Binding var text: String var highlightRanges: [Range<String.Index>] var selectedRange: Int var onChange: () -> Void = {} + init( + text: Binding<String>, highlightRanges: [Range<String.Index>] = [], selectedRange: Int = 0, + onChange: @escaping () -> Void = {} + ) { + self._text = text + self.highlightRanges = highlightRanges + self.selectedRange = selectedRange + self.onChange = onChange + } + func makeNSViewController( context: NSViewControllerRepresentableContext<MapTextEditor> ) -> MapTextEditorController { return MapTextEditorController( - document: $document, highlightRanges: highlightRanges, selectedRange: selectedRange, + text: $text, highlightRanges: highlightRanges, selectedRange: selectedRange, onChange: onChange) } @@ -256,5 +267,12 @@ struct MapTextEditor: NSViewControllerRepresentable { if nsViewController.selectedRange != selectedRange { nsViewController.selectedRange = selectedRange } + + // Update text view content if text has changed + if let textView = nsViewController.currentTextView { + if textView.string != text { + textView.string = text + } + } } } diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift index 5dc1adb..5005a73 100644 --- a/Map/Presentation/MapEditor.swift +++ b/Map/Presentation/MapEditor.swift @@ -91,10 +91,17 @@ struct MapEditor: View { } adaptiveStack { ZStack(alignment: .topLeading) { - MapTextEditor(document: $document, highlightRanges: results, selectedRange: selectedTerm) - .background(Color.Theme.UI.background) - .foregroundColor(Color.Theme.UI.foreground) - .frame(minHeight: 96.0) + MapTextEditor( + text: Binding( + get: { document.text }, + set: { document.text = $0 } + ), + highlightRanges: results, + selectedRange: selectedTerm + ) + .background(Color.Theme.UI.background) + .foregroundColor(Color.Theme.UI.foreground) + .frame(minHeight: 96.0) } .background(Color.Theme.UI.background) .cornerRadius(5.0) diff --git a/Map/Presentation/Preferences/StagesPreferencesView.swift b/Map/Presentation/Preferences/StagesPreferencesView.swift index 1e57cbf..b1ce31c 100644 --- a/Map/Presentation/Preferences/StagesPreferencesView.swift +++ b/Map/Presentation/Preferences/StagesPreferencesView.swift @@ -129,7 +129,7 @@ struct StagesPreferencesView: View { } else { // First use - create default Cynefin stage customStages = [CustomStage.cynefinDefault()] - saveStages() // Save the default to UserDefaults + saveStages() // Save the default to UserDefaults } } @@ -156,7 +156,9 @@ struct StagesPreferencesView: View { } } - private func updateStageValue(_ id: UUID, _ keyPath: WritableKeyPath<Stage, String>, _ value: String) { + private func updateStageValue( + _ id: UUID, _ keyPath: WritableKeyPath<Stage, String>, _ value: String + ) { if let index = customStages.firstIndex(where: { $0.id == id }) { customStages[index].stage[keyPath: keyPath] = value saveStages() diff --git a/Map/Presentation/Preferences/TemplatesPreferencesView.swift b/Map/Presentation/Preferences/TemplatesPreferencesView.swift index 42db1a3..2994450 100644 --- a/Map/Presentation/Preferences/TemplatesPreferencesView.swift +++ b/Map/Presentation/Preferences/TemplatesPreferencesView.swift @@ -19,10 +19,12 @@ struct Template: Identifiable, Codable, Hashable { var id = UUID() var name: String var content: String + var isDefault: Bool = false - init(name: String, content: String) { + init(name: String, content: String, isDefault: Bool = false) { self.name = name self.content = content + self.isDefault = isDefault } func hash(into hasher: inout Hasher) { @@ -34,56 +36,47 @@ struct Template: Identifiable, Codable, Hashable { } } -struct SimpleTextEditor: NSViewRepresentable { - @Binding var text: String - - func makeNSView(context: Context) -> NSScrollView { - let scrollView = NSTextView.scrollableTextView() - let textView = scrollView.documentView as! NSTextView - - textView.backgroundColor = .textBackgroundColor - textView.allowsUndo = true - textView.delegate = context.coordinator - textView.string = text - textView.isEditable = true - textView.font = .monospacedSystemFont(ofSize: 14.0, weight: .regular) - - return scrollView - } - - func updateNSView(_ nsView: NSScrollView, context: Context) { - let textView = nsView.documentView as! NSTextView - if textView.string != text { - textView.string = text - } - } +struct TemplatesPreferencesView: View { + @AppStorage("mapTemplates") private var templatesData: Data = Data() + @State private var selectedTemplate: Template? + @State private var showingAddSheet = false + @State private var newTemplateName = "" - func makeCoordinator() -> Coordinator { - Coordinator(self) + private var templates: Binding<[Template]> { + Binding( + get: { + if let decoded = try? JSONDecoder().decode([Template].self, from: templatesData) { + return decoded + } + return [] + }, + set: { newValue in + if let encoded = try? JSONEncoder().encode(newValue) { + templatesData = encoded + } + } + ) } - class Coordinator: NSObject, NSTextViewDelegate { - var parent: SimpleTextEditor - - init(_ parent: SimpleTextEditor) { - self.parent = parent - } + private var selectedTemplateContent: Binding<String> { + Binding( + get: { + guard let selected = selectedTemplate, + let template = templates.wrappedValue.first(where: { $0.id == selected.id }) + else { return "" } + return template.content + }, + set: { newContent in + guard let selected = selectedTemplate, + let index = templates.wrappedValue.firstIndex(where: { $0.id == selected.id }) + else { return } - func textDidChange(_ notification: Notification) { - if let textView = notification.object as? NSTextView { - parent.text = textView.string + var currentTemplates = templates.wrappedValue + currentTemplates[index].content = newContent + templates.wrappedValue = currentTemplates } - } + ) } -} - -struct TemplatesPreferencesView: View { - @AppStorage("mapTemplates") private var templatesData: Data = Data() - @State private var templates: [Template] = [] - @State private var selectedTemplate: Template? - @State private var templateText: String = "" - @State private var showingAddSheet = false - @State private var newTemplateName = "" var body: some View { VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { @@ -97,35 +90,65 @@ struct TemplatesPreferencesView: View { .padding(.top, Dimensions.Spacing.loose) .padding(.bottom, Dimensions.Spacing.regular) HStack(spacing: 0) { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { HStack { - Text("Templates") - .font(.headline) - + Text("preferences.templates.table.title") + .font(.Theme.Body.emphasized) + Spacer() - + Button(action: { showingAddSheet = true }) { Image(systemName: "plus") } .buttonStyle(.borderless) - - Button(action: removeTemplate) { + + Button(action: { + removeTemplate(selectedTemplate) + }) { Image(systemName: "minus") } .buttonStyle(.borderless) .disabled(selectedTemplate == nil) } - - List(templates, selection: $selectedTemplate) { template in - Text(template.name) - .tag(template) + + List(templates.wrappedValue) { template in + HStack { + Button(action: { setDefaultTemplate(template) }) { + Image(systemName: template.isDefault ? "largecircle.fill.circle" : "circle") + .foregroundColor( + selectedTemplate?.id == template.id + ? Color.white + : (template.isDefault + ? Color.Theme.UI.accent : Color.Theme.UI.foreground.opacity(0.5)) + ) + } + .buttonStyle(.plain) + .help("Set as default template") + + Text(template.name) + .tag(template) + .foregroundColor( + selectedTemplate?.id == template.id ? Color.white : Color.Theme.UI.foreground + ) + + Spacer() + } + .padding(Dimensions.Spacing.cozy) + .background(selectedTemplate?.id == template.id ? Color.Theme.UI.accent : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: Dimensions.Preferences.Sidebar.radius)) + .padding(Dimensions.Spacing.coziest) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + .onTapGesture { + selectedTemplate = template + } } - .listStyle(.sidebar) + .listStyle(.plain) } - .frame(width: 200) - + .frame(width: Dimensions.Preferences.Sidebar.width) + Divider() - + VStack(alignment: .leading, spacing: 8) { if let selected = selectedTemplate { HStack { @@ -133,11 +156,8 @@ struct TemplatesPreferencesView: View { .font(.headline) Spacer() } - - SimpleTextEditor(text: $templateText) - .onChange(of: templateText) { _, newValue in - updateSelectedTemplate(newValue) - } + + MapTextEditor(text: selectedTemplateContent) } else { VStack { Spacer() @@ -151,89 +171,102 @@ struct TemplatesPreferencesView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.leading) } - .padding() .onAppear { - loadTemplates() - } - .onChange(of: selectedTemplate) { _, newSelection in - if let template = newSelection { - templateText = template.content + // Initialize default templates if needed + if templates.wrappedValue.isEmpty + || !templates.wrappedValue.contains(where: { $0.isDefault }) + { + createDefaultTemplates() + } + // Set first template as selected if none is selected + if selectedTemplate == nil && !templates.wrappedValue.isEmpty { + selectedTemplate = templates.wrappedValue.first } } .sheet(isPresented: $showingAddSheet) { - VStack(spacing: 16) { + VStack(spacing: Dimensions.Spacing.indulgent) { Text("Add New Template") - .font(.headline) - + .font(.Theme.Body.emphasized) + TextField("Template Name", text: $newTemplateName) .textFieldStyle(.roundedBorder) - + .font(.Theme.Body.regular) + HStack { Button("Cancel") { showingAddSheet = false newTemplateName = "" } - + Spacer() - + Button("Add") { addTemplate() } .disabled(newTemplateName.isEmpty) } } + .padding(Dimensions.Spacing.loose) .frame(width: 300, height: 120) } }.padding(.vertical, Dimensions.Spacing.loose) .padding(.horizontal, Dimensions.Spacing.indulgent) } - private func loadTemplates() { - if let decoded = try? JSONDecoder().decode([Template].self, from: templatesData) { - templates = decoded - } else { - templates = [ - Template( - name: "Basic Map", - content: - "Component A (90, 20)\nComponent B (70, 40)\nComponent A -> Component B\n\n[Note] (50, 10) This is a sample template" - ) - ] - saveTemplates() - } - } + private func createDefaultTemplates() { + let defaultTemplate = Template( + name: String(localized: "map_template.default.title"), + content: String(localized: "map_template.default.content"), + isDefault: true + ) + + let emptyTemplate = Template( + name: String(localized: "map_template.empty.title"), + content: "", + isDefault: false + ) - private func saveTemplates() { - if let encoded = try? JSONEncoder().encode(templates) { - templatesData = encoded + if templates.wrappedValue.isEmpty { + templates.wrappedValue = [defaultTemplate, emptyTemplate] + } else { + // Add default template if none exists + if !templates.wrappedValue.contains(where: { $0.isDefault }) { + var currentTemplates = templates.wrappedValue + currentTemplates.insert(defaultTemplate, at: 0) + templates.wrappedValue = currentTemplates + } } } private func addTemplate() { - let newTemplate = Template(name: newTemplateName, content: "") - templates.append(newTemplate) + let newTemplate = Template( + name: newTemplateName.isEmpty + ? String(localized: "map_template.empty.title") : newTemplateName, + content: "" + ) + var currentTemplates = templates.wrappedValue + currentTemplates.append(newTemplate) + templates.wrappedValue = currentTemplates selectedTemplate = newTemplate - templateText = "" - saveTemplates() showingAddSheet = false newTemplateName = "" } - private func removeTemplate() { - guard let selected = selectedTemplate else { return } - templates.removeAll { $0.id == selected.id } - selectedTemplate = nil - templateText = "" - saveTemplates() + private func removeTemplate(_ template: Template?) { + guard let template else { return } + var currentTemplates = templates.wrappedValue + currentTemplates.removeAll { $0 == template } + templates.wrappedValue = currentTemplates + selectedTemplate = templates.wrappedValue.first } - private func updateSelectedTemplate(_ newContent: String) { - guard let selected = selectedTemplate, - let index = templates.firstIndex(where: { $0.id == selected.id }) - else { return } - - templates[index].content = newContent - saveTemplates() + private func setDefaultTemplate(_ template: Template) { + // Direct assignment to binding - this will automatically save via @AppStorage + templates.wrappedValue = templates.wrappedValue.map { t in + var newTemplate = t + newTemplate.isDefault = (t.id == template.id) + return newTemplate + } } } diff --git a/Map/Presentation/PreferencesWindow.swift b/Map/Presentation/PreferencesWindow.swift index da2b75f..2471711 100644 --- a/Map/Presentation/PreferencesWindow.swift +++ b/Map/Presentation/PreferencesWindow.swift @@ -68,8 +68,14 @@ struct PreferencesWindow: View { Text(tab.rawValue) .font(.Theme.SmallControl.regular) } - .frame(width: Dimensions.Preferences.Toolbar.width, height: Dimensions.Preferences.Toolbar.height) - .background(selectedTab == tab ? Color.Theme.UI.accent : Color.clear) + .frame( + width: Dimensions.Preferences.Toolbar.width, + height: Dimensions.Preferences.Toolbar.height + ) + .padding(.horizontal, Dimensions.Spacing.coziest) + .background( + selectedTab == tab ? Color.Theme.UI.accent : Color.background.opacity(0.01) + ) .foregroundColor(selectedTab == tab ? Color.white : Color.Theme.UI.foreground) .cornerRadius(Dimensions.Preferences.Toolbar.radius) } diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift index 5597c4e..94ab7c0 100644 --- a/Map/Presentation/Theme/Dimensions.swift +++ b/Map/Presentation/Theme/Dimensions.swift @@ -22,7 +22,7 @@ struct Dimensions { static let body: CGFloat = 13 static let title: CGFloat = 32 static let caption: CGFloat = 11 - static let smallControl: CGFloat = 9 + static let smallControl: CGFloat = 11 struct Map { static let note: CGFloat = 12 @@ -43,7 +43,7 @@ struct Dimensions { static let vertexLabel: CGFloat = 18 } } - + struct Kerning { static let body: CGFloat = 0.5 } @@ -66,25 +66,30 @@ struct Dimensions { } // MARK: - Assorted Controls - + struct EvolutionPicker { static let radius = 8.0 static let controlSize = 8.0 static let controlSpacing = 3.0 } - + struct Preferences { - + struct Window { static let width: CGFloat = 600.0 static let height: CGFloat = 400.0 } - + struct Toolbar { static let radius: CGFloat = 8.0 static let size: CGFloat = 16.0 static let width: CGFloat = 60.0 static let height: CGFloat = 40.0 } + + struct Sidebar { + static let radius: CGFloat = 8.0 + static let width: CGFloat = 200.0 + } } } |