Skip to content

Commit 8f1c088

Browse files
authored
Merge pull request #450 from wordpress-mobile/issue/fix-block-styling-of-empty-lines
Issue/fix block styling of empty lines
2 parents 9c5481e + fec40b7 commit 8f1c088

File tree

6 files changed

+402
-103
lines changed

6 files changed

+402
-103
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
### Fixed
88
- Image/video loading placeholder drawable usage
9+
- Quote styling of the paragraph ends, empty and mixed lines
910
- The missing hint bug if text is empty
1011
- Crash in the URL dialog caused by the non plain text content of a clipboard
1112

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ class AztecText : AppCompatAutoCompleteTextView, TextWatcher, UnknownHtmlSpan.On
665665
AztecTextFormat.FORMAT_STRIKETHROUGH -> return inlineFormatter.containsInlineStyle(AztecTextFormat.FORMAT_STRIKETHROUGH, selStart, selEnd)
666666
AztecTextFormat.FORMAT_UNORDERED_LIST -> return blockFormatter.containsList(AztecTextFormat.FORMAT_UNORDERED_LIST, selStart, selEnd)
667667
AztecTextFormat.FORMAT_ORDERED_LIST -> return blockFormatter.containsList(AztecTextFormat.FORMAT_ORDERED_LIST, selStart, selEnd)
668-
AztecTextFormat.FORMAT_QUOTE -> return blockFormatter.containQuote(selectionStart, selectionEnd)
668+
AztecTextFormat.FORMAT_QUOTE -> return blockFormatter.containsQuote(selectionStart, selectionEnd)
669669
AztecTextFormat.FORMAT_PREFORMAT -> return blockFormatter.containsPreformat(selectionStart, selectionEnd)
670670
AztecTextFormat.FORMAT_LINK -> return linkFormatter.containLink(selStart, selEnd)
671671
AztecTextFormat.FORMAT_CODE -> return inlineFormatter.containsInlineStyle(AztecTextFormat.FORMAT_CODE, selStart, selEnd)

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

Lines changed: 81 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class BlockFormatter(editor: AztecText, val listStyle: ListStyle, val quoteStyle
4242
}
4343

4444
fun toggleQuote() {
45-
if (!containQuote()) {
45+
if (!containsQuote()) {
4646
applyBlockStyle(AztecTextFormat.FORMAT_QUOTE)
4747
} else {
4848
removeBlockStyle(AztecTextFormat.FORMAT_QUOTE)
@@ -134,7 +134,7 @@ class BlockFormatter(editor: AztecText, val listStyle: ListStyle, val quoteStyle
134134
val boundsOfSelectedText = if (ignoreLineBounds) {
135135
IntRange(start, end)
136136
} else {
137-
getSelectedTextBounds(editableText, start, end)
137+
getBoundsOfText(editableText, start, end)
138138
}
139139

140140
var startOfBounds = boundsOfSelectedText.start
@@ -296,34 +296,59 @@ class BlockFormatter(editor: AztecText, val listStyle: ListStyle, val quoteStyle
296296
}
297297
}
298298

299-
fun getSelectedTextBounds(editable: Editable, selectionStart: Int, selectionEnd: Int): IntRange {
300-
val startOfLine: Int
301-
val endOfLine: Int
299+
/**
300+
* Returns paragraph bounds (\n) to the left and to the right of selection.
301+
*/
302+
fun getBoundsOfText(editable: Editable, selectionStart: Int, selectionEnd: Int): IntRange {
303+
val startOfBlock: Int
304+
val endOfBlock: Int
305+
306+
val selectionStartIsOnTheNewLine = selectionStart != selectionEnd && selectionStart > 0
307+
&& selectionStart < editableText.length
308+
&& editable[selectionStart] == '\n'
309+
310+
val selectionStartIsBetweenNewlines = selectionStartIsOnTheNewLine
311+
&& selectionStart > 0
312+
&& selectionStart < editableText.length
313+
&& editable[selectionStart - 1] == '\n'
314+
315+
val isTrailingNewlineAtTheEndOfSelection = selectionStart != selectionEnd
316+
&& selectionEnd > 0
317+
&& editableText.length > selectionEnd
318+
&& editableText[selectionEnd] != Constants.END_OF_BUFFER_MARKER
319+
&& editableText[selectionEnd] != '\n'
320+
&& editableText[selectionEnd - 1] == '\n'
302321

303322
val indexOfFirstLineBreak: Int
304-
val indexOfLastLineBreak = editable.indexOf("\n", selectionEnd)
323+
var indexOfLastLineBreak = editable.indexOf("\n", selectionEnd)
305324

306-
if (indexOfLastLineBreak > 0) {
307-
val characterBeforeLastLineBreak = editable[indexOfLastLineBreak - 1]
308-
if (characterBeforeLastLineBreak != '\n') {
309-
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart - 1) + 1
310-
} else {
311-
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart)
325+
326+
if (selectionStartIsBetweenNewlines) {
327+
indexOfFirstLineBreak = selectionStart
328+
} else if (selectionStartIsOnTheNewLine) {
329+
val isSingleCharacterLine = (selectionStart > 1 && editableText[selectionStart - 1] != '\n' && editableText[selectionStart - 2] == '\n') || selectionStart == 1
330+
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart - if (isSingleCharacterLine) 0 else 1) - if (isSingleCharacterLine) 1 else 0
331+
332+
if (isTrailingNewlineAtTheEndOfSelection) {
333+
indexOfLastLineBreak = editable.indexOf("\n", selectionEnd - 1)
312334
}
313-
} else {
314-
if (indexOfLastLineBreak == -1) {
315-
indexOfFirstLineBreak = if (selectionStart == 0) 0 else {
316-
editable.lastIndexOf("\n", selectionStart) + 1
317-
}
318-
} else {
319-
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart)
335+
} else if (isTrailingNewlineAtTheEndOfSelection) {
336+
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart - 1) + 1
337+
indexOfLastLineBreak = editable.indexOf("\n", selectionEnd - 1)
338+
} else if (indexOfLastLineBreak > 0) {
339+
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart - 1) + 1
340+
} else if (indexOfLastLineBreak == -1) {
341+
indexOfFirstLineBreak = if (selectionStart == 0) 0 else {
342+
editable.lastIndexOf("\n", selectionStart) + 1
320343
}
344+
} else {
345+
indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart)
321346
}
322347

323-
startOfLine = if (indexOfFirstLineBreak != -1) indexOfFirstLineBreak else 0
324-
endOfLine = if (indexOfLastLineBreak != -1) (indexOfLastLineBreak + 1) else editable.length
348+
startOfBlock = if (indexOfFirstLineBreak != -1) indexOfFirstLineBreak else 0
349+
endOfBlock = if (indexOfLastLineBreak != -1) (indexOfLastLineBreak + 1) else editable.length
325350

326-
return IntRange(startOfLine, endOfLine)
351+
return IntRange(startOfBlock, endOfBlock)
327352
}
328353

329354
fun applyBlockStyle(blockElementType: ITextFormat, start: Int = selectionStart, end: Int = selectionEnd) {
@@ -333,42 +358,27 @@ class BlockFormatter(editor: AztecText, val listStyle: ListStyle, val quoteStyle
333358
}
334359

335360
if (start != end) {
336-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, start)
361+
val nestingLevelAtTheStartOfSelection = IAztecNestable.getNestingLevelAt(editableText, start)
362+
val nestingLevelAtTheEndOfSelection = IAztecNestable.getNestingLevelAt(editableText, end)
337363

338-
if (IAztecNestable.getNestingLevelAt(editableText, end) != nestingLevel) {
339-
// TODO: styling across multiple nesting levels not support yet
340-
return
364+
// TODO: styling across multiple nesting levels not support yet
365+
if (nestingLevelAtTheStartOfSelection != nestingLevelAtTheEndOfSelection) {
366+
if (nestingLevelAtTheStartOfSelection == 0 && nestingLevelAtTheEndOfSelection == 1) {
367+
// 0/1 is ok!
368+
} else {
369+
return
370+
}
341371
}
342372

343-
val indexOfFirstLineBreak = editableText.indexOf("\n", end)
344-
345-
val endOfBlock = if (indexOfFirstLineBreak != -1) indexOfFirstLineBreak else editableText.length
346-
val startOfBlock = editableText.lastIndexOf("\n", start)
347-
348-
val selectedLines = editableText.subSequence(startOfBlock + 1..endOfBlock - 1) as Editable
349-
350-
var numberOfLinesWithSpanApplied = 0
351-
var numberOfLines = 0
352-
353-
val lines = TextUtils.split(selectedLines.toString(), "\n")
373+
val boundsOfSelectedText = getBoundsOfText(editableText, start, end)
354374

355-
for (i in lines.indices) {
356-
numberOfLines++
357-
if (containsList(blockElementType, i, selectedLines, nestingLevel)) {
358-
numberOfLinesWithSpanApplied++
359-
}
360-
}
375+
val startOfBlock = boundsOfSelectedText.start
376+
val endOfBlock = boundsOfSelectedText.endInclusive
361377

362-
if (numberOfLines == numberOfLinesWithSpanApplied) {
363-
removeBlockStyle(blockElementType)
364-
} else {
365-
//if block starts with newline do not move index to the right
366-
val startOfBlockModifier = if (startOfBlock >= 0 && editableText[startOfBlock] == '\n') 0 else 1
367-
applyBlock(makeBlockSpan(blockElementType, nestingLevel), startOfBlock + startOfBlockModifier,
368-
(if (endOfBlock == editableText.length) endOfBlock else endOfBlock + 1))
369-
}
378+
applyBlock(makeBlockSpan(blockElementType, nestingLevelAtTheStartOfSelection), startOfBlock,
379+
(if (endOfBlock == editableText.length) endOfBlock else endOfBlock))
370380
} else {
371-
val boundsOfSelectedText = getSelectedTextBounds(editableText, start, end)
381+
val boundsOfSelectedText = getBoundsOfText(editableText, start, end)
372382

373383
val startOfLine = boundsOfSelectedText.start
374384
val endOfLine = boundsOfSelectedText.endInclusive
@@ -517,10 +527,10 @@ class BlockFormatter(editor: AztecText, val listStyle: ListStyle, val quoteStyle
517527

518528
if (list.isEmpty()) return false
519529

520-
return list.any { containsList(textFormat, it, editableText, nestingLevel) }
530+
return list.any { containsBlockElement(textFormat, it, editableText, nestingLevel) }
521531
}
522532

523-
fun containsList(textFormat: ITextFormat, index: Int, text: Editable, nestingLevel: Int): Boolean {
533+
fun containsBlockElement(textFormat: ITextFormat, index: Int, text: Editable, nestingLevel: Int): Boolean {
524534
val lines = TextUtils.split(text.toString(), "\n")
525535
if (index < 0 || index >= lines.size) {
526536
return false
@@ -537,53 +547,26 @@ class BlockFormatter(editor: AztecText, val listStyle: ListStyle, val quoteStyle
537547
return spans.isNotEmpty()
538548
}
539549

540-
fun containQuote(selStart: Int = selectionStart, selEnd: Int = selectionEnd): Boolean {
541-
val lines = TextUtils.split(editableText.toString(), "\n")
542-
val list = ArrayList<Int>()
543-
544-
for (i in lines.indices) {
545-
val lineStart = (0..i - 1).sumBy { lines[it].length + 1 }
546-
val lineEnd = lineStart + lines[i].length
547-
548-
if (lineStart >= lineEnd) {
549-
continue
550-
}
551-
552-
/**
553-
* lineStart >= selStart && selEnd >= lineEnd // single line, current entirely selected OR
554-
* multiple lines (before and/or after), current entirely selected
555-
* lineStart <= selEnd && selEnd <= lineEnd // single line, current partially or entirely selected OR
556-
* multiple lines (after), current partially or entirely selected
557-
* lineStart <= selStart && selStart <= lineEnd // single line, current partially or entirely selected OR
558-
* multiple lines (before), current partially or entirely selected
559-
*/
560-
if ((lineStart >= selStart && selEnd >= lineEnd)
561-
|| (lineStart <= selEnd && selEnd <= lineEnd)
562-
|| (lineStart <= selStart && selStart <= lineEnd)) {
563-
list.add(i)
564-
}
565-
}
566-
567-
if (list.isEmpty()) return false
568-
569-
return list.any { containQuote(it) }
570-
}
571-
572-
fun containQuote(index: Int): Boolean {
573-
val lines = TextUtils.split(editableText.toString(), "\n")
574-
if (index < 0 || index >= lines.size) {
575-
return false
576-
}
550+
fun containsQuote(selStart: Int = selectionStart, selEnd: Int = selectionEnd): Boolean {
551+
if (selStart < 0 || selEnd < 0) return false
577552

578-
val start = (0..index - 1).sumBy { lines[it].length + 1 }
579-
val end = start + lines[index].length
553+
return editableText.getSpans(selStart, selEnd, AztecQuoteSpan::class.java)
554+
.any {
555+
val spanStart = editableText.getSpanStart(it)
556+
val spanEnd = editableText.getSpanEnd(it)
580557

581-
if (start >= end) {
582-
return false
583-
}
558+
if (selStart == selEnd) {
559+
if (editableText.length == selStart) {
560+
selStart in spanStart..spanEnd
561+
} else {
562+
(spanEnd != selStart) && selStart in spanStart..spanEnd
563+
}
584564

585-
val spans = editableText.getSpans(start, end, AztecQuoteSpan::class.java)
586-
return spans.isNotEmpty()
565+
} else {
566+
(selStart in spanStart..spanEnd || selEnd in spanStart..spanEnd) ||
567+
(spanStart in selStart..selEnd || spanEnd in spanStart..spanEnd)
568+
}
569+
}
587570
}
588571

589572
fun containsHeading(textFormat: ITextFormat, selStart: Int = selectionStart, selEnd: Int = selectionEnd): Boolean {

aztec/src/main/kotlin/org/wordpress/aztec/handlers/BlockHandler.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,13 @@ abstract class BlockHandler<SpanType : IAztecBlockSpan>(val clazz: Class<SpanTyp
109109

110110
companion object {
111111
fun set(text: Spannable, block: IAztecBlockSpan, start: Int, end: Int) {
112-
//TODO Super temporary fix that disables styling multiline selection with trailing/leading newlines
113-
try{
112+
//TODO remove try/catch when we will be sure the crash is not happening
113+
try {
114114
text.setSpan(block, start, end, Spanned.SPAN_PARAGRAPH)
115-
}catch (e: RuntimeException){
115+
} catch (e: RuntimeException) {
116+
throw RuntimeException("### START: $start, END: $end\n---\n### TEXT:${text.toString()}", e)
116117
}
117-
118118
}
119+
119120
}
120121
}

aztec/src/test/kotlin/org/wordpress/aztec/AztecToolbarTest.kt

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,4 +707,90 @@ class AztecToolbarTest {
707707
htmlButton.performClick()
708708
TestUtils.equalsIgnoreWhitespace("", sourceText.text.toString())
709709
}
710+
711+
@Test
712+
@Throws(Exception::class)
713+
fun quoteSingleSelectionHighlight() {
714+
// 1\n2\n3\n4
715+
editText.fromHtml("1<blockquote>2<br>3</blockquote>4")
716+
717+
editText.setSelection(0)
718+
Assert.assertFalse(quoteButton.isChecked)
719+
720+
editText.setSelection(1)
721+
Assert.assertFalse(quoteButton.isChecked)
722+
723+
editText.setSelection(2)
724+
Assert.assertTrue(quoteButton.isChecked)
725+
726+
editText.setSelection(3)
727+
Assert.assertTrue(quoteButton.isChecked)
728+
729+
editText.setSelection(4)
730+
Assert.assertTrue(quoteButton.isChecked)
731+
732+
editText.setSelection(5)
733+
Assert.assertTrue(quoteButton.isChecked)
734+
735+
editText.setSelection(6)
736+
Assert.assertFalse(quoteButton.isChecked)
737+
738+
editText.setSelection(7)
739+
Assert.assertFalse(quoteButton.isChecked)
740+
}
741+
742+
@Test
743+
@Throws(Exception::class)
744+
fun quoteMultiSelectionHighlight() {
745+
// 1\n2\n3\n4
746+
editText.fromHtml("1<blockquote>2<br>3</blockquote>4")
747+
748+
//selected 1
749+
editText.setSelection(0, 1)
750+
Assert.assertFalse(quoteButton.isChecked)
751+
752+
//selected 1\n
753+
editText.setSelection(0, 2)
754+
Assert.assertFalse(quoteButton.isChecked)
755+
756+
//selected 1\n2
757+
editText.setSelection(0, 3)
758+
Assert.assertTrue(quoteButton.isChecked)
759+
760+
//selected 1\n2\n
761+
editText.setSelection(0, 4)
762+
Assert.assertTrue(quoteButton.isChecked)
763+
764+
//selected 1\n2\n3\n4
765+
editText.setSelection(0, 7)
766+
Assert.assertTrue(quoteButton.isChecked)
767+
768+
//selected \n
769+
editText.setSelection(1, 2)
770+
Assert.assertFalse(quoteButton.isChecked)
771+
772+
//selected \n2
773+
editText.setSelection(1, 3)
774+
Assert.assertTrue(quoteButton.isChecked)
775+
776+
//selected 2
777+
editText.setSelection(2, 3)
778+
Assert.assertTrue(quoteButton.isChecked)
779+
780+
//selected \n
781+
editText.setSelection(3, 4)
782+
Assert.assertTrue(quoteButton.isChecked)
783+
784+
//selected \n3
785+
editText.setSelection(3, 5)
786+
Assert.assertTrue(quoteButton.isChecked)
787+
788+
//selected 3\n
789+
editText.setSelection(4, 6)
790+
Assert.assertTrue(quoteButton.isChecked)
791+
792+
//selected \n4
793+
editText.setSelection(5, 7)
794+
Assert.assertTrue(quoteButton.isChecked)
795+
}
710796
}

0 commit comments

Comments
 (0)