]> git.r.bdr.sh - rbdr/map/commitdiff
Add search
authorRuben Beltran del Rio <redacted>
Mon, 16 Sep 2024 18:32:25 +0000 (20:32 +0200)
committerRuben Beltran del Rio <redacted>
Mon, 16 Sep 2024 18:32:25 +0000 (20:32 +0200)
Map.xcodeproj/project.pbxproj
Map/Data/FocusedValues+document.swift [new file with mode: 0644]
Map/Logic/MapParser/Strategies/GroupParserStrategy.swift
Map/MapApp.swift
Map/Presentation/Base Components/MapTextEditor.swift
Map/Presentation/Base Components/SearchBar.swift [new file with mode: 0644]
Map/Presentation/Commands/MapCommands.swift
Map/Presentation/Commands/UpdateCommands.swift
Map/Presentation/MapEditor.swift
Map/Presentation/Theme/NSColor+theme.swift

index 7dc8883d30711d677b7397d9719de3f6392eb006..5601b8e3ed4aa4d015d5c917f5d8f00bcc3fe4e0 100644 (file)
@@ -48,6 +48,8 @@
                B54587252C961E9E0067B788 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587242C961E9E0067B788 /* MapTests.swift */; };
                B545872F2C961E9E0067B788 /* MapUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B545872E2C961E9E0067B788 /* MapUITests.swift */; };
                B54587312C961E9E0067B788 /* MapUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */; };
                B54587252C961E9E0067B788 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587242C961E9E0067B788 /* MapTests.swift */; };
                B545872F2C961E9E0067B788 /* MapUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B545872E2C961E9E0067B788 /* MapUITests.swift */; };
                B54587312C961E9E0067B788 /* MapUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */; };
+               B5D42DC22C984E870075473D /* FocusedValues+document.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D42DC12C984E7F0075473D /* FocusedValues+document.swift */; };
+               B5D42DC42C9851ED0075473D /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5D42DC32C9851ED0075473D /* SearchBar.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
                B545872A2C961E9E0067B788 /* Map2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Map2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
                B545872E2C961E9E0067B788 /* MapUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITests.swift; sourceTree = "<group>"; };
                B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITestsLaunchTests.swift; sourceTree = "<group>"; };
                B545872A2C961E9E0067B788 /* Map2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Map2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
                B545872E2C961E9E0067B788 /* MapUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITests.swift; sourceTree = "<group>"; };
                B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITestsLaunchTests.swift; sourceTree = "<group>"; };
+               B5D42DC12C984E7F0075473D /* FocusedValues+document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FocusedValues+document.swift"; sourceTree = "<group>"; };
+               B5D42DC32C9851ED0075473D /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
                B5012E3C2C96222E00AC4D68 /* Data */ = {
                        isa = PBXGroup;
                        children = (
                B5012E3C2C96222E00AC4D68 /* Data */ = {
                        isa = PBXGroup;
                        children = (
+                               B5D42DC12C984E7F0075473D /* FocusedValues+document.swift */,
                                B5012E412C96235B00AC4D68 /* Stage.swift */,
                                B54587112C961E9C0067B788 /* MapDocument.swift */,
                        );
                                B5012E412C96235B00AC4D68 /* Stage.swift */,
                                B54587112C961E9C0067B788 /* MapDocument.swift */,
                        );
                                B5012E6A2C96255A00AC4D68 /* MapRender */,
                                B5012E462C96243500AC4D68 /* MapTextEditor.swift */,
                                B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */,
                                B5012E6A2C96255A00AC4D68 /* MapRender */,
                                B5012E462C96243500AC4D68 /* MapTextEditor.swift */,
                                B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */,
+                               B5D42DC32C9851ED0075473D /* SearchBar.swift */,
                        );
                        path = "Base Components";
                        sourceTree = "<group>";
                        );
                        path = "Base Components";
                        sourceTree = "<group>";
                                B54587122C961E9C0067B788 /* MapDocument.swift in Sources */,
                                B54587102C961E9C0067B788 /* MapApp.swift in Sources */,
                                B5012E8C2C98244000AC4D68 /* ViewStyle.swift in Sources */,
                                B54587122C961E9C0067B788 /* MapDocument.swift in Sources */,
                                B54587102C961E9C0067B788 /* MapApp.swift in Sources */,
                                B5012E8C2C98244000AC4D68 /* ViewStyle.swift in Sources */,
+                               B5D42DC42C9851ED0075473D /* SearchBar.swift in Sources */,
                                B5012E8E2C9828D000AC4D68 /* Constants.swift in Sources */,
                                B5012E7C2C972B6C00AC4D68 /* GroupParserStrategy.swift in Sources */,
                                B5012E6B2C96255A00AC4D68 /* MapAxes.swift in Sources */,
                                B5012E8E2C9828D000AC4D68 /* Constants.swift in Sources */,
                                B5012E7C2C972B6C00AC4D68 /* GroupParserStrategy.swift in Sources */,
                                B5012E6B2C96255A00AC4D68 /* MapAxes.swift in Sources */,
                                B5012E5B2C96249400AC4D68 /* OpportunityParserStrategy.swift in Sources */,
                                B5012E5C2C96249400AC4D68 /* EdgeParserStrategy.swift in Sources */,
                                B5012E5D2C96249400AC4D68 /* StageParserStrategy.swift in Sources */,
                                B5012E5B2C96249400AC4D68 /* OpportunityParserStrategy.swift in Sources */,
                                B5012E5C2C96249400AC4D68 /* EdgeParserStrategy.swift in Sources */,
                                B5012E5D2C96249400AC4D68 /* StageParserStrategy.swift in Sources */,
+                               B5D42DC22C984E870075473D /* FocusedValues+document.swift in Sources */,
                                B5012E812C97318600AC4D68 /* MapGroups.swift in Sources */,
                                B5012E8A2C98235500AC4D68 /* MapCommands.swift in Sources */,
                                B5012E5E2C96249400AC4D68 /* Debouncer.swift in Sources */,
                                B5012E812C97318600AC4D68 /* MapGroups.swift in Sources */,
                                B5012E8A2C98235500AC4D68 /* MapCommands.swift in Sources */,
                                B5012E5E2C96249400AC4D68 /* Debouncer.swift in Sources */,
diff --git a/Map/Data/FocusedValues+document.swift b/Map/Data/FocusedValues+document.swift
new file mode 100644 (file)
index 0000000..a28404f
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ 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 DocumentFocusedValueKey: FocusedValueKey {
+  typealias Value = Binding<MapDocument>
+}
+
+struct IsSearchingValueKey: FocusedValueKey {
+  typealias Value = Binding<Bool>
+}
+
+struct SelectedEvolutionValueKey: FocusedValueKey {
+  typealias Value = Binding<StageType>
+}
+
+struct FileURLValueKey: FocusedValueKey {
+  typealias Value = URL
+}
+
+extension FocusedValues {
+  var document: DocumentFocusedValueKey.Value? {
+    get { self[DocumentFocusedValueKey.self] }
+    set { self[DocumentFocusedValueKey.self] = newValue }
+  }
+  var isSearching: IsSearchingValueKey.Value? {
+    get { self[IsSearchingValueKey.self] }
+    set { self[IsSearchingValueKey.self] = newValue }
+  }
+  var selectedEvolution: SelectedEvolutionValueKey.Value? {
+    get { self[SelectedEvolutionValueKey.self] }
+    set { self[SelectedEvolutionValueKey.self] = newValue }
+  }
+  var fileURL: FileURLValueKey.Value? {
+    get { self[FileURLValueKey.self] }
+    set { self[FileURLValueKey.self] = newValue }
+  }
+}
index d1acec2764f70a4d78bbf3e67c673c8eed335103..d5ab60c5c8a218699878ccad3ea206597a7fa596 100644 (file)
@@ -33,9 +33,10 @@ struct GroupParserStrategy: MapParserStrategy {
     var groupVertices: [Vertex] = []
     let vertexIdString = String(line[Range(match.range(at: 2), in: line)!])
     let vertexIds = vertexIdString.split(separator: ",", omittingEmptySubsequences: true).map(
     var groupVertices: [Vertex] = []
     let vertexIdString = String(line[Range(match.range(at: 2), in: line)!])
     let vertexIds = vertexIdString.split(separator: ",", omittingEmptySubsequences: true).map(
-      String.init).map({ vertexId in
-        vertexId.trimmingCharacters(in: .whitespacesAndNewlines)
-      })
+      String.init
+    ).map({ vertexId in
+      vertexId.trimmingCharacters(in: .whitespacesAndNewlines)
+    })
 
     for vertexId in vertexIds {
       if let vertex = vertices[vertexId] {
 
     for vertexId in vertexIds {
       if let vertex = vertices[vertexId] {
index fa60ab098c1a3b6ab98ab4cb2d6823ac9a9ea29d..4578bd85d53143fe77afa000831404fbd7469afb 100644 (file)
@@ -1,3 +1,4 @@
+import Sparkle
 /*
  Copyright (C) 2024 Rubén Beltrán del Río
 
 /*
  Copyright (C) 2024 Rubén Beltrán del Río
 
  along with this program. If not, see https://map.tranquil.systems.
  */
 import SwiftUI
  along with this program. If not, see https://map.tranquil.systems.
  */
 import SwiftUI
-import Sparkle
 
 @main
 struct MapApp: App {
 
 @main
 struct MapApp: App {
-  
-  private let updaterController: SPUStandardUpdaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
-  
+
+  private let updaterController: SPUStandardUpdaterController = SPUStandardUpdaterController(
+    startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
+
   var body: some Scene {
     DocumentGroup(newDocument: MapDocument()) { file in
       MapEditor(document: file.$document, url: file.fileURL)
   var body: some Scene {
     DocumentGroup(newDocument: MapDocument()) { file in
       MapEditor(document: file.$document, url: file.fileURL)
+        .focusedSceneValue(\.document, file.$document)
+        .focusedSceneValue(\.fileURL, file.fileURL)
     }.commands {
       MapCommands()
       UpdateCommands(updaterController: updaterController)
     }.commands {
       MapCommands()
       UpdateCommands(updaterController: updaterController)
index 31da790c41cbe3b9c009e99364c1d928a335be52..a7dbe52ee644b97ad0d3ad37cc3fff2b77d3447e 100644 (file)
@@ -20,6 +20,19 @@ import SwiftUI
 class MapTextEditorController: NSViewController {
 
   @Binding var document: MapDocument
 class MapTextEditorController: NSViewController {
 
   @Binding var document: MapDocument
+  var highlightRanges: [Range<String.Index>] {
+    didSet {
+      updateHighlights()
+    }
+  }
+
+  var selectedRange: Int {
+    didSet {
+      updateHighlights()
+      focusOnResult()
+    }
+  }
+
   let onChange: () -> Void
 
   private let vertexRegex = MapParsingPatterns.vertex
   let onChange: () -> Void
 
   private let vertexRegex = MapParsingPatterns.vertex
@@ -32,9 +45,14 @@ class MapTextEditorController: NSViewController {
 
   private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
 
 
   private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
 
-  init(document: Binding<MapDocument>, onChange: @escaping () -> Void) {
+  init(
+    document: Binding<MapDocument>, highlightRanges: [Range<String.Index>], selectedRange: Int,
+    onChange: @escaping () -> Void
+  ) {
     self._document = document
     self.onChange = onChange
     self._document = document
     self.onChange = onChange
+    self.highlightRanges = highlightRanges
+    self.selectedRange = selectedRange
     super.init(nibName: nil, bundle: nil)
   }
 
     super.init(nibName: nil, bundle: nil)
   }
 
@@ -60,6 +78,50 @@ class MapTextEditorController: NSViewController {
 
   override func viewDidAppear() {
     self.view.window?.makeFirstResponder(self.view)
 
   override func viewDidAppear() {
     self.view.window?.makeFirstResponder(self.view)
+    updateHighlights()
+  }
+
+  private var textView: NSTextView? {
+    return (view as? NSScrollView)?.documentView as? NSTextView
+  }
+
+  private func updateHighlights() {
+    if let textView {
+      if let textStorage = textView.textStorage {
+        textStorage.removeAttribute(
+          .backgroundColor, range: NSRange(location: 0, length: textStorage.length))
+
+        for range in highlightRanges {
+          let nsRange = NSRange(range, in: textStorage.string)
+
+          textStorage.addAttribute(.backgroundColor, value: NSColor.syntax.match, range: nsRange)
+        }
+
+        textView.needsDisplay = true
+
+      }
+    }
+  }
+
+  private func focusOnResult() {
+    if let textView {
+      if let textStorage = textView.textStorage {
+        if selectedRange < highlightRanges.count {
+          let range = highlightRanges[selectedRange]
+          let nsRange = NSRange(range, in: textStorage.string)
+          textView.scrollRangeToVisible(nsRange)
+          textView.selectedRange = nsRange
+        }
+      }
+    }
+  }
+
+  private func setSelectionColor() {
+    guard let textView = self.textView else { return }
+
+    var selectedTextAttributes = textView.selectedTextAttributes
+    selectedTextAttributes[.backgroundColor] = NSColor.yellow.withAlphaComponent(0.3)
+    textView.selectedTextAttributes = selectedTextAttributes
   }
 }
 
   }
 }
 
@@ -182,16 +244,23 @@ extension MapTextEditorController: NSTextStorageDelegate {
 struct MapTextEditor: NSViewControllerRepresentable {
 
   @Binding var document: MapDocument
 struct MapTextEditor: NSViewControllerRepresentable {
 
   @Binding var document: MapDocument
+  var highlightRanges: [Range<String.Index>]
+  var selectedRange: Int
   var onChange: () -> Void = {}
 
   func makeNSViewController(
     context: NSViewControllerRepresentableContext<MapTextEditor>
   ) -> MapTextEditorController {
   var onChange: () -> Void = {}
 
   func makeNSViewController(
     context: NSViewControllerRepresentableContext<MapTextEditor>
   ) -> MapTextEditorController {
-    return MapTextEditorController(document: $document, onChange: onChange)
+    return MapTextEditorController(
+      document: $document, highlightRanges: highlightRanges, selectedRange: selectedRange,
+      onChange: onChange)
   }
 
   func updateNSViewController(
     _ nsViewController: MapTextEditorController,
     context: NSViewControllerRepresentableContext<MapTextEditor>
   }
 
   func updateNSViewController(
     _ nsViewController: MapTextEditorController,
     context: NSViewControllerRepresentableContext<MapTextEditor>
-  ) {}
+  ) {
+    nsViewController.highlightRanges = highlightRanges
+    nsViewController.selectedRange = selectedRange
+  }
 }
 }
diff --git a/Map/Presentation/Base Components/SearchBar.swift b/Map/Presentation/Base Components/SearchBar.swift
new file mode 100644 (file)
index 0000000..7e921b0
--- /dev/null
@@ -0,0 +1,74 @@
+/*
+ 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 SearchBar: View {
+
+  @Binding var term: String
+  @FocusState var isSearchFocused: Bool
+  var onNext: () -> Void = {}
+  var onPrevious: () -> Void = {}
+  var onSubmit: () -> Void = {}
+  var onDismiss: () -> Void = {}
+
+  var body: some View {
+    HStack(spacing: 2.0) {
+      ZStack {
+        TextField("Search", text: $term)
+          .textFieldStyle(.roundedBorder)
+          .padding(.trailing, 16.0)
+          .focused($isSearchFocused)
+          .onAppear {
+            isSearchFocused = true
+          }
+          .onKeyPress { press in
+            if press.key == .return {
+              if press.modifiers.contains(.shift) {
+                onPrevious()
+                return .handled
+              }
+              onNext()
+              return .handled
+            }
+            return .ignored
+          }
+      }
+      Spacer()
+      Button(action: onPrevious) {
+        Image(systemName: "chevron.left")
+          .font(.theme.smallControl)
+      }.keyboardShortcut(
+        "g", modifiers: EventModifiers([.command, .shift])
+      ).help("Find Previous (⇧⌘G)")
+      Button(action: onNext) {
+        Image(systemName: "chevron.right")
+          .font(.theme.smallControl)
+      }.keyboardShortcut(
+        "g", modifiers: EventModifiers([.command])
+      ).help("Find Next (⌘G)")
+      Button(action: onDismiss) {
+        Text("Done")
+          .font(.theme.smallControl)
+      }.keyboardShortcut(.escape, modifiers: EventModifiers())
+        .help("Done (⎋)")
+    }.padding(4.0)
+  }
+}
+
+#Preview {
+  SearchBar(term: .constant("Hello"))
+}
index b0fbf9cf2447112b13998a34d18028e187514cc4..83c4b273eb60a49459b07feb3a2ade21fad35064 100644 (file)
@@ -20,9 +20,63 @@ struct MapCommands: Commands {
 
   @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
   @AppStorage("zoom") var zoom = 1.0
 
   @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
   @AppStorage("zoom") var zoom = 1.0
+  @FocusedBinding(\.document) var document: MapDocument?
+  @FocusedValue(\.fileURL) var url: URL?
+  @FocusedBinding(\.selectedEvolution) var selectedEvolution: StageType?
+  @FocusedBinding(\.isSearching) var isSearching
 
   var body: some Commands {
 
 
   var body: some Commands {
 
+    // File
+
+    CommandGroup(after: CommandGroupPlacement.saveItem) {
+      Divider()
+      Button("Export...") {
+        if let selectedEvolution, let document {
+          if let image = document.exportAsImage(withEvolution: selectedEvolution) {
+
+            let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled"
+
+            let savePanel = NSSavePanel()
+            savePanel.allowedContentTypes = [.png]
+            savePanel.canCreateDirectories = true
+            savePanel.isExtensionHidden = false
+            savePanel.title = "Save \(filename) as image"
+            savePanel.message = "Choose a location to save the image"
+            savePanel.nameFieldStringValue = "\(filename).png"
+            savePanel.begin { result in
+              if result == .OK, let url = savePanel.url {
+                if let tiffRepresentation = image.tiffRepresentation {
+                  let bitmapImage = NSBitmapImageRep(data: tiffRepresentation)
+                  let pngData = bitmapImage?.representation(using: .png, properties: [:])
+                  do {
+                    try pngData?.write(to: url)
+                  } catch {
+                    return
+                  }
+                }
+              }
+            }
+          }
+        }
+      }.keyboardShortcut(
+        "e", modifiers: EventModifiers([.command])
+      ).disabled(document == nil)
+    }
+
+    // Edit
+
+    CommandGroup(after: CommandGroupPlacement.pasteboard) {
+      Divider()
+      Button("Find...") {
+        withAnimation {
+          isSearching = isSearching != nil ? !isSearching! : true
+        }
+      }.keyboardShortcut(
+        "f", modifiers: EventModifiers([.command])
+      ).disabled(document == nil)
+    }
+
     // View
 
     CommandGroup(after: CommandGroupPlacement.toolbar) {
     // View
 
     CommandGroup(after: CommandGroupPlacement.toolbar) {
@@ -31,25 +85,25 @@ struct MapCommands: Commands {
           viewStyle = .vertical
         }.keyboardShortcut(
           "l", modifiers: EventModifiers([.command])
           viewStyle = .vertical
         }.keyboardShortcut(
           "l", modifiers: EventModifiers([.command])
-        )
+        ).disabled(document == nil)
       } else {
         Button("Use Horizontal Layout") {
           viewStyle = .horizontal
         }.keyboardShortcut(
           "l", modifiers: EventModifiers([.command])
       } else {
         Button("Use Horizontal Layout") {
           viewStyle = .horizontal
         }.keyboardShortcut(
           "l", modifiers: EventModifiers([.command])
-        )
+        ).disabled(document == nil)
       }
       Divider()
       Button("Zoom In") {
         zoom = min(Constants.kMaxZoom, zoom + 0.1)
       }.keyboardShortcut(
         "+", modifiers: EventModifiers([.command])
       }
       Divider()
       Button("Zoom In") {
         zoom = min(Constants.kMaxZoom, zoom + 0.1)
       }.keyboardShortcut(
         "+", modifiers: EventModifiers([.command])
-      )
+      ).disabled(document == nil)
       Button("Zoom Out") {
         zoom = max(Constants.kMinZoom, zoom - 0.1)
       }.keyboardShortcut(
         "-", modifiers: EventModifiers([.command])
       Button("Zoom Out") {
         zoom = max(Constants.kMinZoom, zoom - 0.1)
       }.keyboardShortcut(
         "-", modifiers: EventModifiers([.command])
-      )
+      ).disabled(document == nil)
       Divider()
     }
 
       Divider()
     }
 
index 51d2b0a87fd150d872e3baac528b6e597852c327..3a019cc79eda8150fd825d73d3788eb455cc27d8 100644 (file)
@@ -1,3 +1,4 @@
+import Sparkle
 /*
  Copyright (C) 2024 Rubén Beltrán del Río
 
 /*
  Copyright (C) 2024 Rubén Beltrán del Río
 
  along with this program. If not, see https://map.tranquil.systems.
  */
 import SwiftUI
  along with this program. If not, see https://map.tranquil.systems.
  */
 import SwiftUI
-import Sparkle
 
 struct UpdateCommands: Commands {
   let updaterController: SPUStandardUpdaterController
 
 struct UpdateCommands: Commands {
   let updaterController: SPUStandardUpdaterController
-  
+
   var body: some Commands {
     CommandGroup(after: .appInfo) {
       CheckForUpdatesView(updater: updaterController.updater)
   var body: some Commands {
     CommandGroup(after: .appInfo) {
       CheckForUpdatesView(updater: updaterController.updater)
@@ -27,29 +27,28 @@ struct UpdateCommands: Commands {
   }
 }
 
   }
 }
 
-
 struct CheckForUpdatesView: View {
 struct CheckForUpdatesView: View {
-    @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel
-    private let updater: SPUUpdater
-    
-    init(updater: SPUUpdater) {
-        self.updater = updater
-        
-        // Create our view model for our CheckForUpdatesView
-        self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater)
-    }
-    
-    var body: some View {
-        Button("Check for Updates…", action: updater.checkForUpdates)
-            .disabled(!checkForUpdatesViewModel.canCheckForUpdates)
-    }
+  @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel
+  private let updater: SPUUpdater
+
+  init(updater: SPUUpdater) {
+    self.updater = updater
+
+    // Create our view model for our CheckForUpdatesView
+    self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater)
+  }
+
+  var body: some View {
+    Button("Check for Updates…", action: updater.checkForUpdates)
+      .disabled(!checkForUpdatesViewModel.canCheckForUpdates)
+  }
 }
 
 final class CheckForUpdatesViewModel: ObservableObject {
 }
 
 final class CheckForUpdatesViewModel: ObservableObject {
-    @Published var canCheckForUpdates = false
+  @Published var canCheckForUpdates = false
 
 
-    init(updater: SPUUpdater) {
-        updater.publisher(for: \.canCheckForUpdates)
-            .assign(to: &$canCheckForUpdates)
-    }
+  init(updater: SPUUpdater) {
+    updater.publisher(for: \.canCheckForUpdates)
+      .assign(to: &$canCheckForUpdates)
+  }
 }
 }
index 8c5ab2214c3cc7283d9d95044753222a205a2445..d7c93dda3ce597fce10ec123b32a3befd71d03f5 100644 (file)
@@ -20,18 +20,75 @@ struct MapEditor: View {
   @Binding var document: MapDocument
   var url: URL?
   @State var selectedEvolution: StageType = .behavior
   @Binding var document: MapDocument
   var url: URL?
   @State var selectedEvolution: StageType = .behavior
+  @State var isSearching: Bool = false
 
   @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
 
   let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
   @AppStorage("zoom") var zoom = 1.0
   @State var lastZoom = 1.0
 
   @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
 
   let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
   @AppStorage("zoom") var zoom = 1.0
   @State var lastZoom = 1.0
+  @State var searchTerm = ""
+  @State var selectedTerm = 0
+
+  var results: [Range<String.Index>] {
+    if !isSearching || searchTerm.isEmpty {
+      return []
+    }
+    let options: NSString.CompareOptions = [.caseInsensitive, .diacriticInsensitive]
+    var searchRange = document.text.startIndex..<document.text.endIndex
+    var ranges: [Range<String.Index>] = []
+
+    while let range = document.text.range(of: searchTerm, options: options, range: searchRange) {
+      ranges.append(range)
+      searchRange = range.upperBound..<document.text.endIndex
+    }
+
+    return ranges
+
+  }
 
   var body: some View {
     VStack(spacing: 0) {
 
   var body: some View {
     VStack(spacing: 0) {
+      if isSearching {
+        SearchBar(
+          term: $searchTerm,
+          onNext: {
+            withAnimation {
+              if results.count > 0 {
+                selectedTerm = (selectedTerm + 1) % results.count
+              }
+            }
+          },
+          onPrevious: {
+            withAnimation {
+              if results.count > 0 {
+                if selectedTerm == 0 {
+                  selectedTerm = results.count - 1
+                } else {
+                  selectedTerm = (selectedTerm - 1) % results.count
+                }
+              }
+            }
+          },
+          onSubmit: {
+
+          },
+          onDismiss: {
+            withAnimation {
+              isSearching = false
+            }
+          }
+        )
+        .onChange(
+          of: searchTerm,
+          {
+            selectedTerm = 0
+          })
+        Divider()
+      }
       adaptiveStack {
         ZStack(alignment: .topLeading) {
       adaptiveStack {
         ZStack(alignment: .topLeading) {
-          MapTextEditor(document: $document)
+          MapTextEditor(document: $document, highlightRanges: results, selectedRange: selectedTerm)
             .background(Color.ui.background)
             .foregroundColor(Color.ui.foreground)
             .frame(minHeight: 96.0)
             .background(Color.ui.background)
             .foregroundColor(Color.ui.foreground)
             .frame(minHeight: 96.0)
@@ -81,14 +138,10 @@ struct MapEditor: View {
       }.padding(4.0)
     }.toolbar {
       HStack {
       }.padding(4.0)
     }.toolbar {
       HStack {
-        Button(action: saveImage) {
-          Image(systemName: "photo")
-        }
-        .help("Export Image (⌘E)")
-        .padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
+        EvolutionPicker(selectedEvolution: $selectedEvolution)
       }
       }
-      EvolutionPicker(selectedEvolution: $selectedEvolution)
-    }
+    }.focusedSceneValue(\.isSearching, $isSearching)
+      .focusedSceneValue(\.selectedEvolution, $selectedEvolution)
   }
 
   @ViewBuilder
   }
 
   @ViewBuilder
@@ -114,34 +167,6 @@ struct MapEditor: View {
 
   private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
   }
 
   private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
   }
-
-  private func saveImage() {
-    if let image = document.exportAsImage(withEvolution: selectedEvolution) {
-
-      let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled"
-
-      let savePanel = NSSavePanel()
-      savePanel.allowedContentTypes = [.png]
-      savePanel.canCreateDirectories = true
-      savePanel.isExtensionHidden = false
-      savePanel.title = "Save \(filename) as image"
-      savePanel.message = "Choose a location to save the image"
-      savePanel.nameFieldStringValue = "\(filename).png"
-      savePanel.begin { result in
-        if result == .OK, let url = savePanel.url {
-          if let tiffRepresentation = image.tiffRepresentation {
-            let bitmapImage = NSBitmapImageRep(data: tiffRepresentation)
-            let pngData = bitmapImage?.representation(using: .png, properties: [:])
-            do {
-              try pngData?.write(to: url)
-            } catch {
-              return
-            }
-          }
-        }
-      }
-    }
-  }
 }
 
 #Preview {
 }
 
 #Preview {
index 2b9b8f7c4272096300615dace3ac59f70c179de0..f1a046daf3aa2235e31ff2764a80698e0c11420c 100644 (file)
@@ -22,6 +22,9 @@ extension NSColor {
     static let number = NSColor(named: "Number") ?? .textColor
     static let option = NSColor(named: "Option") ?? .textColor
     static let symbol = NSColor(named: "Symbol") ?? .textColor
     static let number = NSColor(named: "Number") ?? .textColor
     static let option = NSColor(named: "Option") ?? .textColor
     static let symbol = NSColor(named: "Symbol") ?? .textColor
+    static let match = (NSColor(named: "Light Neutral Gray") ?? .textColor).withAlphaComponent(0.3)
+    static let highlightMatch = (NSColor(named: "Naples Yellow") ?? .textColor).withAlphaComponent(
+      0.3)
   }
 
   struct ui {
   }
 
   struct ui {