diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecContentChangeWatcher.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecContentChangeWatcher.kt new file mode 100644 index 000000000..07a9864ff --- /dev/null +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecContentChangeWatcher.kt @@ -0,0 +1,33 @@ +package org.wordpress.aztec + +import java.lang.ref.WeakReference + +class AztecContentChangeWatcher { + private val observers = mutableListOf>() + fun registerObserver(observer: AztecTextChangeObserver) { + if (observers.none { it.get() == observer }) { + observers.add(WeakReference(observer)) + } + } + + fun unregisterObserver(observer: AztecTextChangeObserver) { + observers.removeAll { it.get() == observer } + } + + internal fun notifyContentChanged() { + val iterator = observers.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + val foundObserver = item.get() + if (foundObserver == null) { + iterator.remove() + } else { + foundObserver.onContentChanged() + } + } + } + + interface AztecTextChangeObserver { + fun onContentChanged() + } +} diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt index 8671b1aab..99f7e9e0e 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt @@ -293,6 +293,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown private var focusOnVisible = true + val contentChangeWatcher = AztecContentChangeWatcher() + interface OnSelectionChangedListener { fun onSelectionChanged(selStart: Int, selEnd: Int) } @@ -497,6 +499,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown // use HTML from the new text to set the state of the editText directly fromHtml(toFormattedHtml(newText), false) + contentChangeWatcher.notifyContentChanged() + // re-enable MediaDeleted listener enableMediaDeletedListener() // re-enable this very filter @@ -565,6 +569,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown setText("") enableTextChangedListener() } + contentChangeWatcher.notifyContentChanged() } return wasStyleRemoved } @@ -627,6 +632,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown } override fun afterTextChanged(text: Editable) { + contentChangeWatcher.notifyContentChanged() if (isTextChangedListenerDisabled()) { return } @@ -1024,6 +1030,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown .forEach { it.toggle() } } } + + contentChangeWatcher.notifyContentChanged() } fun contains(format: ITextFormat, selStart: Int = selectionStart, selEnd: Int = selectionEnd): Boolean { @@ -1128,10 +1136,12 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown fun redo() { history.redo(this) + contentChangeWatcher.notifyContentChanged() } fun undo() { history.undo(this) + contentChangeWatcher.notifyContentChanged() } // Helper ====================================================================================== @@ -1618,6 +1628,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown fromHtml(newHtml, false) inlineFormatter.joinStyleSpans(0, length()) } + contentChangeWatcher.notifyContentChanged() } } @@ -1643,6 +1654,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown } else { linkFormatter.addLink(url, anchor, openInNewWindow, selectionStart, selectionEnd) } + contentChangeWatcher.notifyContentChanged() } fun removeLink() { diff --git a/aztec/src/test/kotlin/org/wordpress/aztec/AztecContentChangeWatcherTest.kt b/aztec/src/test/kotlin/org/wordpress/aztec/AztecContentChangeWatcherTest.kt new file mode 100644 index 000000000..3313879c9 --- /dev/null +++ b/aztec/src/test/kotlin/org/wordpress/aztec/AztecContentChangeWatcherTest.kt @@ -0,0 +1,74 @@ +package org.wordpress.aztec + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class AztecContentChangeWatcherTest { + private lateinit var aztecContentChangeWatcher: AztecContentChangeWatcher + @Before + fun setUp() { + aztecContentChangeWatcher = AztecContentChangeWatcher() + } + + @Test + fun `notifies registered observer`() { + // Given + var contentChanged = false + setupRegisteredObserver { + contentChanged = true + } + + // When + aztecContentChangeWatcher.notifyContentChanged() + + // Then + assertTrue(contentChanged) + } + + @Test + fun `does not notify unregistered observer`() { + // Given + var contentChanged = false + val observer = setupRegisteredObserver { + contentChanged = true + } + + // When + aztecContentChangeWatcher.unregisterObserver(observer) + aztecContentChangeWatcher.notifyContentChanged() + + // Then + assertFalse(contentChanged) + } + + @Test + fun `observer is garbage collected and reference is lost`() { + // Given + var contentChanged = false + setupRegisteredObserver { + contentChanged = true + } + System.gc() + + // When + aztecContentChangeWatcher.notifyContentChanged() + + // Then + assertFalse(contentChanged) + } + + private fun setupRegisteredObserver(onContentChanged: () -> Unit): AztecContentChangeWatcher.AztecTextChangeObserver { + val observer = object : AztecContentChangeWatcher.AztecTextChangeObserver { + override fun onContentChanged() { + onContentChanged() + } + } + aztecContentChangeWatcher.registerObserver(observer) + return observer + } +}