From 144915635bdfc90445321189914929a911fe77d4 Mon Sep 17 00:00:00 2001 From: Ruben Beltran del Rio Date: Mon, 16 Sep 2024 20:32:25 +0200 Subject: Add search --- .../Base Components/MapTextEditor.swift | 75 +++++++++++++++++++++- Map/Presentation/Base Components/SearchBar.swift | 74 +++++++++++++++++++++ 2 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 Map/Presentation/Base Components/SearchBar.swift (limited to 'Map/Presentation/Base Components') diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift index 31da790..a7dbe52 100644 --- a/Map/Presentation/Base Components/MapTextEditor.swift +++ b/Map/Presentation/Base Components/MapTextEditor.swift @@ -20,6 +20,19 @@ import SwiftUI class MapTextEditorController: NSViewController { @Binding var document: MapDocument + var highlightRanges: [Range] { + didSet { + updateHighlights() + } + } + + var selectedRange: Int { + didSet { + updateHighlights() + focusOnResult() + } + } + let onChange: () -> Void private let vertexRegex = MapParsingPatterns.vertex @@ -32,9 +45,14 @@ class MapTextEditorController: NSViewController { private let changeDebouncer: Debouncer = Debouncer(seconds: 1) - init(document: Binding, onChange: @escaping () -> Void) { + init( + document: Binding, highlightRanges: [Range], selectedRange: Int, + onChange: @escaping () -> Void + ) { self._document = document self.onChange = onChange + self.highlightRanges = highlightRanges + self.selectedRange = selectedRange super.init(nibName: nil, bundle: nil) } @@ -60,6 +78,50 @@ class MapTextEditorController: NSViewController { 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 + var highlightRanges: [Range] + var selectedRange: Int var onChange: () -> Void = {} func makeNSViewController( context: NSViewControllerRepresentableContext ) -> MapTextEditorController { - return MapTextEditorController(document: $document, onChange: onChange) + return MapTextEditorController( + document: $document, highlightRanges: highlightRanges, selectedRange: selectedRange, + onChange: onChange) } func updateNSViewController( _ nsViewController: MapTextEditorController, context: NSViewControllerRepresentableContext - ) {} + ) { + 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 index 0000000..7e921b0 --- /dev/null +++ b/Map/Presentation/Base Components/SearchBar.swift @@ -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")) +} -- cgit