// Copyright (C) 2024 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import SwiftUI struct Template: Identifiable, Codable, Hashable { var id = UUID() var name: String var content: String init(name: String, content: String) { self.name = name self.content = content } func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: Template, rhs: Template) -> Bool { lhs.id == rhs.id } } 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 } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, NSTextViewDelegate { var parent: SimpleTextEditor init(_ parent: SimpleTextEditor) { self.parent = parent } func textDidChange(_ notification: Notification) { if let textView = notification.object as? NSTextView { parent.text = textView.string } } } } 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 { HStack(spacing: 0) { VStack(alignment: .leading, spacing: 8) { HStack { Text("Templates") .font(.headline) Spacer() Button(action: { showingAddSheet = true }) { Image(systemName: "plus") } .buttonStyle(.borderless) Button(action: removeTemplate) { Image(systemName: "minus") } .buttonStyle(.borderless) .disabled(selectedTemplate == nil) } List(templates, selection: $selectedTemplate) { template in Text(template.name) .tag(template) } .listStyle(.sidebar) } .frame(width: 200) Divider() VStack(alignment: .leading, spacing: 8) { if let selected = selectedTemplate { HStack { Text("Template: \(selected.name)") .font(.headline) Spacer() } SimpleTextEditor(text: $templateText) .onChange(of: templateText) { _, newValue in updateSelectedTemplate(newValue) } } else { VStack { Spacer() Text("Select a template to edit") .foregroundColor(.secondary) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.leading) } .padding() .onAppear { loadTemplates() } .onChange(of: selectedTemplate) { _, newSelection in if let template = newSelection { templateText = template.content } } .sheet(isPresented: $showingAddSheet) { VStack(spacing: 16) { Text("Add New Template") .font(.headline) TextField("Template Name", text: $newTemplateName) .textFieldStyle(.roundedBorder) HStack { Button("Cancel") { showingAddSheet = false newTemplateName = "" } Spacer() Button("Add") { addTemplate() } .disabled(newTemplateName.isEmpty) } } .padding() .frame(width: 300, height: 120) } } 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 saveTemplates() { if let encoded = try? JSONEncoder().encode(templates) { templatesData = encoded } } private func addTemplate() { let newTemplate = Template(name: newTemplateName, content: "") templates.append(newTemplate) 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 updateSelectedTemplate(_ newContent: String) { guard let selected = selectedTemplate, let index = templates.firstIndex(where: { $0.id == selected.id }) else { return } templates[index].content = newContent saveTemplates() } } #Preview { TemplatesPreferencesView() }