From 2fa5d0d91e9575e5ca7b27f6430459912420911d Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Thu, 26 Dec 2024 13:08:52 -1000 Subject: [PATCH 1/9] Added getting text container height with tk2 for search --- .../Classes/SPNoteEditorViewController+Extensions.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift index 48fc05943..083a0a503 100644 --- a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift +++ b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift @@ -870,7 +870,13 @@ extension SPNoteEditorViewController { } private func textContainerHeightForSearchMap() -> CGFloat { - var textContainerHeight = noteEditorTextView.layoutManager.usedRect(for: noteEditorTextView.textContainer).size.height + var textContainerHeight: CGFloat = 0 + + if #available (iOS 17.0, *) { + textContainerHeight = noteEditorTextView.textLayoutManager?.usageBoundsForTextContainer.size.height ?? CGFloat.leastNormalMagnitude + } else { + textContainerHeight = noteEditorTextView.layoutManager.usedRect(for: noteEditorTextView.textContainer).size.height + } textContainerHeight = textContainerHeight + noteEditorTextView.textContainerInset.top + noteEditorTextView.textContainerInset.bottom let textContainerMinHeight = noteEditorTextView.editingRectInWindow().size.height From 373acda2e668d56bda8130130a428c3fd9873f94 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Sun, 29 Dec 2024 10:10:02 -1000 Subject: [PATCH 2/9] Add search highlightable text paragraph --- Simplenote.xcodeproj/project.pbxproj | 4 ++++ .../SearchHighlightableTextParagraph.swift | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 Simplenote/SearchHighlightableTextParagraph.swift diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index 605d06472..fb74e3f24 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -472,6 +472,7 @@ BA55B06325F068650042582B /* NoticeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA55B06225F068650042582B /* NoticeController.swift */; }; BA5768EC269BE4D0008B510E /* AccountDeletionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5768EB269BE4D0008B510E /* AccountDeletionController.swift */; }; BA5C1C0725BF9D6C006E3820 /* SPDragBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5C1C0625BF9D6C006E3820 /* SPDragBar.swift */; }; + BA5ED24C2D1F7B4C005ECF89 /* SearchHighlightableTextParagraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA5ED24B2D1F7B40005ECF89 /* SearchHighlightableTextParagraph.swift */; }; BA608EF526BB6E0200A9D94E /* ListWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA608EF426BB6E0200A9D94E /* ListWidget.swift */; }; BA608EF726BB6E7400A9D94E /* ListWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA608EF626BB6E7400A9D94E /* ListWidgetProvider.swift */; }; BA608EF926BB6F4C00A9D94E /* ListWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA608EF826BB6F4C00A9D94E /* ListWidgetView.swift */; }; @@ -1176,6 +1177,7 @@ BA55B06225F068650042582B /* NoticeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeController.swift; sourceTree = ""; }; BA5768EB269BE4D0008B510E /* AccountDeletionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletionController.swift; sourceTree = ""; }; BA5C1C0625BF9D6C006E3820 /* SPDragBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SPDragBar.swift; sourceTree = ""; }; + BA5ED24B2D1F7B40005ECF89 /* SearchHighlightableTextParagraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHighlightableTextParagraph.swift; sourceTree = ""; }; BA608EF426BB6E0200A9D94E /* ListWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidget.swift; sourceTree = ""; }; BA608EF626BB6E7400A9D94E /* ListWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgetProvider.swift; sourceTree = ""; }; BA608EF826BB6F4C00A9D94E /* ListWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgetView.swift; sourceTree = ""; }; @@ -1885,6 +1887,7 @@ B52E2B142537480A0074509A /* SPEditorTapRecognizerDelegate.swift */, BABB22E02D162E6200FCF47D /* NSTextLayoutManager+Simplenote.swift */, BABB22E22D162E7D00FCF47D /* NSTextLayoutFragment.swift */, + BA5ED24B2D1F7B40005ECF89 /* SearchHighlightableTextParagraph.swift */, ); name = Editor; sourceTree = ""; @@ -3674,6 +3677,7 @@ B5C2EDF0255AFB6C00C09B32 /* PassthruView.swift in Sources */, A6F4882325A8889E0050CFA8 /* UITextField+Tag.swift in Sources */, A64DE6F1255D1CD9001D0526 /* NoteContentHelper.swift in Sources */, + BA5ED24C2D1F7B4C005ECF89 /* SearchHighlightableTextParagraph.swift in Sources */, A628BEB625ECD97900121B64 /* SignupVerificationViewController.swift in Sources */, B550F93322BA6A3300091939 /* ShortcutsHandler.swift in Sources */, A6C0589424AD2B8F006BC572 /* SPNoteHistoryViewController.swift in Sources */, diff --git a/Simplenote/SearchHighlightableTextParagraph.swift b/Simplenote/SearchHighlightableTextParagraph.swift new file mode 100644 index 000000000..66689469c --- /dev/null +++ b/Simplenote/SearchHighlightableTextParagraph.swift @@ -0,0 +1,22 @@ +import Foundation + +class SearchHighlightableTextParagraph: NSTextParagraph { + + init(attributedString: NSAttributedString, searchText: String?) { + guard let searchText, + attributedString.string.contains(searchText), + let mutableString = attributedString.mutableCopy() as? NSMutableAttributedString else { + super.init(attributedString: attributedString) + return + } + + let range = attributedString.string.nsString.range(of: searchText) + mutableString.addAttributes([ + .backgroundColor: UIColor.simplenoteEditorSearchHighlightSelectedColor, + .foregroundColor: UIColor.simplenoteEditorSearchHighlightTextColor + ], range: range) + + + super.init(attributedString: mutableString) + } +} From 1089664832184576a56d193cb4b8a1da55591013 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Sun, 29 Dec 2024 10:12:48 -1000 Subject: [PATCH 3/9] Moved text content storage delegate to editor VC --- Simplenote/SPTextView.swift | 64 ------------------------------------- 1 file changed, 64 deletions(-) diff --git a/Simplenote/SPTextView.swift b/Simplenote/SPTextView.swift index 70f3d01c2..92796acd3 100644 --- a/Simplenote/SPTextView.swift +++ b/Simplenote/SPTextView.swift @@ -16,7 +16,6 @@ extension SPTextView { if #available(iOS 16.0, *) { let textLayoutManager = NSTextLayoutManager() let contentStorage = NSTextContentStorage() - contentStorage.delegate = self contentStorage.addTextLayoutManager(textLayoutManager) textLayoutManager.textContainer = container @@ -29,66 +28,3 @@ extension SPTextView { return container } } - -// MARK: NSTextContentStorageDelegate -// -extension SPTextView: NSTextContentStorageDelegate { - public func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { - guard let originalText = textContentStorage.textStorage?.attributedSubstring(from: range).mutableCopy() as? NSMutableAttributedString else { - return nil - } - - let style = textInRangeIsHeader(range) ? headlineStyle : defaultStyle - originalText.addAttributes(style, range: originalText.fullRange) - - return NSTextParagraph(attributedString: originalText) - } - - func textInRangeIsHeader(_ range: NSRange) -> Bool { - range.location == .zero - } - - // MARK: Styles - // - var headlineFont: UIFont { - UIFont.preferredFont(for: .title1, weight: .bold) - } - - var defaultFont: UIFont { - UIFont.preferredFont(forTextStyle: .body) - } - - var defaultTextColor: UIColor { - UIColor.simplenoteNoteHeadlineColor - } - - var lineSpacing: CGFloat { - defaultFont.lineHeight * Metrics.lineSpacingMultipler - } - - var defaultStyle: [NSAttributedString.Key: Any] { - [ - .font: defaultFont, - .foregroundColor: defaultTextColor, - .paragraphStyle: NSMutableParagraphStyle(lineSpacing: lineSpacing) - ] - } - - var headlineStyle: [NSAttributedString.Key: Any] { - [ - .font: headlineFont, - .foregroundColor: defaultTextColor, - ] - } -} - -// MARK: - Metrics -// -private enum Metrics { - static let lineSpacingMultiplerPad: CGFloat = 0.40 - static let lineSpacingMultiplerPhone: CGFloat = 0.20 - - static var lineSpacingMultipler: CGFloat { - UIDevice.isPad ? lineSpacingMultiplerPad : lineSpacingMultiplerPhone - } -} From 97c698a5c77d1e08d4c951df68d57ee502f11159 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Sun, 29 Dec 2024 10:13:36 -1000 Subject: [PATCH 4/9] Updated sp note editor vc to create set its self as content delegate --- ...PNoteEditorViewController+Extensions.swift | 96 +++++++++++++++++++ .../Classes/SPNoteEditorViewController.h | 1 + .../Classes/SPNoteEditorViewController.m | 10 +- 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift index 083a0a503..76ace4240 100644 --- a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift +++ b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift @@ -1065,6 +1065,102 @@ private enum Metrics { static let additionalTagViewAndEditorCollisionDistance: CGFloat = 16.0 } +// MARK: - TextKit 2 +// +extension SPNoteEditorViewController { + @objc + func makeTextView() -> SPEditorTextView { + let textStorage = SPInteractiveTextStorage() + let textContainer = setupTextContainer(with: textStorage) + + return SPEditorTextView(frame: .zero, textContainer: textContainer) + } + + @objc + func setupTextContainer(with textStorage: SPInteractiveTextStorage) -> NSTextContainer { + let container = NSTextContainer(size: .zero) + container.widthTracksTextView = true + container.heightTracksTextView = true + + if #available(iOS 16.0, *) { + let textLayoutManager = NSTextLayoutManager() + let contentStorage = NSTextContentStorage() + contentStorage.delegate = self + contentStorage.addTextLayoutManager(textLayoutManager) + textLayoutManager.textContainer = container + + } else { + let layoutManager = NSLayoutManager() + layoutManager.addTextContainer(container) + textStorage.addLayoutManager(layoutManager) + } + + return container + } + + +} + +// MARK: NSTextContentStorageDelegate +// +extension SPNoteEditorViewController: NSTextContentStorageDelegate { + public func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { + guard let originalText = textContentStorage.textStorage?.attributedSubstring(from: range).mutableCopy() as? NSMutableAttributedString else { + return nil + } + + let style = textInRangeIsHeader(range) ? headlineStyle : defaultStyle + originalText.addAttributes(style, range: originalText.fullRange) + + /*if (!self.searching || !self.searchQuery || self.searchQuery.isEmpty || self.searchResultRanges)*/ + + guard searching, + let searchQuery = searchQueryText(), + searchQuery.isEmpty == false else { + return NSTextParagraph(attributedString: originalText) + } + + return SearchHighlightableTextParagraph(attributedString: originalText, searchText: searchQuery) + } + + func textInRangeIsHeader(_ range: NSRange) -> Bool { + range.location == .zero + } + + // MARK: Styles + // + var headlineFont: UIFont { + UIFont.preferredFont(for: .title1, weight: .bold) + } + + var defaultFont: UIFont { + UIFont.preferredFont(forTextStyle: .body) + } + + var defaultTextColor: UIColor { + UIColor.simplenoteNoteHeadlineColor + } + + var lineSpacing: CGFloat { + defaultFont.lineHeight * Metrics.lineSpacingMultipler + } + + var defaultStyle: [NSAttributedString.Key: Any] { + [ + .font: defaultFont, + .foregroundColor: defaultTextColor, + .paragraphStyle: NSMutableParagraphStyle(lineSpacing: lineSpacing) + ] + } + + var headlineStyle: [NSAttributedString.Key: Any] { + [ + .font: headlineFont, + .foregroundColor: defaultTextColor, + ] + } +} + // MARK: - Localization // private enum Localization { diff --git a/Simplenote/Classes/SPNoteEditorViewController.h b/Simplenote/Classes/SPNoteEditorViewController.h index 1de296fa0..618928f03 100644 --- a/Simplenote/Classes/SPNoteEditorViewController.h +++ b/Simplenote/Classes/SPNoteEditorViewController.h @@ -78,6 +78,7 @@ NS_ASSUME_NONNULL_BEGIN // TODO: We can't use `SearchQuery` as a type here because it doesn't work from swift code (because of SPM) :-( - (void)updateWithSearchQuery:(id _Nullable )query; +- (nullable NSString *)searchQueryText; @end diff --git a/Simplenote/Classes/SPNoteEditorViewController.m b/Simplenote/Classes/SPNoteEditorViewController.m index 57c4f9ab0..feb5d4d31 100644 --- a/Simplenote/Classes/SPNoteEditorViewController.m +++ b/Simplenote/Classes/SPNoteEditorViewController.m @@ -77,7 +77,7 @@ - (instancetype _Nonnull)initWithNote:(Note * _Nonnull)note { - (void)configureTextView { - _noteEditorTextView = [[SPEditorTextView alloc] init]; + _noteEditorTextView = [self makeTextView]; _noteEditorTextView.delegate = self; _noteEditorTextView.dataDetectorTypes = UIDataDetectorTypeAll; _noteEditorTextView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody]; @@ -541,6 +541,14 @@ - (void)updateWithSearchQuery:(SearchQuery *)searchQuery [self.navigationController setToolbarHidden:NO animated:YES]; } +- (NSString *)searchQueryText +{ + if (!_searchQuery) { + return nil; + } + return _searchQuery.searchText; +} + - (void)highlightNextSearchResult { [self highlightSearchResultAtIndex:(self.highlightedSearchResultIndex + 1) animated:YES]; From bba6f08c82aa146745d7c88a5ad232f434da7a01 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Sun, 29 Dec 2024 10:44:13 -1000 Subject: [PATCH 5/9] Fixed highlighting to work case unsensitive --- Simplenote/SearchHighlightableTextParagraph.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Simplenote/SearchHighlightableTextParagraph.swift b/Simplenote/SearchHighlightableTextParagraph.swift index 66689469c..7b4bc1817 100644 --- a/Simplenote/SearchHighlightableTextParagraph.swift +++ b/Simplenote/SearchHighlightableTextParagraph.swift @@ -2,21 +2,21 @@ import Foundation class SearchHighlightableTextParagraph: NSTextParagraph { - init(attributedString: NSAttributedString, searchText: String?) { - guard let searchText, - attributedString.string.contains(searchText), + init(attributedString: NSAttributedString, searchText: String?, isSelected: Bool) { + let plainText = attributedString.string.lowercased() + guard let searchText = searchText?.lowercased(), + plainText.contains(searchText), let mutableString = attributedString.mutableCopy() as? NSMutableAttributedString else { super.init(attributedString: attributedString) return } - let range = attributedString.string.nsString.range(of: searchText) + let range = plainText.nsString.range(of: searchText) mutableString.addAttributes([ .backgroundColor: UIColor.simplenoteEditorSearchHighlightSelectedColor, .foregroundColor: UIColor.simplenoteEditorSearchHighlightTextColor ], range: range) - super.init(attributedString: mutableString) } } From 780ef01cd5dc716b49c7a5540b57da712ec83349 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Sun, 29 Dec 2024 11:36:08 -1000 Subject: [PATCH 6/9] Show search map with textkit 2 --- .../Classes/UITextView+Simplenote.swift | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Simplenote/Classes/UITextView+Simplenote.swift b/Simplenote/Classes/UITextView+Simplenote.swift index ba65e2998..d4cd9562f 100644 --- a/Simplenote/Classes/UITextView+Simplenote.swift +++ b/Simplenote/Classes/UITextView+Simplenote.swift @@ -134,8 +134,24 @@ extension UITextView { /// Returns the Bounding Rect for the specified NSRange /// func boundingRect(for range: NSRange) -> CGRect { - let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) - let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + var rect: CGRect = .zero + if #available(iOS 17.0, *) { + guard let textLayoutManager, + let contentManager = textLayoutManager.textContentManager, + let startLocation = contentManager.location(contentManager.documentRange.location, + offsetBy: range.location) else { + return .zero + } + + textLayoutManager.enumerateTextLayoutFragments(from: startLocation, using: { fragment in + // We want the frame of the first layout fragment at the given location, so we can return false + rect = fragment.layoutFragmentFrame + return false + }) + } else { + let glyphRange = layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) + rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) + } return rect.offsetBy(dx: textContainerInset.left, dy: textContainerInset.top) } From 9d7a03a5bb8abae128bc0852e92c1ebf18914e2d Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Sun, 29 Dec 2024 11:55:16 -1000 Subject: [PATCH 7/9] Add selection highlighting with text kit 2 --- ...PNoteEditorViewController+Extensions.swift | 37 ++++++++++++++++--- .../Classes/SPNoteEditorViewController.h | 3 ++ .../Classes/SPNoteEditorViewController.m | 4 +- .../SearchHighlightableTextParagraph.swift | 4 +- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift index 76ace4240..cfd2cdd98 100644 --- a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift +++ b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift @@ -1098,7 +1098,26 @@ extension SPNoteEditorViewController { return container } - + @objc + func highlight(range: NSRange) { + + if #available(iOS 17.0, *) { + let textContentManager = noteEditorTextView.textLayoutManager!.textContentManager! + + let startLocation = textContentManager.location(textContentManager.documentRange.location, + offsetBy: range.location)! + + let endLocation = textContentManager.location(startLocation, + offsetBy: range.length) + + let nsTextRange = NSTextRange(location: startLocation, end: endLocation)! + noteEditorTextView.textLayoutManager?.invalidateLayout(for: nsTextRange) + } else { + noteEditorTextView.highlight(range, animated: true) { highlightFrame in + self.noteEditorTextView.scrollRectToVisible(highlightFrame, animated: true) + } + } + } } // MARK: NSTextContentStorageDelegate @@ -1111,22 +1130,30 @@ extension SPNoteEditorViewController: NSTextContentStorageDelegate { let style = textInRangeIsHeader(range) ? headlineStyle : defaultStyle originalText.addAttributes(style, range: originalText.fullRange) - - /*if (!self.searching || !self.searchQuery || self.searchQuery.isEmpty || self.searchResultRanges)*/ guard searching, let searchQuery = searchQueryText(), - searchQuery.isEmpty == false else { + searchQuery.isEmpty == false, + let searchResultRanges else { return NSTextParagraph(attributedString: originalText) } - return SearchHighlightableTextParagraph(attributedString: originalText, searchText: searchQuery) + return SearchHighlightableTextParagraph(attributedString: originalText, searchText: searchQuery, isSelected: rangeIsSelected(range)) } func textInRangeIsHeader(_ range: NSRange) -> Bool { range.location == .zero } + func rangeIsSelected(_ range: NSRange) -> Bool { + guard let searchResultRanges, + let selected = searchResultRanges[highlightedSearchResultIndex] as? NSRange else { + return false + } + + return NSIntersectionRange(range, selected).length > .zero + } + // MARK: Styles // var headlineFont: UIFont { diff --git a/Simplenote/Classes/SPNoteEditorViewController.h b/Simplenote/Classes/SPNoteEditorViewController.h index 618928f03..170958db9 100644 --- a/Simplenote/Classes/SPNoteEditorViewController.h +++ b/Simplenote/Classes/SPNoteEditorViewController.h @@ -53,6 +53,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL searching; @property (nonatomic, assign) NSInteger highlightedSearchResultIndex; +// Search +@property (nonatomic, strong, nullable) NSArray *searchResultRanges; + @property (nonatomic, strong) NoteScrollPositionCache *scrollPositionCache; - (instancetype)initWithNote:(Note *)note; diff --git a/Simplenote/Classes/SPNoteEditorViewController.m b/Simplenote/Classes/SPNoteEditorViewController.m index feb5d4d31..1cd42defa 100644 --- a/Simplenote/Classes/SPNoteEditorViewController.m +++ b/Simplenote/Classes/SPNoteEditorViewController.m @@ -576,9 +576,7 @@ - (void)highlightSearchResultAtIndex:(NSInteger)index animated:(BOOL)animated self.nextSearchButton.enabled = index < searchResultCount - 1; NSRange targetRange = [(NSValue *)self.searchResultRanges[index] rangeValue]; - [_noteEditorTextView highlightRange:targetRange animated:YES withBlock:^(CGRect highlightFrame) { - [self.noteEditorTextView scrollRectToVisible:highlightFrame animated:animated]; - }]; + [self highlightWithRange:targetRange]; } - (void)endSearching:(id)sender { diff --git a/Simplenote/SearchHighlightableTextParagraph.swift b/Simplenote/SearchHighlightableTextParagraph.swift index 7b4bc1817..f6ce45974 100644 --- a/Simplenote/SearchHighlightableTextParagraph.swift +++ b/Simplenote/SearchHighlightableTextParagraph.swift @@ -13,7 +13,9 @@ class SearchHighlightableTextParagraph: NSTextParagraph { let range = plainText.nsString.range(of: searchText) mutableString.addAttributes([ - .backgroundColor: UIColor.simplenoteEditorSearchHighlightSelectedColor, + .backgroundColor: isSelected ? + UIColor.simplenoteEditorSearchHighlightSelectedColor: + UIColor.simplenoteEditorSearchHighlightColor, .foregroundColor: UIColor.simplenoteEditorSearchHighlightTextColor ], range: range) From 59128ebdb26aa8dec3b9ba73e8ff2bc2a5af59ca Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 3 Jan 2025 11:16:00 -1000 Subject: [PATCH 8/9] Added convenience method to get range in document --- Simplenote.xcodeproj/project.pbxproj | 4 ++++ Simplenote/NSTextContentManager.swift | 13 +++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 Simplenote/NSTextContentManager.swift diff --git a/Simplenote.xcodeproj/project.pbxproj b/Simplenote.xcodeproj/project.pbxproj index fb74e3f24..294f04ff0 100644 --- a/Simplenote.xcodeproj/project.pbxproj +++ b/Simplenote.xcodeproj/project.pbxproj @@ -521,6 +521,7 @@ BAA63C3325EEDA83001589D7 /* NoteLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAA63C3225EEDA83001589D7 /* NoteLinkTests.swift */; }; BAADC8A426C634DB004CAAA9 /* WidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAADC8A326C634DB004CAAA9 /* WidgetConstants.swift */; }; BAADC8A526C634DB004CAAA9 /* WidgetConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAADC8A326C634DB004CAAA9 /* WidgetConstants.swift */; }; + BAAE76BB2D21FCD800D04273 /* NSTextContentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAAE76BA2D21FCD200D04273 /* NSTextContentManager.swift */; }; BAB017722609456D007A9CC3 /* PublishController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB017712609456D007A9CC3 /* PublishController.swift */; }; BAB01792260AAE93007A9CC3 /* NoticeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB01791260AAE93007A9CC3 /* NoticeFactory.swift */; }; BAB0179B260AD591007A9CC3 /* PublishControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB0179A260AD591007A9CC3 /* PublishControllerTests.swift */; }; @@ -1231,6 +1232,7 @@ BAA59E78269F9FE30068BD3D /* Date+Simplenote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Simplenote.swift"; sourceTree = ""; }; BAA63C3225EEDA83001589D7 /* NoteLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteLinkTests.swift; sourceTree = ""; }; BAADC8A326C634DB004CAAA9 /* WidgetConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetConstants.swift; sourceTree = ""; }; + BAAE76BA2D21FCD200D04273 /* NSTextContentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSTextContentManager.swift; sourceTree = ""; }; BAB017712609456D007A9CC3 /* PublishController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishController.swift; sourceTree = ""; }; BAB01791260AAE93007A9CC3 /* NoticeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeFactory.swift; sourceTree = ""; }; BAB0179A260AD591007A9CC3 /* PublishControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishControllerTests.swift; sourceTree = ""; }; @@ -1887,6 +1889,7 @@ B52E2B142537480A0074509A /* SPEditorTapRecognizerDelegate.swift */, BABB22E02D162E6200FCF47D /* NSTextLayoutManager+Simplenote.swift */, BABB22E22D162E7D00FCF47D /* NSTextLayoutFragment.swift */, + BAAE76BA2D21FCD200D04273 /* NSTextContentManager.swift */, BA5ED24B2D1F7B40005ECF89 /* SearchHighlightableTextParagraph.swift */, ); name = Editor; @@ -3588,6 +3591,7 @@ 46A3C98617DFA81A002865AE /* SPEntryListViewController.m in Sources */, A6ABB689256D95EB00E2A076 /* PinLockProgressView.swift in Sources */, B56A696322F9D53400B90398 /* SPAuthViewController.swift in Sources */, + BAAE76BB2D21FCD800D04273 /* NSTextContentManager.swift in Sources */, B50789FE1C1F5517009F097A /* SPInteractivePushPopAnimationController.m in Sources */, B5D3FCD0201F96AC00A813B7 /* StatusChecker.m in Sources */, B57DE27825013C6600B4D435 /* Simperium+Simplenote.swift in Sources */, diff --git a/Simplenote/NSTextContentManager.swift b/Simplenote/NSTextContentManager.swift new file mode 100644 index 000000000..f032567fb --- /dev/null +++ b/Simplenote/NSTextContentManager.swift @@ -0,0 +1,13 @@ +import Foundation + +extension NSTextContentManager { + func textRangeInDocument(for range: NSRange) -> NSTextRange? { + guard let startLocation = location(documentRange.location, offsetBy: range.location) else { + return nil + } + + let endLocation = location(startLocation, offsetBy: range.length) + + return NSTextRange(location: startLocation, end: endLocation) + } +} From bda53d61221834b20b87443584040236f4000676 Mon Sep 17 00:00:00 2001 From: Charlie Scheer Date: Fri, 3 Jan 2025 11:16:55 -1000 Subject: [PATCH 9/9] Added nsTextLayoutDelegate to note editor --- ...PNoteEditorViewController+Extensions.swift | 28 +++++++++++++------ .../Classes/SPNoteEditorViewController.m | 3 +- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift index cfd2cdd98..e8a62727b 100644 --- a/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift +++ b/Simplenote/Classes/SPNoteEditorViewController+Extensions.swift @@ -1086,6 +1086,7 @@ extension SPNoteEditorViewController { let textLayoutManager = NSTextLayoutManager() let contentStorage = NSTextContentStorage() contentStorage.delegate = self + textLayoutManager.delegate = self contentStorage.addTextLayoutManager(textLayoutManager) textLayoutManager.textContainer = container @@ -1100,18 +1101,22 @@ extension SPNoteEditorViewController { @objc func highlight(range: NSRange) { - if #available(iOS 17.0, *) { - let textContentManager = noteEditorTextView.textLayoutManager!.textContentManager! + guard let textLayoutManager = noteEditorTextView.textLayoutManager, + let nsTextRange = textLayoutManager.textContentManager?.textRangeInDocument(for: range) else { + return + } + + textLayoutManager.replaceContents(in: nsTextRange, with: NSAttributedString(string: "This is a string")) - let startLocation = textContentManager.location(textContentManager.documentRange.location, - offsetBy: range.location)! + // textLayoutManager.invalidateLayout(for: nsTextRange) - let endLocation = textContentManager.location(startLocation, - offsetBy: range.length) + // textLayoutManager.ensureLayout(for: nsTextRange) - let nsTextRange = NSTextRange(location: startLocation, end: endLocation)! - noteEditorTextView.textLayoutManager?.invalidateLayout(for: nsTextRange) + // textLayoutManager.textContentManager?.performEditingTransaction({ + // let newString = NSAttributedString(string: "new string") + // (textLayoutManager.textContentManager as! NSTextContentStorage).textStorage!.insert(newString, at: 0) + // }) } else { noteEditorTextView.highlight(range, animated: true) { highlightFrame in self.noteEditorTextView.scrollRectToVisible(highlightFrame, animated: true) @@ -1122,6 +1127,13 @@ extension SPNoteEditorViewController { // MARK: NSTextContentStorageDelegate // + +extension SPNoteEditorViewController: NSTextLayoutManagerDelegate { + public func textLayoutManager(_ textLayoutManager: NSTextLayoutManager, textLayoutFragmentFor location: any NSTextLocation, in textElement: NSTextElement) -> NSTextLayoutFragment { + NSTextLayoutFragment(textElement: textElement, range: textElement.elementRange) + } +} + extension SPNoteEditorViewController: NSTextContentStorageDelegate { public func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? { guard let originalText = textContentStorage.textStorage?.attributedSubstring(from: range).mutableCopy() as? NSMutableAttributedString else { diff --git a/Simplenote/Classes/SPNoteEditorViewController.m b/Simplenote/Classes/SPNoteEditorViewController.m index 1cd42defa..8804ed527 100644 --- a/Simplenote/Classes/SPNoteEditorViewController.m +++ b/Simplenote/Classes/SPNoteEditorViewController.m @@ -55,7 +55,6 @@ @interface SPNoteEditorViewController ()