Skip to content

Commit 1b4e4de

Browse files
authored
Merge pull request #398 from wordpress-mobile/issue/345-fixing-toolbar-highlight-issues
Fixing toolbar highlight issues
2 parents 072979d + c3769d3 commit 1b4e4de

File tree

7 files changed

+308
-61
lines changed

7 files changed

+308
-61
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package org.wordpress.aztec.demo
2+
3+
import android.support.test.espresso.action.ViewActions
4+
import android.support.test.espresso.action.ViewActions.closeSoftKeyboard
5+
import android.support.test.espresso.action.ViewActions.typeText
6+
import android.support.test.espresso.assertion.ViewAssertions.matches
7+
import android.support.test.espresso.matcher.ViewMatchers.*
8+
import android.support.test.rule.ActivityTestRule
9+
import android.support.test.runner.AndroidJUnit4
10+
import android.view.KeyEvent
11+
import org.junit.Rule
12+
import org.junit.Test
13+
import org.junit.runner.RunWith
14+
import org.wordpress.aztec.demo.TestUtils.*
15+
16+
@RunWith(AndroidJUnit4::class)
17+
class ToolbarHighlightingTest {
18+
19+
@Rule @JvmField
20+
var mActivityTestRule = ActivityTestRule(MainActivity::class.java)
21+
22+
//test behavior of highlighted style at 0 index of editor with 1 line of text (EOB marker at the 1 line)
23+
@Test
24+
fun testLeadingStyleHighlightInEmptyEditor() {
25+
boldButton.perform(betterScrollTo(), betterClick())
26+
aztecText.perform(typeText(formattedText))
27+
28+
italicButton.perform(betterScrollTo(), betterClick())
29+
30+
boldButton.check(matches(isChecked()))
31+
italicButton.check(matches(isChecked()))
32+
33+
formattedText.forEach {
34+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL))
35+
}
36+
37+
boldButton.check(matches(isChecked()))
38+
italicButton.check(matches(isNotChecked()))
39+
40+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL))
41+
42+
boldButton.check(matches(isNotChecked()))
43+
italicButton.check(matches(isNotChecked()))
44+
45+
}
46+
47+
//test behavior of highlighted style at 0 index of editor with > 1 lines of text (no EOB marker at the 1 line)
48+
@Test
49+
fun testLeadingStyleHighlightInNotEmptyEditor() {
50+
boldButton.perform(betterScrollTo(), betterClick())
51+
aztecText.perform(typeText(formattedText))
52+
italicButton.perform(betterScrollTo(), betterClick())
53+
54+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER))
55+
56+
boldButton.check(matches(isNotChecked()))
57+
italicButton.check(matches(isNotChecked()))
58+
59+
aztecText.perform(typeText(formattedText))
60+
61+
62+
formattedText.forEach {
63+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL))
64+
}
65+
66+
formattedText.forEach {
67+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL))
68+
}
69+
70+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL))
71+
72+
boldButton.check(matches(isChecked()))
73+
italicButton.check(matches(isNotChecked()))
74+
75+
aztecText.perform(ViewActions.pressKey(KeyEvent.KEYCODE_DEL))
76+
77+
boldButton.check(matches(isNotChecked()))
78+
italicButton.check(matches(isNotChecked()))
79+
}
80+
81+
//make sure that inline style is not sticking to end of buffer marker
82+
@Test
83+
fun testInlineIsDeselectedNearEndOfBufferMarker() {
84+
boldButton.perform(betterScrollTo(), betterClick())
85+
aztecText.perform(typeText(formattedText))
86+
87+
boldButton.check(matches(isChecked()))
88+
boldButton.perform(betterScrollTo(), betterClick())
89+
boldButton.check(matches(isNotChecked()))
90+
91+
aztecText.perform(typeText(unformattedText))
92+
93+
boldButton.check(matches(isNotChecked()))
94+
95+
// Check that HTML formatting tags were correctly added
96+
toggleHTMLView()
97+
sourceText.check(matches(withText("<b>$formattedText</b>$unformattedText")))
98+
}
99+
100+
101+
//make sure that selected toolbar style in empty editor remains when soft keyboard is displayed
102+
@Test
103+
fun testStyleHighlightPersistenceInEmptyEditorOnWindowFocusChange() {
104+
aztecText.perform(closeSoftKeyboard()) //make sure keyboard is closed
105+
boldButton.perform(betterScrollTo(), betterClick())
106+
aztecText.perform(betterClick()) //click in editor so the soft keyboard is up
107+
108+
boldButton.check(matches(isChecked()))
109+
110+
aztecText.perform(closeSoftKeyboard())
111+
112+
boldButton.check(matches(isChecked()))
113+
114+
aztecText.perform(typeText(formattedText))
115+
toggleHTMLView()
116+
sourceText.check(matches(withText("<b>$formattedText</b>")))
117+
}
118+
119+
}

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

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,15 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
8686
private var addLinkDialog: AlertDialog? = null
8787
private var blockEditorDialog: AlertDialog? = null
8888
private var consumeEditEvent: Boolean = false
89+
private var consumeSelectionChangedEvent: Boolean = false
8990

9091
private var onSelectionChangedListener: OnSelectionChangedListener? = null
9192
private var onImeBackListener: OnImeBackListener? = null
9293
private var onImageTappedListener: OnImageTappedListener? = null
9394
private var onVideoTappedListener: OnVideoTappedListener? = null
9495

9596
private var isViewInitialized = false
97+
private var isLeadingStyleRemoved = false
9698
private var previousCursorPosition = 0
9799

98100
var isInCalypsoMode = true
@@ -235,9 +237,13 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
235237
// detect the press of backspace from hardware keyboard when no characters are deleted (eg. at 0 index of EditText)
236238
setOnKeyListener { v, keyCode, event ->
237239
var consumeKeyEvent = false
238-
history.beforeTextChanged(toFormattedHtml())
239240
if (keyCode == KeyEvent.KEYCODE_DEL && event.action == KeyEvent.ACTION_DOWN) {
240-
inlineFormatter.tryRemoveLeadingInlineStyle()
241+
history.beforeTextChanged(toFormattedHtml())
242+
if (selectionStart == 0 || selectionEnd == 0) {
243+
inlineFormatter.tryRemoveLeadingInlineStyle()
244+
isLeadingStyleRemoved = true
245+
onSelectionChanged(0, 0)
246+
}
241247
consumeKeyEvent = blockFormatter.tryRemoveBlockStyleFromFirstLine()
242248
}
243249

@@ -282,6 +288,7 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
282288
FullWidthImageElementWatcher.install(this)
283289

284290
EndOfBufferMarkerAdder.install(this)
291+
ZeroIndexContentWatcher.install(this)
285292
}
286293

287294
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
@@ -477,6 +484,11 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
477484
super.onSelectionChanged(selStart, selEnd)
478485
if (!isViewInitialized) return
479486

487+
if (isOnSelectionListenerDisabled()) {
488+
enableOnSelectionListener()
489+
return
490+
}
491+
480492
if (length() != 0) {
481493
// if the text end has the marker, let's make sure the cursor never includes it or surpasses it
482494
if ((selStart == length() || selEnd == length()) && text[length() - 1] == Constants.END_OF_BUFFER_MARKER) {
@@ -498,16 +510,23 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
498510

499511
previousCursorPosition = selEnd
500512

501-
onSelectionChangedListener?.onSelectionChanged(selStart, selEnd)
502513

514+
//do not update toolbar or selected styles when we removed the last character in editor
515+
if (!isLeadingStyleRemoved && length() == 1 && text[0] == Constants.END_OF_BUFFER_MARKER) {
516+
return
517+
}
518+
519+
onSelectionChangedListener?.onSelectionChanged(selStart, selEnd)
503520
setSelectedStyles(getAppliedStyles(selStart, selEnd))
521+
522+
isLeadingStyleRemoved = false
504523
}
505524

525+
506526
override fun getSelectionStart(): Int {
507527
return Math.min(super.getSelectionStart(), super.getSelectionEnd())
508528
}
509529

510-
511530
override fun getSelectionEnd(): Int {
512531
return Math.max(super.getSelectionStart(), super.getSelectionEnd())
513532
}
@@ -843,6 +862,19 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
843862
return consumeEditEvent
844863
}
845864

865+
fun disableOnSelectionListener() {
866+
consumeSelectionChangedEvent = true
867+
}
868+
869+
fun enableOnSelectionListener() {
870+
consumeSelectionChangedEvent = false
871+
}
872+
873+
fun isOnSelectionListenerDisabled(): Boolean {
874+
return consumeSelectionChangedEvent
875+
}
876+
877+
846878
fun refreshText() {
847879
disableTextChangedListener()
848880
val selStart = selectionStart
@@ -893,6 +925,13 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
893925
android.R.id.cut -> {
894926
copy(text, min, max)
895927
text.delete(min, max) //this will hide text action menu
928+
929+
//if we are cutting text from the beginning of editor, remove leading inline style
930+
if (min == 0) {
931+
inlineFormatter.tryRemoveLeadingInlineStyle()
932+
isLeadingStyleRemoved = true
933+
onSelectionChanged(0, 0)
934+
}
896935
}
897936
else -> return super.onTextContextMenuItem(id)
898937
}
@@ -1067,10 +1106,16 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
10671106

10681107
override fun sendKeyEvent(event: KeyEvent): Boolean {
10691108
if (event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) {
1109+
val isStyleRemoved = blockFormatter.tryRemoveBlockStyleFromFirstLine()
1110+
10701111
history.beforeTextChanged(toFormattedHtml())
1112+
if (selectionStart == 0 || selectionEnd == 0) {
1113+
inlineFormatter.tryRemoveLeadingInlineStyle()
1114+
isLeadingStyleRemoved = true
1115+
onSelectionChanged(0, 0)
1116+
return false
1117+
}
10711118

1072-
inlineFormatter.tryRemoveLeadingInlineStyle()
1073-
val isStyleRemoved = blockFormatter.tryRemoveBlockStyleFromFirstLine()
10741119
if (isStyleRemoved) {
10751120
history.handleHistory(this@AztecText)
10761121
return false
@@ -1080,6 +1125,10 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
10801125
}
10811126

10821127
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
1128+
//detect pressing of backspace with soft keyboard on 0 index, when no text is deleted
1129+
if (beforeLength == 1 && afterLength == 0 && selectionStart == 0 && selectionEnd == 0) {
1130+
sendKeyEvent(KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL))
1131+
}
10831132
return super.deleteSurroundingText(beforeLength, afterLength)
10841133
}
10851134
}

aztec/src/main/kotlin/org/wordpress/aztec/formatting/InlineFormatter.kt

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle) : AztecFormat
4848
}
4949

5050
fun handleInlineStyling(textChangedEvent: TextChangedEvent) {
51-
//trailing styling
52-
if (!editor.formattingHasChanged() || textChangedEvent.isNewLineButNotAtTheBeginning()) return
51+
if (textChangedEvent.isEndOfBufferMarker()) return
5352

5453
//because we use SPAN_INCLUSIVE_INCLUSIVE for inline styles
5554
//we need to make sure unselected styles are not applied
56-
clearInlineStyles(textChangedEvent.inputStart, textChangedEvent.inputEnd, textChangedEvent.isNewLineButNotAtTheBeginning())
55+
clearInlineStyles(textChangedEvent.inputStart, textChangedEvent.inputEnd, textChangedEvent.isNewLine())
56+
57+
if (textChangedEvent.isNewLine()) return
5758

5859
if (editor.formattingIsApplied()) {
5960
for (item in editor.selectedStyles) {
@@ -62,7 +63,7 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle) : AztecFormat
6263
AztecTextFormat.FORMAT_ITALIC,
6364
AztecTextFormat.FORMAT_STRIKETHROUGH,
6465
AztecTextFormat.FORMAT_UNDERLINE,
65-
AztecTextFormat.FORMAT_CODE -> if (!editor.contains(item, textChangedEvent.inputStart, textChangedEvent.inputEnd)) {
66+
AztecTextFormat.FORMAT_CODE -> {
6667
applyInlineStyle(item, textChangedEvent.inputStart, textChangedEvent.inputEnd)
6768
}
6869
else -> {
@@ -77,24 +78,27 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle) : AztecFormat
7778

7879
private fun clearInlineStyles(start: Int, end: Int, ignoreSelectedStyles: Boolean) {
7980
val newStart = if (start > end) end else start
81+
//if there is END_OF_BUFFER_MARKER at the end of or range, extend the range to include it
8082

81-
editor.getAppliedStyles(start, end).forEach {
82-
if (!editor.selectedStyles.contains(it) || ignoreSelectedStyles || (start == 0 && end == 0) ||
83-
(start > end && editableText.length > end && editableText[end] == '\n')) {
84-
when (it) {
85-
AztecTextFormat.FORMAT_BOLD,
86-
AztecTextFormat.FORMAT_ITALIC,
87-
AztecTextFormat.FORMAT_STRIKETHROUGH,
88-
AztecTextFormat.FORMAT_UNDERLINE,
89-
AztecTextFormat.FORMAT_CODE -> removeInlineStyle(it, newStart, end)
90-
else -> {
91-
//do nothing
92-
}
93-
}
83+
//remove lingering empty spans when removing characters
84+
if (start > end) {
85+
editableText.getSpans(newStart, end, IAztecInlineSpan::class.java)
86+
.filter { editableText.getSpanStart(it) == editableText.getSpanEnd(it) }
87+
.forEach { editableText.removeSpan(it) }
88+
return
89+
}
90+
91+
92+
editableText.getSpans(newStart, end, IAztecInlineSpan::class.java).forEach {
93+
if (!editor.selectedStyles.contains(spanToTextFormat(it)) || ignoreSelectedStyles || (newStart == 0 && end == 0) ||
94+
(newStart > end && editableText.length > end && editableText[end] == '\n')) {
95+
removeInlineStyle(it, newStart, end)
9496
}
97+
9598
}
9699
}
97100

101+
98102
fun applyInlineStyle(textFormat: ITextFormat, start: Int = selectionStart, end: Int = selectionEnd) {
99103
val spanToApply = makeInlineSpan(textFormat)
100104

@@ -164,9 +168,19 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle) : AztecFormat
164168
joinStyleSpans(start, end)
165169
}
166170

167-
fun removeInlineStyle(textFormat: ITextFormat, start: Int = selectionStart, end: Int = selectionEnd) {
168-
//for convenience sake we are initializing the span of same type we are planing to remove
169-
val spanToRemove = makeInlineSpan(textFormat)
171+
fun spanToTextFormat(span: IAztecInlineSpan): ITextFormat? {
172+
when (span::class.java) {
173+
AztecStyleBoldSpan::class.java -> return AztecTextFormat.FORMAT_BOLD
174+
AztecStyleItalicSpan::class.java -> return AztecTextFormat.FORMAT_ITALIC
175+
AztecStrikethroughSpan::class.java -> return AztecTextFormat.FORMAT_STRIKETHROUGH
176+
AztecUnderlineSpan::class.java -> return AztecTextFormat.FORMAT_UNDERLINE
177+
AztecCodeSpan::class.java -> return AztecTextFormat.FORMAT_CODE
178+
else -> return null
179+
}
180+
}
181+
182+
fun removeInlineStyle(spanToRemove: IAztecInlineSpan, start: Int = selectionStart, end: Int = selectionEnd) {
183+
val textFormat = spanToTextFormat(spanToRemove) ?: return
170184

171185
val spans = editableText.getSpans(start, end, IAztecInlineSpan::class.java)
172186
val list = ArrayList<AztecPart>()
@@ -192,6 +206,10 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle) : AztecFormat
192206
joinStyleSpans(start, end)
193207
}
194208

209+
fun removeInlineStyle(textFormat: ITextFormat, start: Int = selectionStart, end: Int = selectionEnd) {
210+
removeInlineStyle(makeInlineSpan(textFormat), start, end)
211+
}
212+
195213
fun isSameInlineSpanType(firstSpan: IAztecInlineSpan, secondSpan: IAztecInlineSpan): Boolean {
196214
if (firstSpan.javaClass == secondSpan.javaClass) {
197215
//special check for StyleSpan
@@ -354,6 +372,12 @@ class InlineFormatter(editor: AztecText, val codeStyle: CodeStyle) : AztecFormat
354372
editableText.removeSpan(it)
355373
}
356374
}
375+
} else if (editor.length() == 1 && editor.text[0] == Constants.END_OF_BUFFER_MARKER) {
376+
editableText.getSpans(0, 1, IAztecInlineSpan::class.java).forEach {
377+
if (editableText.getSpanStart(it) == 1 && editableText.getSpanEnd(it) == 1) {
378+
editableText.removeSpan(it)
379+
}
380+
}
357381
}
358382
}
359383
}

0 commit comments

Comments
 (0)