diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 724dd818d..c4cf1a0e2 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -118,6 +118,8 @@ A6672FBF25C7F77000090DE3 /* NoteBodyExcerptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6672FBD25C7F77000090DE3 /* NoteBodyExcerptTests.swift */; }; A667305425C9751B00090DE3 /* SearchMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A667305325C9751B00090DE3 /* SearchMapView.swift */; }; A6BBDA3325501398005C8343 /* SPTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6BBDA3225501398005C8343 /* SPTextView.swift */; }; + A6FFE3AF25CAC75400F937A5 /* SearchMatchesBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6FFE3AD25CAC75400F937A5 /* SearchMatchesBarViewController.swift */; }; + A6FFE3B025CAC75400F937A5 /* SearchMatchesBarViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = A6FFE3AE25CAC75400F937A5 /* SearchMatchesBarViewController.xib */; }; B50099322421218B0037A431 /* NSTextViewSimplenoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50099312421218B0037A431 /* NSTextViewSimplenoteTests.swift */; }; B5009937242130F70037A431 /* UnicodeScalar+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5009936242130F70037A431 /* UnicodeScalar+Simplenote.swift */; }; B5009938242130F70037A431 /* UnicodeScalar+Simplenote.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5009936242130F70037A431 /* UnicodeScalar+Simplenote.swift */; }; @@ -555,6 +557,8 @@ A6672FBD25C7F77000090DE3 /* NoteBodyExcerptTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoteBodyExcerptTests.swift; sourceTree = ""; }; A667305325C9751B00090DE3 /* SearchMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMapView.swift; sourceTree = ""; }; A6BBDA3225501398005C8343 /* SPTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPTextView.swift; sourceTree = ""; }; + A6FFE3AD25CAC75400F937A5 /* SearchMatchesBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchMatchesBarViewController.swift; sourceTree = ""; }; + A6FFE3AE25CAC75400F937A5 /* SearchMatchesBarViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchMatchesBarViewController.xib; sourceTree = ""; }; B50099312421218B0037A431 /* NSTextViewSimplenoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSTextViewSimplenoteTests.swift; sourceTree = ""; }; B5009936242130F70037A431 /* UnicodeScalar+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UnicodeScalar+Simplenote.swift"; sourceTree = ""; }; B5009939242131410037A431 /* UnicodeScalarSimplenoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnicodeScalarSimplenoteTests.swift; sourceTree = ""; }; @@ -1094,6 +1098,15 @@ name = config; sourceTree = ""; }; + A6FFE39F25CABD6800F937A5 /* SearchMatchesBar */ = { + isa = PBXGroup; + children = ( + A6FFE3AD25CAC75400F937A5 /* SearchMatchesBarViewController.swift */, + A6FFE3AE25CAC75400F937A5 /* SearchMatchesBarViewController.xib */, + ); + name = SearchMatchesBar; + sourceTree = ""; + }; B501AAD22437E52D0084CDA3 /* Constants */ = { isa = PBXGroup; children = ( @@ -1442,6 +1455,7 @@ B5C7DD4B243E697F00BEE354 /* ViewControllers */ = { isa = PBXGroup; children = ( + A6FFE39F25CABD6800F937A5 /* SearchMatchesBar */, B53BF19E24AC1F9500938C34 /* Editor */, B5C63352251E727300C8BF46 /* Interlinks */, B55C313124A5339300B23B3F /* Metrics */, @@ -1696,6 +1710,7 @@ 37AE49C91FFEBB0B00FCB165 /* markdown-dark.css in Resources */, B5686D0E1AEAE6D7009F9E20 /* Images.xcassets in Resources */, B58117D625B9D5F500927E0C /* AccountVerificationViewController.xib in Resources */, + A6FFE3B025CAC75400F937A5 /* SearchMatchesBarViewController.xib in Resources */, B5C7DD43243E4A8E00BEE354 /* CollaborateViewController.xib in Resources */, B5A89195231ECB3C0007EDCB /* LICENSE in Resources */, 46F0E66717A3300B005BB4D1 /* Localizable.strings in Resources */, @@ -2008,6 +2023,7 @@ B58CE26524F9AE870079C04B /* NSStoryboard+Simplenote.swift in Sources */, 3700E97621C1E390004771C9 /* SPTextAttachment.swift in Sources */, B5E0862F2448E58C00DEF476 /* NSImageName+Simplenote.swift in Sources */, + A6FFE3AF25CAC75400F937A5 /* SearchMatchesBarViewController.swift in Sources */, B54F9A6724D0B21D00BCF754 /* NSNotification+Simplenote.swift in Sources */, B505068D25A3DF4E008A8E10 /* SortBarView.swift in Sources */, 3712FC8E1FE1ACAA008544AC /* Theme.swift in Sources */, diff --git a/Simplenote/NoteEditorViewController+Swift.swift b/Simplenote/NoteEditorViewController+Swift.swift index 10bf19ca4..9cac02c50 100644 --- a/Simplenote/NoteEditorViewController+Swift.swift +++ b/Simplenote/NoteEditorViewController+Swift.swift @@ -34,6 +34,44 @@ extension NoteEditorViewController { clipView.contentInsets.top = SplitItemMetrics.editorContentTopInset scrollView.scrollerInsets.top = SplitItemMetrics.editorScrollerTopInset } + + @objc + func setupSearchMatchesBar() { + let viewController = SearchMatchesBarViewController() + addChild(viewController) + viewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(viewController.view) + + NSLayoutConstraint.activate([ + viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + viewController.view.topAnchor.constraint(equalTo: toolbarView.bottomAnchor), + viewController.view.heightAnchor.constraint(equalToConstant: 28), + ]) + + viewController.onCompletion = { [weak self] in +// self?.toolbarView.endSearch() + } + + searchMatchesBarViewController = viewController + } + + @objc + func setupSearchMap() { + let searchMapView = SearchMapView() + searchMapView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(searchMapView) + + NSLayoutConstraint.activate([ + searchMapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + searchMapView.widthAnchor.constraint(equalToConstant: EditorMetrics.searchMapWidth), + searchMapView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + searchMapView.topAnchor.constraint(equalTo: toolbarView.bottomAnchor) + ]) + + self.searchMapView = searchMapView + } } @@ -238,6 +276,8 @@ extension NoteEditorViewController { if let note = note { storage.refreshStyle(markdownEnabled: note.markdown) } + + searchMatchesBarViewController?.refreshStyle() } /// Refreshes the Toolbar's Inner State @@ -913,39 +953,19 @@ extension NoteEditorViewController { private func updateKeywordsHighlight() { let ranges = highlightedRanges - noteEditor.highlightedRanges = ranges + searchMatchesBarViewController?.setup(with: ranges.count, onChange: { [weak self] (index) in + let range = ranges[index] + self?.noteEditor.scrollRangeToVisible(range) + self?.noteEditor.showFindIndicator(for: range) + }) + searchMatchesBarViewController?.view.isHidden = ranges.isEmpty - createSearchMapViewIfNeeded() + noteEditor.highlightedRanges = ranges searchMapView?.update(with: noteEditor.relativeLocationsForText(in: ranges)) } } -// MARK: - Search Map -// -extension NoteEditorViewController { - private func createSearchMapViewIfNeeded() { - guard searchMapView == nil else { - return - } - - let searchMapView = SearchMapView() - searchMapView.translatesAutoresizingMaskIntoConstraints = false - - view.addSubview(searchMapView) - - NSLayoutConstraint.activate([ - searchMapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - searchMapView.widthAnchor.constraint(equalToConstant: EditorMetrics.searchMapWidth), - searchMapView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - searchMapView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: SplitItemMetrics.editorScrollerTopInset) - ]) - - self.searchMapView = searchMapView - } -} - - // MARK: - EditorMetrics // private enum EditorMetrics { diff --git a/Simplenote/NoteEditorViewController.h b/Simplenote/NoteEditorViewController.h index 8c0475ab0..ec0a275b8 100644 --- a/Simplenote/NoteEditorViewController.h +++ b/Simplenote/NoteEditorViewController.h @@ -20,7 +20,7 @@ @class ToolbarView; @class SPTextView; @class SearchMapView; -@class SearchQuery; +@class SearchMatchesBarViewController; NS_ASSUME_NONNULL_BEGIN @@ -75,6 +75,7 @@ typedef NS_ENUM(NSInteger, NoteFontSize) { @property (nonatomic, weak) id noteActionsDelegate; @property (nonatomic, weak) id tagActionsDelegate; @property (nonatomic, strong, nullable) SearchMapView *searchMapView; +@property (nonatomic, strong, nullable) SearchMatchesBarViewController *searchMatchesBarViewController; // TODO: Switch NSObject >> SearchQuery. ObjC compiler isn't picking up the Swift Package =( @property (nonatomic, strong, nullable) NSObject *searchQuery; diff --git a/Simplenote/NoteEditorViewController.m b/Simplenote/NoteEditorViewController.m index 6740c285d..4bd83c1b7 100644 --- a/Simplenote/NoteEditorViewController.m +++ b/Simplenote/NoteEditorViewController.m @@ -99,6 +99,8 @@ - (void)viewDidLoad [self setupScrollView]; [self setupStatusImageView]; [self setupTagsField]; + [self setupSearchMap]; + [self setupSearchMatchesBar]; // Preload Markdown Preview self.markdownViewController = [MarkdownViewController new]; diff --git a/Simplenote/SearchMatchesBarViewController.swift b/Simplenote/SearchMatchesBarViewController.swift new file mode 100644 index 000000000..68c9d14e8 --- /dev/null +++ b/Simplenote/SearchMatchesBarViewController.swift @@ -0,0 +1,114 @@ +import Cocoa + +// MARK: - SearchMatchesBarViewController +// +class SearchMatchesBarViewController: NSViewController { + + @IBOutlet private weak var textLabel: NSTextField! + @IBOutlet private weak var navigationControl: NSSegmentedControl! + @IBOutlet private weak var doneButton: NSButton! { + didSet { + doneButton.title = Localization.doneButton + } + } + + private var total: Int = 0 + + /// + private var current: Int = Constants.defaultCurrentValue { + didSet { + if oldValue != current, current >= 0 { + onChange?(current) + } + + update() + } + } + + private var onChange: ((_ current: Int) -> Void)? + + /// Callback will be invoked when user presses "done" button + /// + var onCompletion: (() -> Void)? + + override func viewDidLoad() { + super.viewDidLoad() + refreshStyle() + update() + } + + /// Setup with total number of matches and "on change" callback + /// + func setup(with total: Int, onChange: @escaping (_ current: Int) -> Void) { + self.onChange = onChange + self.total = total + current = Constants.defaultCurrentValue + } + + private func update() { + textLabel.stringValue = Localization.matches(with: total) + + navigationControl.setEnabled(current - 1 >= 0 && total > 0, forSegment: Constants.backButtonIndex) + navigationControl.setEnabled(current + 1 < total && total > 0, forSegment: Constants.forwardButtonIndex) + } +} + + +// MARK: - Style +// +extension SearchMatchesBarViewController { + func refreshStyle() { + textLabel.textColor = .simplenoteSecondaryTextColor + doneButton.contentTintColor = .simplenoteActionButtonTintColor + } +} + + +// MARK: - Actions +// +private extension SearchMatchesBarViewController { + @IBAction func handlePressOnDoneButton(_ sender: Any) { + onCompletion?() + } + + @IBAction func handlePressOnNavigationControl(_ sender: Any) { + var newCurrent = current + if navigationControl.selectedSegment == Constants.backButtonIndex { + newCurrent -= 1 + } else { + newCurrent += 1 + } + + newCurrent = min(newCurrent, total - 1) + newCurrent = max(newCurrent, 0) + + current = newCurrent + } +} + + +// MARK: - Constants +// +private struct Constants { + static let backButtonIndex = 0 + static let forwardButtonIndex = 1 + + /// We use -1 so that we can navigate to the first match. User presses ">", we go from -1 to 0 and 0 is the index of the first match. + static let defaultCurrentValue = -1 +} + + +// MARK: - Localization +// +private struct Localization { + static let doneButton = NSLocalizedString("Done", comment: "Done button on Search Matches bar") + + static private let matchesSingular = NSLocalizedString("%1$d match", comment: "Number of matches shown on Search Matches bar when the amount is 1. Parameters: %1$d - amount") + static private let matchesPlural = NSLocalizedString("%1$d matches", comment: "Number of matches shown on Search Matches bar when the amount is not 1. Parameters: %1$d - amount") + + static func matches(with value: Int) -> String { + let template = value == 1 ? matchesSingular : matchesPlural + + return String(format: template, value) + } +} diff --git a/Simplenote/SearchMatchesBarViewController.xib b/Simplenote/SearchMatchesBarViewController.xib new file mode 100644 index 000000000..986da5f78 --- /dev/null +++ b/Simplenote/SearchMatchesBarViewController.xib @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +