Skip to content

Commit e4ff470

Browse files
authored
Merge pull request #619 from wordpress-mobile/issue/610-api26-style-lost-prepend-new-line
Issue/610 api26 style lost prepend new line
2 parents 4c368e3 + 8293e13 commit e4ff470

File tree

10 files changed

+364
-43
lines changed

10 files changed

+364
-43
lines changed

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ class EndOfBufferMarkerAdder(text: Editable) : TextWatcher {
118118
}
119119
}
120120

121+
fun safeLength(charSequence: CharSequence): Int {
122+
if (charSequence.length == 0) {
123+
return 0
124+
} else if (charSequence[charSequence.length - 1] == Constants.END_OF_BUFFER_MARKER) {
125+
return charSequence.length - 1
126+
} else {
127+
return charSequence.length
128+
}
129+
}
130+
121131
fun strip(string: String): String {
122132
if (string.isEmpty()) {
123133
return string

aztec/src/main/kotlin/org/wordpress/aztec/watchers/event/buckets/API26Bucket.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package org.wordpress.aztec.watchers.event.buckets
22

33
import org.wordpress.aztec.watchers.event.sequence.known.space.API25InWordSpaceInsertionEvent
4+
import org.wordpress.aztec.watchers.event.sequence.known.space.API26PrependNewLineOnStyledSpecialTextEvent
5+
import org.wordpress.aztec.watchers.event.sequence.known.space.API26PrependNewLineOnStyledTextEvent
46

57
class API26Bucket : Bucket() {
68
init {
79
// constructor - here add all identified sequences for this bucket
810
userOperations.add(API25InWordSpaceInsertionEvent())
11+
userOperations.add(API26PrependNewLineOnStyledTextEvent())
12+
userOperations.add(API26PrependNewLineOnStyledSpecialTextEvent())
913
//mUserOperations.add(new ...);
1014
//mUserOperations.add(new ...);
1115
//mUserOperations.add(new ...);

aztec/src/main/kotlin/org/wordpress/aztec/watchers/event/sequence/ObservationQueue.kt

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ class ObservationQueue(val injector: IEventInjector) : EventSequence<TextWatcher
4141
private fun processQueue() {
4242
// here let's check whether our current queue matches / fits any of the installed buckets
4343
var foundOnePartialMatch = false
44+
45+
// if we only have 2 events and the first one is older than xxx milliseconds,
46+
// that means that event is certainly not worth observing so let's discard that one
47+
// we never pile up events we are not interested in this way
48+
if (size == 2) {
49+
val timeDistance = this.get(1).timestamp - this.get(0).timestamp
50+
if (timeDistance > ObservationQueue.MAXIMUM_TIME_BETWEEN_EVENTS_IN_PATTERN_MS) {
51+
removeAt(0)
52+
}
53+
}
54+
55+
// now let's continue processing
4456
for (bucket in buckets) {
4557
for (operation in bucket.userOperations) {
4658
if (size < operation.sequence.size) {
@@ -49,13 +61,19 @@ class ObservationQueue(val injector: IEventInjector) : EventSequence<TextWatcher
4961
}
5062
} else {
5163
// does this particular event look like a part of any of the user operations as defined in this bucket?
52-
if (operation.isUserOperationObservedInSequence(this)) {
64+
val result = operation.isUserOperationObservedInSequence(this)
65+
if (operation.isFound(result)) {
5366
// replace user operation with ONE TextWatcherEvent and inject this one in the actual
5467
// textwatchers
5568
val replacementEvent = operation.buildReplacementEventWithSequenceData(this)
5669
injector.executeEvent(replacementEvent)
5770
clear()
5871
}
72+
73+
// regardless of the operation being found, let's check if it needs the queue to be cleared
74+
if (operation.needsClear(result)) {
75+
clear()
76+
}
5977
}
6078
}
6179
}
@@ -66,5 +84,9 @@ class ObservationQueue(val injector: IEventInjector) : EventSequence<TextWatcher
6684
clear()
6785
}
6886
}
87+
88+
companion object {
89+
val MAXIMUM_TIME_BETWEEN_EVENTS_IN_PATTERN_MS = 100
90+
}
6991
}
7092

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,29 @@
11
package org.wordpress.aztec.watchers.event.sequence
22

3+
import org.wordpress.android.util.AppLog
4+
import org.wordpress.aztec.spans.AztecCodeSpan
5+
import org.wordpress.aztec.spans.AztecHeadingSpan
6+
import org.wordpress.aztec.spans.AztecListItemSpan
7+
import org.wordpress.aztec.spans.AztecPreformatSpan
8+
import org.wordpress.aztec.watchers.event.text.BeforeTextChangedEventData
39
import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
410

511
abstract class UserOperationEvent(var sequence: EventSequence<TextWatcherEvent> = EventSequence()) {
612

13+
enum class ObservedOperationResultType {
14+
SEQUENCE_FOUND,
15+
SEQUENCE_NOT_FOUND,
16+
SEQUENCE_FOUND_CLEAR_QUEUE
17+
}
18+
19+
fun isFound(resultType: ObservedOperationResultType) : Boolean {
20+
return resultType == ObservedOperationResultType.SEQUENCE_FOUND
21+
}
22+
23+
fun needsClear(resultType: ObservedOperationResultType) : Boolean {
24+
return resultType == ObservedOperationResultType.SEQUENCE_FOUND_CLEAR_QUEUE
25+
}
26+
727
fun addSequenceStep(event: TextWatcherEvent) {
828
sequence.add(event)
929
}
@@ -12,8 +32,59 @@ abstract class UserOperationEvent(var sequence: EventSequence<TextWatcherEvent>
1232
sequence.clear()
1333
}
1434

15-
abstract fun isUserOperationObservedInSequence(sequence: EventSequence<TextWatcherEvent>) : Boolean
16-
abstract fun isUserOperationPartiallyObservedInSequence(sequence: EventSequence<TextWatcherEvent>) : Boolean
35+
fun isUserOperationPartiallyObservedInSequence(sequence: EventSequence<TextWatcherEvent>): Boolean {
36+
for (i in sequence.indices) {
37+
38+
val eventHolder = this.sequence[i]
39+
val observableEvent = sequence[i]
40+
41+
// if time distance between any of the events is longer than 50 millis, discard this as this pattern is
42+
// likely not produced by the platform, but rather the user.
43+
// WARNING! When debugging with breakpoints, you should disable this check as time can exceed the 50 MS limit and
44+
// create undesired behavior.
45+
if (i > 0) { // only try to compare when we have at least 2 events, so we can compare with the previous one
46+
val timestampForPreviousEvent = sequence[i - 1].timestamp
47+
val timeDistance = observableEvent.timestamp - timestampForPreviousEvent
48+
if (timeDistance > ObservationQueue.MAXIMUM_TIME_BETWEEN_EVENTS_IN_PATTERN_MS) {
49+
return false
50+
}
51+
}
52+
53+
eventHolder.beforeEventData = observableEvent.beforeEventData
54+
eventHolder.onEventData = observableEvent.onEventData
55+
eventHolder.afterEventData = observableEvent.afterEventData
56+
57+
// return immediately as soon as we realize the pattern diverges
58+
if (!eventHolder.testFitsBeforeOnAndAfter()) {
59+
return false
60+
}
61+
}
62+
63+
return true
64+
}
65+
66+
fun isEventFoundWithinABlock(data: BeforeTextChangedEventData) : Boolean {
67+
// ok finally let's make sure we are not within a Block element
68+
val inputStart = data.start + data.count
69+
val inputEnd = data.start + data.count + 1
70+
71+
val text = data.textBefore!!
72+
val isInsideList = text.getSpans(inputStart, inputEnd, AztecListItemSpan::class.java).isNotEmpty()
73+
val isInsidePre = text.getSpans(inputStart, inputEnd, AztecPreformatSpan::class.java).isNotEmpty()
74+
val isInsideCode = text.getSpans(inputStart, inputEnd, AztecCodeSpan::class.java).isNotEmpty()
75+
var insideHeading = text.getSpans(inputStart, inputEnd, AztecHeadingSpan::class.java).isNotEmpty()
76+
77+
if (insideHeading && (text.length > inputEnd && text[inputEnd] == '\n')) {
78+
insideHeading = false
79+
}
80+
81+
AppLog.d(AppLog.T.EDITOR, "SEQUENCE OBSERVED COMPLETELY, IS IT WITHIN BLOCK?: " +
82+
(isInsideList || insideHeading || isInsidePre || isInsideCode))
83+
84+
return isInsideList || insideHeading || isInsidePre || isInsideCode
85+
}
86+
87+
abstract fun isUserOperationObservedInSequence(sequence: EventSequence<TextWatcherEvent>) : ObservedOperationResultType
1788
abstract fun buildReplacementEventWithSequenceData(sequence: EventSequence<TextWatcherEvent>) : TextWatcherEvent
1889
}
1990

aztec/src/main/kotlin/org/wordpress/aztec/watchers/event/sequence/known/space/API25InWordSpaceInsertionEvent.kt

Lines changed: 12 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
1414
class API25InWordSpaceInsertionEvent : UserOperationEvent() {
1515
private val SPACE = ' '
1616
private val SPACE_STRING = "" + SPACE
17-
private val MAXIMUM_TIME_BETWEEN_EVENTS_IN_PATTERN_MS = 50
1817

1918
init {
2019
// here we populate our model of reference (which is the sequence of events we expect to find)
@@ -43,7 +42,7 @@ class API25InWordSpaceInsertionEvent : UserOperationEvent() {
4342
addSequenceStep(step4)
4443
}
4544

46-
override fun isUserOperationObservedInSequence(sequence: EventSequence<TextWatcherEvent>): Boolean {
45+
override fun isUserOperationObservedInSequence(sequence: EventSequence<TextWatcherEvent>): ObservedOperationResultType {
4746
/* here check:
4847
4948
If we have 2 deletes followed by 2 inserts AND:
@@ -56,57 +55,32 @@ class API25InWordSpaceInsertionEvent : UserOperationEvent() {
5655

5756
// populate data in our own sequence to be able to run the comparator checks
5857
if (!isUserOperationPartiallyObservedInSequence(sequence)) {
59-
return false
58+
return ObservedOperationResultType.SEQUENCE_NOT_FOUND
6059
}
6160

6261
// ok all events are good individually and match the sequence we want to compare against.
6362
// now let's make sure the BEFORE / AFTER situation is what we are trying to identify
6463
val firstEvent = sequence.first()
65-
val lastEvent = sequence[sequence.size - 1]
64+
val lastEvent = sequence.last()
6665

6766
// if new text length is longer than original text by 1
6867
if (firstEvent.beforeEventData.textBefore?.length == lastEvent.afterEventData.textAfter!!.length - 1) {
6968
// now check that the inserted character is actually a space
70-
//val (_, start, count) = firstEvent.beforeEventData
7169
val data = firstEvent.beforeEventData
7270
if (lastEvent.afterEventData.textAfter!![data.start + data.count] == SPACE) {
73-
return true
71+
// okay sequence has been observed completely, let's make sure we are not within a Block
72+
if (!isEventFoundWithinABlock(data)) {
73+
return ObservedOperationResultType.SEQUENCE_FOUND
74+
} else {
75+
// we're within a Block, things are going to be handled by the BlockHandler so let's just request
76+
// a queue clear only
77+
return ObservedOperationResultType.SEQUENCE_FOUND_CLEAR_QUEUE
78+
}
7479
}
7580
}
7681
}
7782

78-
return false
79-
}
80-
81-
override fun isUserOperationPartiallyObservedInSequence(sequence: EventSequence<TextWatcherEvent>): Boolean {
82-
for (i in sequence.indices) {
83-
84-
val eventHolder = this.sequence[i]
85-
val observableEvent = sequence[i]
86-
87-
// if time distance between any of the events is longer than 50 millis, discard this as this pattern is
88-
// likely not produced by the platform, but rather the user.
89-
// WARNING! When debugging with breakpoints, you should disable this check as time can exceed the 50 MS limit and
90-
// create undesired behavior.
91-
if (i > 0) { // only try to compare when we have at least 2 events, so we can compare with the previous one
92-
val timestampForPreviousEvent = sequence[i - 1].timestamp
93-
val timeDistance = observableEvent.timestamp - timestampForPreviousEvent
94-
if (timeDistance > MAXIMUM_TIME_BETWEEN_EVENTS_IN_PATTERN_MS) {
95-
return false
96-
}
97-
}
98-
99-
eventHolder.beforeEventData = observableEvent.beforeEventData
100-
eventHolder.onEventData = observableEvent.onEventData
101-
eventHolder.afterEventData = observableEvent.afterEventData
102-
103-
// return immediately as soon as we realize the pattern diverges
104-
if (!eventHolder.testFitsBeforeOnAndAfter()) {
105-
return false
106-
}
107-
}
108-
109-
return true
83+
return ObservedOperationResultType.SEQUENCE_NOT_FOUND
11084
}
11185

11286
override fun buildReplacementEventWithSequenceData(sequence: EventSequence<TextWatcherEvent>): TextWatcherEvent {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package org.wordpress.aztec.watchers.event.sequence.known.space
2+
3+
import org.wordpress.aztec.Constants
4+
import org.wordpress.aztec.watchers.event.sequence.EventSequence
5+
import org.wordpress.aztec.watchers.event.sequence.UserOperationEvent
6+
import org.wordpress.aztec.watchers.event.sequence.known.space.steps.TextWatcherEventDeleteText
7+
import org.wordpress.aztec.watchers.event.sequence.known.space.steps.TextWatcherEventInsertText
8+
import org.wordpress.aztec.watchers.event.sequence.known.space.steps.TextWatcherEventInsertTextDelAfter
9+
import org.wordpress.aztec.watchers.event.text.AfterTextChangedEventData
10+
import org.wordpress.aztec.watchers.event.text.TextWatcherEvent
11+
12+
/*
13+
This case implements the behavior observed in https://github.com/wordpress-mobile/AztecEditor-Android/issues/610
14+
special case for block formated text like HEADING, LIST, etc.
15+
*/
16+
class API26PrependNewLineOnStyledSpecialTextEvent : UserOperationEvent() {
17+
18+
init {
19+
// here we populate our model of reference (which is the sequence of events we expect to find)
20+
// note we don' populate the TextWatcherEvents with actual data, but rather we just want
21+
// to instantiate them so we can populate them later and test whether data holds true to their
22+
// validation.
23+
24+
// 1 generic delete, followed by 1 special insert, then 1 generic insert
25+
val builder = TextWatcherEventDeleteText.Builder()
26+
val step1 = builder.build()
27+
28+
val builderStep2 = TextWatcherEventInsertTextDelAfter.Builder()
29+
val step2 = builderStep2.build()
30+
31+
val builderStep3 = TextWatcherEventInsertText.Builder()
32+
val step3 = builderStep3.build()
33+
34+
// add each of the steps that make up for the identified API26InWordSpaceInsertionEvent here
35+
clear()
36+
addSequenceStep(step1)
37+
addSequenceStep(step2)
38+
addSequenceStep(step3)
39+
}
40+
41+
override fun isUserOperationObservedInSequence(sequence: EventSequence<TextWatcherEvent>): ObservedOperationResultType {
42+
/* here check:
43+
44+
If we have 1 delete followed by 2 inserts AND:
45+
1) checking the first BEFORETEXTCHANGED and
46+
2) checking the LAST AFTERTEXTCHANGED
47+
text length is longer by 1, and the item that is now located start of AFTERTEXTCHANGED is a NEWLINE character.
48+
49+
*/
50+
if (this.sequence.size == sequence.size) {
51+
52+
// populate data in our own sequence to be able to run the comparator checks
53+
if (!isUserOperationPartiallyObservedInSequence(sequence)) {
54+
return ObservedOperationResultType.SEQUENCE_NOT_FOUND
55+
}
56+
57+
// ok all events are good individually and match the sequence we want to compare against.
58+
// now let's make sure the BEFORE / AFTER situation is what we are trying to identify
59+
val firstEvent = sequence.first()
60+
val lastEvent = sequence.last()
61+
val midEvent = sequence[1]
62+
63+
// if new text length is equal as original text length
64+
if (firstEvent.beforeEventData.textBefore?.length == lastEvent.afterEventData.textAfter!!.length) {
65+
//but, middle event has a new line at the start index of change
66+
if (midEvent.onEventData.textOn!![midEvent.onEventData.start] == Constants.NEWLINE) {
67+
return ObservedOperationResultType.SEQUENCE_FOUND
68+
}
69+
}
70+
}
71+
72+
return ObservedOperationResultType.SEQUENCE_NOT_FOUND
73+
}
74+
75+
override fun buildReplacementEventWithSequenceData(sequence: EventSequence<TextWatcherEvent>): TextWatcherEvent {
76+
val builder = TextWatcherEventInsertText.Builder()
77+
// here make it all up as a unique event that does the insert as usual, as we'd get it on older APIs
78+
val firstEvent = sequence.first()
79+
80+
val (oldText) = firstEvent.beforeEventData
81+
82+
val indexWhereToInsertNewLine = firstEvent.beforeEventData.start
83+
oldText?.insert(indexWhereToInsertNewLine, Constants.NEWLINE_STRING)
84+
85+
builder.afterEventData = AfterTextChangedEventData(oldText)
86+
val replacementEvent = builder.build()
87+
replacementEvent.insertionStart = indexWhereToInsertNewLine
88+
replacementEvent.insertionLength = 1
89+
90+
return replacementEvent
91+
}
92+
}

0 commit comments

Comments
 (0)