From 59295ed1478ca9d1583717487d1b7383d2c2d524 Mon Sep 17 00:00:00 2001 From: Eugene Dymov Date: Fri, 29 Nov 2024 16:16:40 +1100 Subject: [PATCH] Extend TextProcessing with receive/lose focus callbacks These new methods are called when EditorView instance receive or lose focus, including all nested editors in the hierarchy --- Proton/Sources/Swift/Editor/EditorView.swift | 4 +++ .../Swift/TextProcessors/TextProcessing.swift | 12 +++++++ .../Editor/EditorViewDelegateTests.swift | 32 +++++++++++++++++++ .../Mocks/MockTextProcessor.swift | 10 ++++++ .../TextProcessors/TextProcessorTests.swift | 22 +++++++++++++ 5 files changed, 80 insertions(+) diff --git a/Proton/Sources/Swift/Editor/EditorView.swift b/Proton/Sources/Swift/Editor/EditorView.swift index 9cb44e8e..c8fc3dca 100644 --- a/Proton/Sources/Swift/Editor/EditorView.swift +++ b/Proton/Sources/Swift/Editor/EditorView.swift @@ -1451,10 +1451,14 @@ extension EditorView: RichTextViewDelegate { } func richTextView(_ richTextView: RichTextView, didReceiveFocusAt range: NSRange) { + let executableProcessors = textProcessor?.filteringExecutableOn(editor: self) ?? [] + executableProcessors.forEach { $0.didReceiveFocus(editor: self) } AggregateEditorViewDelegate.editor(self, didReceiveFocusAt: range) } func richTextView(_ richTextView: RichTextView, didLoseFocusFrom range: NSRange) { + let executableProcessors = textProcessor?.filteringExecutableOn(editor: self) ?? [] + executableProcessors.forEach { $0.didLoseFocus(editor: self) } AggregateEditorViewDelegate.editor(self, didLoseFocusFrom: range) } diff --git a/Proton/Sources/Swift/TextProcessors/TextProcessing.swift b/Proton/Sources/Swift/TextProcessors/TextProcessing.swift index 2452ebe9..9544d9cf 100644 --- a/Proton/Sources/Swift/TextProcessors/TextProcessing.swift +++ b/Proton/Sources/Swift/TextProcessors/TextProcessing.swift @@ -104,6 +104,16 @@ public protocol TextProcessing { /// - newRange: Current range after the change func selectedRangeChanged(editor: EditorView, oldRange: NSRange?, newRange: NSRange?) + /// Notifies the processor when `editor` receives focus. + /// - Note: This function is also called when focus moves between `EditorView`'s nested editors. + /// - Parameter editor: `EditorView` which received focus + func didReceiveFocus(editor: EditorView) + + /// Notifies the processor when `editor` loses focus. + /// - Note: This function is also called when focus moves between `EditorView`'s nested editors. + /// - Parameter editor: `EditorView` which lost focus + func didLoseFocus(editor: EditorView) + /// Invoked after the text has been processed in the `Editor`. /// - Parameter editor: EditorView in which text is changed. func didProcess(editor: EditorView) @@ -134,4 +144,6 @@ public extension TextProcessing { func shouldProcess(_ editorView: EditorView, shouldProcessTextIn range: NSRange, replacementText text: String) -> Bool { return true } func willProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) { } func didProcessEditing(editor: EditorView, editedMask: NSTextStorage.EditActions, range editedRange: NSRange, changeInLength delta: Int) { } + func didReceiveFocus(editor: EditorView) { } + func didLoseFocus(editor: EditorView) { } } diff --git a/Proton/Tests/Editor/EditorViewDelegateTests.swift b/Proton/Tests/Editor/EditorViewDelegateTests.swift index 0ed821eb..cb6c41d3 100644 --- a/Proton/Tests/Editor/EditorViewDelegateTests.swift +++ b/Proton/Tests/Editor/EditorViewDelegateTests.swift @@ -133,6 +133,38 @@ class EditorViewDelegateTests: XCTestCase { try assertKeyPress(.tab, replacementText: "\t") } + func testNotifiesTextProcessorsOnDidReceiveFocus() { + let expectation = functionExpectation() + let mockProcessor = MockTextProcessor() + mockProcessor.onDidReceiveFocus = { _ in + expectation.fulfill() + } + let editor = EditorView() + editor.textProcessor?.register(mockProcessor) + let richTextView = editor.richTextView + let richTextViewDelegate = richTextView.richTextViewDelegate + + richTextViewDelegate?.richTextView(richTextView, didReceiveFocusAt: .zero) + + waitForExpectations(timeout: 1.0) + } + + func testNotifiesTextProcessorsOnDidLoseFocus() { + let expectation = functionExpectation() + let mockProcessor = MockTextProcessor() + mockProcessor.onDidLoseFocus = { _ in + expectation.fulfill() + } + let editor = EditorView() + editor.textProcessor?.register(mockProcessor) + let richTextView = editor.richTextView + let richTextViewDelegate = richTextView.richTextViewDelegate + + richTextViewDelegate?.richTextView(richTextView, didLoseFocusFrom: .zero) + + waitForExpectations(timeout: 1.0) + } + private func assertKeyPress(_ key: EditorKey, replacementText: String, file: StaticString = #file, line: UInt = #line) throws { let delegateExpectation = functionExpectation() let delegate = MockEditorViewDelegate() diff --git a/Proton/Tests/TextProcessors/Mocks/MockTextProcessor.swift b/Proton/Tests/TextProcessors/Mocks/MockTextProcessor.swift index 46446e79..b1cdd231 100644 --- a/Proton/Tests/TextProcessors/Mocks/MockTextProcessor.swift +++ b/Proton/Tests/TextProcessors/Mocks/MockTextProcessor.swift @@ -33,6 +33,8 @@ class MockTextProcessor: TextProcessing { var onKeyWithModifier: ((EditorView, EditorKey, UIKeyModifierFlags, NSRange) -> Void)? var onProcessInterrupted: ((EditorView, NSRange) -> Void)? var onSelectedRangeChanged: ((EditorView, NSRange?, NSRange?) -> Void)? + var onDidReceiveFocus: ((EditorView) -> Void)? + var onDidLoseFocus: ((EditorView) -> Void)? var onDidProcess: ((EditorView) -> Void)? var onShouldProcess: ((EditorView, NSRange, String) -> Bool)? @@ -72,6 +74,14 @@ class MockTextProcessor: TextProcessing { onSelectedRangeChanged?(editor, oldRange, newRange) } + func didReceiveFocus(editor: EditorView) { + onDidReceiveFocus?(editor) + } + + func didLoseFocus(editor: EditorView) { + onDidLoseFocus?(editor) + } + func didProcess(editor: EditorView) { onDidProcess?(editor) } diff --git a/Proton/Tests/TextProcessors/TextProcessorTests.swift b/Proton/Tests/TextProcessors/TextProcessorTests.swift index 881457b3..1eee2c18 100644 --- a/Proton/Tests/TextProcessors/TextProcessorTests.swift +++ b/Proton/Tests/TextProcessors/TextProcessorTests.swift @@ -138,6 +138,28 @@ class TextProcessorTests: XCTestCase { waitForExpectations(timeout: 1.0) } + func testPreventsDidReceiveFocusOnSetAttributedText() throws { + let expectation = expectation(description: "Should not invoke didReceiveFocus") + expectation.isInverted = true + try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: false) { mockProcessor in + mockProcessor.onDidReceiveFocus = { _ in + expectation.fulfill() + } + } + waitForExpectations(timeout: 1.0) + } + + func testPreventsDidLoseFocusOnSetAttributedText() throws { + let expectation = expectation(description: "Should not invoke didLoseFocus") + expectation.isInverted = true + try assertProcessorInvocationOnSetAttributedText(expectation, isRunOnSettingText: false) { mockProcessor in + mockProcessor.onDidLoseFocus = { _ in + expectation.fulfill() + } + } + waitForExpectations(timeout: 1.0) + } + func testInvokesTextProcessor() { let testExpectation = functionExpectation() let editor = EditorView()