Skip to content

Commit 1209e75

Browse files
authored
Merge pull request #584 from wordpress-mobile/issue/555-buffered-action-detection
Issue/555 buffered action detection
2 parents 13f8cc5 + 6ec86aa commit 1209e75

17 files changed

+467
-13
lines changed

aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,20 @@ import org.wordpress.aztec.watchers.ParagraphCollapseRemover
9898
import org.wordpress.aztec.watchers.SuggestionWatcher
9999
import org.wordpress.aztec.watchers.TextDeleter
100100
import org.wordpress.aztec.watchers.ZeroIndexContentWatcher
101+
import org.wordpress.aztec.watchers.event.IEventInjector
102+
import org.wordpress.aztec.watchers.event.sequence.ObservationQueue
103+
import org.wordpress.aztec.watchers.event.sequence.known.space.steps.TextWatcherEventInsertText
104+
import org.wordpress.aztec.watchers.event.text.AfterTextChangedEventData
105+
import org.wordpress.aztec.watchers.event.text.BeforeTextChangedEventData
106+
import org.wordpress.aztec.watchers.event.text.OnTextChangedEventData
107+
import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
101108
import org.xml.sax.Attributes
102109
import java.util.ArrayList
103110
import java.util.Arrays
104111
import java.util.LinkedList
105112

106113
@Suppress("UNUSED_PARAMETER")
107-
class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlTappedListener {
114+
class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlTappedListener, IEventInjector {
108115
companion object {
109116
val BLOCK_EDITOR_HTML_KEY = "RETAINED_BLOCK_HTML_KEY"
110117
val BLOCK_EDITOR_START_INDEX_KEY = "BLOCK_EDITOR_START_INDEX_KEY"
@@ -155,6 +162,7 @@ class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlT
155162
private var consumeSelectionChangedEvent: Boolean = false
156163
private var consumeHistoryEvent: Boolean = false
157164
private var isInlineTextHandlerEnabled: Boolean = true
165+
private var bypassObservationQueue: Boolean = false
158166

159167
private var onSelectionChangedListener: OnSelectionChangedListener? = null
160168
private var onImeBackListener: OnImeBackListener? = null
@@ -205,6 +213,9 @@ class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlT
205213
var maxImagesWidth: Int = 0
206214
var minImagesWidth: Int = 0
207215

216+
var observationQueue: ObservationQueue = ObservationQueue(this)
217+
var textWatcherEventBuilder: TextWatcherEvent.Builder = TextWatcherEvent.Builder()
218+
208219
interface OnSelectionChangedListener {
209220
fun onSelectionChanged(selStart: Int, selEnd: Int)
210221
}
@@ -434,8 +445,39 @@ class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlT
434445
DeleteMediaElementWatcher.install(this)
435446

436447
// History related logging has to happen before the changes in [ParagraphCollapseRemover]
437-
addTextChangedListener(this)
448+
addHistoryLoggingWatcher()
438449
ParagraphCollapseRemover.install(this)
450+
451+
// finally add the TextChangedListener
452+
addTextChangedListener(this)
453+
}
454+
455+
private fun addHistoryLoggingWatcher() {
456+
val historyLoggingWatcher = object : TextWatcher {
457+
override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {
458+
if (!isViewInitialized) return
459+
if (!isTextChangedListenerDisabled() && !consumeHistoryEvent) {
460+
history.beforeTextChanged(toFormattedHtml())
461+
}
462+
}
463+
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
464+
if (!isViewInitialized) return
465+
}
466+
override fun afterTextChanged(text: Editable) {
467+
if (isTextChangedListenerDisabled()) {
468+
return
469+
}
470+
471+
isMediaAdded = text.getSpans(0, text.length, AztecMediaSpan::class.java).isNotEmpty()
472+
473+
if (consumeHistoryEvent) {
474+
consumeHistoryEvent = false
475+
}
476+
477+
history.handleHistory(this@AztecText)
478+
}
479+
}
480+
addTextChangedListener(historyLoggingWatcher)
439481
}
440482

441483
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
@@ -800,26 +842,37 @@ class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlT
800842
override fun beforeTextChanged(text: CharSequence, start: Int, count: Int, after: Int) {
801843
if (!isViewInitialized) return
802844

803-
if (!isTextChangedListenerDisabled() && !consumeHistoryEvent) {
804-
history.beforeTextChanged(toFormattedHtml())
845+
if (!bypassObservationQueue) {
846+
// we need to make a copy to preserve the contents as they were before the change
847+
val textCopy = SpannableStringBuilder(text)
848+
val data = BeforeTextChangedEventData(textCopy, start, count, after)
849+
textWatcherEventBuilder.beforeEventData = data
805850
}
806851
}
807852

808853
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
809854
if (!isViewInitialized) return
855+
856+
if (!bypassObservationQueue) {
857+
val textCopy = SpannableStringBuilder(text)
858+
val data = OnTextChangedEventData(textCopy, start, before, count)
859+
textWatcherEventBuilder.onEventData = data
860+
}
810861
}
811862

812863
override fun afterTextChanged(text: Editable) {
813864
if (isTextChangedListenerDisabled()) {
814865
return
815866
}
816867

817-
isMediaAdded = text.getSpans(0, text.length, AztecMediaSpan::class.java).isNotEmpty()
868+
if (!bypassObservationQueue) {
869+
val textCopy = Editable.Factory.getInstance().newEditable(editableText)
870+
val data = AfterTextChangedEventData(textCopy)
871+
textWatcherEventBuilder.afterEventData = data
818872

819-
if (consumeHistoryEvent) {
820-
consumeHistoryEvent = false
873+
// now that we have a full event cycle (before, on, and after) we can add the event to the observation queue
874+
observationQueue.add(textWatcherEventBuilder.build())
821875
}
822-
history.handleHistory(this)
823876
}
824877

825878
fun redo() {
@@ -1046,6 +1099,14 @@ class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlT
10461099
consumeEditEvent = false
10471100
}
10481101

1102+
fun disableObservationQueue() {
1103+
bypassObservationQueue = true
1104+
}
1105+
1106+
fun enableObservationQueue() {
1107+
bypassObservationQueue = false
1108+
}
1109+
10491110
fun isTextChangedListenerDisabled(): Boolean {
10501111
return consumeEditEvent
10511112
}
@@ -1455,4 +1516,17 @@ class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknownHtmlT
14551516
override fun onUnknownHtmlTapped(unknownHtmlSpan: UnknownHtmlSpan) {
14561517
showBlockEditorDialog(unknownHtmlSpan)
14571518
}
1519+
1520+
override fun executeEvent(data: TextWatcherEvent) {
1521+
disableObservationQueue()
1522+
1523+
if (data is TextWatcherEventInsertText) {
1524+
// here replace the inserted thing with a new "normal" insertion
1525+
val afterData = data.afterEventData
1526+
setText(afterData.textAfter)
1527+
setSelection(data.insertionStart+data.insertionLength)
1528+
}
1529+
1530+
enableObservationQueue()
1531+
}
14581532
}

aztec/src/main/kotlin/org/wordpress/aztec/watchers/EndOfBufferMarkerAdder.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package org.wordpress.aztec.watchers
33
import android.text.Editable
44
import android.text.SpannableStringBuilder
55
import android.text.TextWatcher
6-
import android.widget.EditText
76
import android.widget.TextView
7+
import org.wordpress.aztec.AztecText
88
import org.wordpress.aztec.Constants
99
import org.wordpress.aztec.spans.IAztecBlockSpan
1010

@@ -32,7 +32,7 @@ class EndOfBufferMarkerAdder(text: Editable) : TextWatcher {
3232
}
3333

3434
companion object {
35-
fun install(editText: EditText) {
35+
fun install(editText: AztecText) {
3636
editText.addTextChangedListener(EndOfBufferMarkerAdder(editText.text))
3737
}
3838

aztec/src/main/kotlin/org/wordpress/aztec/watchers/ParagraphCollapseAdjuster.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package org.wordpress.aztec.watchers
33
import android.text.Editable
44
import android.text.Spannable
55
import android.text.TextWatcher
6-
import android.widget.TextView
6+
import org.wordpress.aztec.AztecText
77
import org.wordpress.aztec.spans.IParagraphFlagged
88
import org.wordpress.aztec.util.SpanWrapper
99

@@ -46,7 +46,7 @@ class ParagraphCollapseAdjuster : TextWatcher {
4646
override fun afterTextChanged(s: Editable) {}
4747

4848
companion object {
49-
fun install(text: TextView) {
49+
fun install(text: AztecText) {
5050
text.addTextChangedListener(ParagraphCollapseAdjuster())
5151
}
5252
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.wordpress.aztec.watchers.event
2+
3+
import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
4+
5+
interface IEventInjector {
6+
fun executeEvent(data: TextWatcherEvent)
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.wordpress.aztec.watchers.event.buckets
2+
3+
import org.wordpress.aztec.watchers.event.sequence.known.space.API26InWordSpaceInsertionEvent
4+
5+
class API26Bucket : Bucket() {
6+
init {
7+
// constructor - here add all identified sequences for this bucket
8+
userOperations.add(API26InWordSpaceInsertionEvent())
9+
//mUserOperations.add(new ...);
10+
//mUserOperations.add(new ...);
11+
//mUserOperations.add(new ...);
12+
}
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.wordpress.aztec.watchers.event.buckets
2+
3+
import org.wordpress.aztec.watchers.event.sequence.UserOperationEvent
4+
5+
import java.util.ArrayList
6+
7+
/*
8+
extend from this class to construct a specific bucket
9+
*/
10+
abstract class Bucket {
11+
val userOperations = ArrayList<UserOperationEvent>()
12+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.wordpress.aztec.watchers.event.sequence
2+
3+
open class EventSequence<TextWatcherEvent> : ArrayList<TextWatcherEvent>()
4+
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package org.wordpress.aztec.watchers.event.sequence
2+
3+
import android.os.Build
4+
import org.wordpress.aztec.watchers.event.IEventInjector
5+
import org.wordpress.aztec.watchers.event.buckets.API26Bucket
6+
import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
7+
import org.wordpress.aztec.watchers.event.buckets.Bucket
8+
9+
class ObservationQueue(val injector: IEventInjector) : EventSequence<TextWatcherEvent>() {
10+
val buckets = ArrayList<Bucket>()
11+
12+
init {
13+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
14+
buckets.add(API26Bucket())
15+
}
16+
/*
17+
remember to add here any other buckets and init logic as suitable, depending on the context
18+
*/
19+
}
20+
21+
override fun add(element: TextWatcherEvent): Boolean {
22+
synchronized(this@ObservationQueue) {
23+
val added: Boolean = super.add(element)
24+
if (buckets.size == 0) {
25+
return added
26+
}
27+
if (added) {
28+
processQueue()
29+
}
30+
return added
31+
}
32+
}
33+
34+
private fun processQueue() {
35+
// here let's check whether our current queue matches / fits any of the installed buckets
36+
var foundOnePartialMatch = false
37+
for (bucket in buckets) {
38+
for (operation in bucket.userOperations) {
39+
if (size < operation.sequence.size) {
40+
if (operation.isUserOperationPartiallyObservedInSequence(this)) {
41+
foundOnePartialMatch = true
42+
}
43+
} else {
44+
// does this particular event look like a part of any of the user operations as defined in this bucket?
45+
if (operation.isUserOperationObservedInSequence(this)) {
46+
// replace user operation with ONE TextWatcherEvent and inject this one in the actual
47+
// textwatchers
48+
val replacementEvent = operation.buildReplacementEventWithSequenceData(this)
49+
injector.executeEvent(replacementEvent)
50+
clear()
51+
}
52+
}
53+
}
54+
}
55+
56+
// we didn't find neither a partial match nor a total match, let's just clear the queue
57+
if (size > 0 && !foundOnePartialMatch) {
58+
// immediately discard the queue
59+
clear()
60+
}
61+
}
62+
}
63+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.wordpress.aztec.watchers.event.sequence
2+
3+
import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
4+
5+
abstract class UserOperationEvent(var sequence: EventSequence<TextWatcherEvent> = EventSequence()) {
6+
7+
fun addSequenceStep(event: TextWatcherEvent) {
8+
sequence.add(event)
9+
}
10+
11+
fun clear() {
12+
sequence.clear()
13+
}
14+
15+
abstract fun isUserOperationObservedInSequence(sequence: EventSequence<TextWatcherEvent>) : Boolean
16+
abstract fun isUserOperationPartiallyObservedInSequence(sequence: EventSequence<TextWatcherEvent>) : Boolean
17+
abstract fun buildReplacementEventWithSequenceData(sequence: EventSequence<TextWatcherEvent>) : TextWatcherEvent
18+
}
19+

0 commit comments

Comments
 (0)