diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt index de8d098a3..a9d025020 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt @@ -81,6 +81,7 @@ import org.wordpress.aztec.spans.AztecAudioSpan import org.wordpress.aztec.spans.AztecCodeSpan import org.wordpress.aztec.spans.AztecCursorSpan import org.wordpress.aztec.spans.AztecDynamicImageSpan +import org.wordpress.aztec.spans.AztecHeadingSpan import org.wordpress.aztec.spans.AztecImageSpan import org.wordpress.aztec.spans.AztecListItemSpan import org.wordpress.aztec.spans.AztecMediaClickableSpan @@ -289,6 +290,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown var widthMeasureSpec: Int = 0 + var verticalParagraphPadding: Int = 0 var verticalParagraphMargin: Int = 0 var verticalHeadingMargin: Int = 0 @@ -420,8 +422,10 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown commentsVisible = styles.getBoolean(R.styleable.AztecText_commentsVisible, commentsVisible) - verticalParagraphMargin = styles.getDimensionPixelSize(R.styleable.AztecText_blockVerticalPadding, + verticalParagraphPadding = styles.getDimensionPixelSize(R.styleable.AztecText_blockVerticalPadding, resources.getDimensionPixelSize(R.dimen.block_vertical_padding)) + verticalParagraphMargin = styles.getDimensionPixelSize(R.styleable.AztecText_paragraphVerticalMargin, + resources.getDimensionPixelSize(R.dimen.block_vertical_margin)) verticalHeadingMargin = styles.getDimensionPixelSize(R.styleable.AztecText_headingVerticalPadding, resources.getDimensionPixelSize(R.dimen.heading_vertical_padding)) @@ -437,25 +441,51 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown styles.getDimensionPixelSize(R.styleable.AztecText_bulletMargin, 0), styles.getDimensionPixelSize(R.styleable.AztecText_bulletPadding, 0), styles.getDimensionPixelSize(R.styleable.AztecText_bulletWidth, 0), - verticalParagraphMargin) - blockFormatter = BlockFormatter(this, - listStyle, - BlockFormatter.QuoteStyle( + verticalParagraphPadding) + blockFormatter = BlockFormatter(editor = this, + listStyle = listStyle, + quoteStyle = BlockFormatter.QuoteStyle( styles.getColor(R.styleable.AztecText_quoteBackground, 0), styles.getColor(R.styleable.AztecText_quoteColor, 0), styles.getFraction(R.styleable.AztecText_quoteBackgroundAlpha, 1, 1, 0f), styles.getDimensionPixelSize(R.styleable.AztecText_quoteMargin, 0), styles.getDimensionPixelSize(R.styleable.AztecText_quotePadding, 0), styles.getDimensionPixelSize(R.styleable.AztecText_quoteWidth, 0), - verticalParagraphMargin), - BlockFormatter.HeaderStyle(verticalHeadingMargin), - BlockFormatter.PreformatStyle( + verticalParagraphPadding), + headerStyle = BlockFormatter.HeaderStyles(verticalHeadingMargin, mapOf( + AztecHeadingSpan.Heading.H1 to BlockFormatter.HeaderStyles.HeadingStyle( + styles.getDimensionPixelSize(R.styleable.AztecText_headingOneFontSize, 0), + styles.getColor(R.styleable.AztecText_headingOneFontColor, 0) + ), + AztecHeadingSpan.Heading.H2 to BlockFormatter.HeaderStyles.HeadingStyle( + styles.getDimensionPixelSize(R.styleable.AztecText_headingTwoFontSize, 0), + styles.getColor(R.styleable.AztecText_headingTwoFontColor, 0) + ), + AztecHeadingSpan.Heading.H3 to BlockFormatter.HeaderStyles.HeadingStyle( + styles.getDimensionPixelSize(R.styleable.AztecText_headingThreeFontSize, 0), + styles.getColor(R.styleable.AztecText_headingThreeFontColor, 0) + ), + AztecHeadingSpan.Heading.H4 to BlockFormatter.HeaderStyles.HeadingStyle( + styles.getDimensionPixelSize(R.styleable.AztecText_headingFourFontSize, 0), + styles.getColor(R.styleable.AztecText_headingFourFontColor, 0) + ), + AztecHeadingSpan.Heading.H5 to BlockFormatter.HeaderStyles.HeadingStyle( + styles.getDimensionPixelSize(R.styleable.AztecText_headingFiveFontSize, 0), + styles.getColor(R.styleable.AztecText_headingFiveFontColor, 0) + ), + AztecHeadingSpan.Heading.H6 to BlockFormatter.HeaderStyles.HeadingStyle( + styles.getDimensionPixelSize(R.styleable.AztecText_headingSixFontSize, 0), + styles.getColor(R.styleable.AztecText_headingSixFontColor, 0) + ) + )), + preformatStyle = BlockFormatter.PreformatStyle( styles.getColor(R.styleable.AztecText_preformatBackground, 0), getPreformatBackgroundAlpha(styles), styles.getColor(R.styleable.AztecText_preformatColor, 0), - verticalParagraphMargin), - alignmentRendering, - BlockFormatter.ExclusiveBlockStyles(styles.getBoolean(R.styleable.AztecText_exclusiveBlocks, false)) + verticalParagraphPadding), + alignmentRendering = alignmentRendering, + exclusiveBlockStyles = BlockFormatter.ExclusiveBlockStyles(styles.getBoolean(R.styleable.AztecText_exclusiveBlocks, false), verticalParagraphPadding), + paragraphStyle = BlockFormatter.ParagraphStyle(verticalParagraphMargin) ) EnhancedMovementMethod.taskListClickHandler = TaskListClickHandler(listStyle) @@ -727,7 +757,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown ParagraphBleedAdjuster.install(this) ParagraphCollapseAdjuster.install(this) - EndOfParagraphMarkerAdder.install(this, verticalParagraphMargin) + EndOfParagraphMarkerAdder.install(this, verticalParagraphPadding) SuggestionWatcher.install(this) @@ -1547,7 +1577,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown private fun switchToAztecStyle(editable: Editable, start: Int, end: Int) { editable.getSpans(start, end, IAztecBlockSpan::class.java).forEach { blockFormatter.setBlockStyle(it) } - editable.getSpans(start, end, EndOfParagraphMarker::class.java).forEach { it.verticalPadding = verticalParagraphMargin } + editable.getSpans(start, end, EndOfParagraphMarker::class.java).forEach { it.verticalPadding = verticalParagraphPadding } editable.getSpans(start, end, AztecURLSpan::class.java).forEach { it.linkStyle = linkFormatter.linkStyle } editable.getSpans(start, end, AztecCodeSpan::class.java).forEach { it.codeStyle = inlineFormatter.codeStyle } diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/formatting/BlockFormatter.kt b/aztec/src/main/kotlin/org/wordpress/aztec/formatting/BlockFormatter.kt index b35d57757..60d6d4030 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/formatting/BlockFormatter.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/formatting/BlockFormatter.kt @@ -40,16 +40,16 @@ import org.wordpress.aztec.spans.createPreformatSpan import org.wordpress.aztec.spans.createTaskListSpan import org.wordpress.aztec.spans.createUnorderedListSpan import org.wordpress.aztec.util.SpanWrapper -import java.util.Arrays import kotlin.reflect.KClass class BlockFormatter(editor: AztecText, private val listStyle: ListStyle, private val quoteStyle: QuoteStyle, - private val headerStyle: HeaderStyle, + private val headerStyle: HeaderStyles, private val preformatStyle: PreformatStyle, private val alignmentRendering: AlignmentRendering, - private val exclusiveBlockStyles: ExclusiveBlockStyles + private val exclusiveBlockStyles: ExclusiveBlockStyles, + private val paragraphStyle: ParagraphStyle ) : AztecFormatter(editor) { private val listFormatter = ListFormatter(editor) private val indentFormatter = IndentFormatter(editor) @@ -62,8 +62,11 @@ class BlockFormatter(editor: AztecText, data class QuoteStyle(val quoteBackground: Int, val quoteColor: Int, val quoteBackgroundAlpha: Float, val quoteMargin: Int, val quotePadding: Int, val quoteWidth: Int, val verticalPadding: Int) data class PreformatStyle(val preformatBackground: Int, val preformatBackgroundAlpha: Float, val preformatColor: Int, val verticalPadding: Int) - data class HeaderStyle(val verticalPadding: Int) - data class ExclusiveBlockStyles(val enabled: Boolean = false) + data class HeaderStyles(val verticalPadding: Int, val styles: Map) { + data class HeadingStyle(val fontSize: Int, val fontColor: Int) + } + data class ExclusiveBlockStyles(val enabled: Boolean = false, val verticalParagraphMargin: Int) + data class ParagraphStyle(val verticalMargin: Int) fun indent() { listFormatter.indentList() @@ -208,7 +211,7 @@ class BlockFormatter(editor: AztecText, var changed = false // try to remove block styling when pressing backspace at the beginning of the text - editableText.getSpans(0, 0, IAztecBlockSpan::class.java).forEach { + editableText.getSpans(0, 0, IAztecBlockSpan::class.java).forEach { it -> val spanEnd = editableText.getSpanEnd(it) val indexOfNewline = editableText.indexOf('\n').let { if (it != -1) it else editableText.length } @@ -318,7 +321,7 @@ class BlockFormatter(editor: AztecText, } fun removeBlockStyle(textFormat: ITextFormat) { - removeBlockStyle(textFormat, selectionStart, selectionEnd, makeBlock(textFormat, 0).map { it -> it.javaClass }) + removeBlockStyle(textFormat, selectionStart, selectionEnd, makeBlock(textFormat, 0).map { it.javaClass }) } fun removeEntireBlock(type: Class) { @@ -329,7 +332,7 @@ class BlockFormatter(editor: AztecText, } fun removeBlockStyle(textFormat: ITextFormat, originalStart: Int, originalEnd: Int, - spanTypes: List> = Arrays.asList(IAztecBlockSpan::class.java), + spanTypes: List> = listOf(IAztecBlockSpan::class.java), ignoreLineBounds: Boolean = false) { var start = originalStart var end = originalEnd @@ -341,8 +344,8 @@ class BlockFormatter(editor: AztecText, getBoundsOfText(editableText, start, end) } - var startOfBounds = boundsOfSelectedText.start - var endOfBounds = boundsOfSelectedText.endInclusive + var startOfBounds = boundsOfSelectedText.first + var endOfBounds = boundsOfSelectedText.last if (ignoreLineBounds) { val hasPrecedingSpans = spanTypes.any { spanType -> @@ -447,7 +450,7 @@ class BlockFormatter(editor: AztecText, AztecTextFormat.FORMAT_HEADING_5, AztecTextFormat.FORMAT_HEADING_6 -> listOf(createHeadingSpan(nestingLevel, textFormat, attrs, alignmentRendering, headerStyle)) AztecTextFormat.FORMAT_PREFORMAT -> listOf(createPreformatSpan(nestingLevel, alignmentRendering, attrs, preformatStyle)) - else -> listOf(createParagraphSpan(nestingLevel, alignmentRendering, attrs)) + else -> listOf(createParagraphSpan(nestingLevel, alignmentRendering, attrs, paragraphStyle)) } } @@ -476,7 +479,7 @@ class BlockFormatter(editor: AztecText, AztecTextFormat.FORMAT_HEADING_5, AztecTextFormat.FORMAT_HEADING_6 -> makeBlockSpan(AztecHeadingSpan::class, textFormat, nestingLevel, attrs) AztecTextFormat.FORMAT_PREFORMAT -> makeBlockSpan(AztecPreformatSpan::class, textFormat, nestingLevel, attrs) - else -> createParagraphSpan(nestingLevel, alignmentRendering, attrs) + else -> createParagraphSpan(nestingLevel, alignmentRendering, attrs, paragraphStyle) } } @@ -490,7 +493,7 @@ class BlockFormatter(editor: AztecText, typeIsAssignableTo(AztecQuoteSpan::class) -> createAztecQuoteSpan(nestingLevel, attrs, alignmentRendering, quoteStyle) typeIsAssignableTo(AztecHeadingSpan::class) -> createHeadingSpan(nestingLevel, textFormat, attrs, alignmentRendering, headerStyle) typeIsAssignableTo(AztecPreformatSpan::class) -> createPreformatSpan(nestingLevel, alignmentRendering, attrs, preformatStyle) - else -> createParagraphSpan(nestingLevel, alignmentRendering, attrs) + else -> createParagraphSpan(nestingLevel, alignmentRendering, attrs, paragraphStyle) } } @@ -500,6 +503,7 @@ class BlockFormatter(editor: AztecText, is AztecUnorderedListSpan -> blockElement.listStyle = listStyle is AztecTaskListSpan -> blockElement.listStyle = listStyle is AztecQuoteSpan -> blockElement.quoteStyle = quoteStyle + is ParagraphSpan -> blockElement.paragraphStyle = paragraphStyle is AztecPreformatSpan -> blockElement.preformatStyle = preformatStyle is AztecHeadingSpan -> blockElement.headerStyle = headerStyle } @@ -603,10 +607,10 @@ class BlockFormatter(editor: AztecText, } else if (selectionStartIsOnTheNewLine) { val isSingleCharacterLine = (selectionStart > 1 && editableText[selectionStart - 1] != '\n' && editableText[selectionStart - 2] == '\n') || selectionStart == 1 - if (isSingleCharacterLine) { - indexOfFirstLineBreak = selectionStart - 1 + indexOfFirstLineBreak = if (isSingleCharacterLine) { + selectionStart - 1 } else { - indexOfFirstLineBreak = editable.lastIndexOf("\n", selectionStart - 1) + 1 + editable.lastIndexOf("\n", selectionStart - 1) + 1 } if (isTrailingNewlineAtTheEndOfSelection) { indexOfLastLineBreak = editable.indexOf("\n", selectionEnd - 1) @@ -636,12 +640,12 @@ class BlockFormatter(editor: AztecText, } val boundsOfSelectedText = getBoundsOfText(editableText, start, end) - var spans = getAlignedSpans(null, boundsOfSelectedText.start, boundsOfSelectedText.endInclusive) + var spans = getAlignedSpans(null, boundsOfSelectedText.first, boundsOfSelectedText.last) if (start == end) { - if (start == boundsOfSelectedText.start && spans.size > 1) { + if (start == boundsOfSelectedText.first && spans.size > 1) { spans = spans.filter { editableText.getSpanEnd(it) != start } - } else if (start == boundsOfSelectedText.endInclusive && spans.size > 1) { + } else if (start == boundsOfSelectedText.last && spans.size > 1) { spans = spans.filter { editableText.getSpanStart(it) != start } } } @@ -649,12 +653,12 @@ class BlockFormatter(editor: AztecText, if (spans.isNotEmpty()) { spans.filter { it !is AztecListSpan }.forEach { changeAlignment(it, textFormat) } } else { - val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, boundsOfSelectedText.start) + val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, boundsOfSelectedText.first) val alignment = getAlignment(textFormat, - editableText.subSequence(boundsOfSelectedText.start until boundsOfSelectedText.endInclusive)) - editableText.setSpan(createParagraphSpan(nestingLevel, alignment), - boundsOfSelectedText.start, boundsOfSelectedText.endInclusive, Spanned.SPAN_PARAGRAPH) + editableText.subSequence(boundsOfSelectedText.first until boundsOfSelectedText.last)) + editableText.setSpan(createParagraphSpan(nestingLevel, alignment, paragraphStyle = paragraphStyle), + boundsOfSelectedText.first, boundsOfSelectedText.last, Spanned.SPAN_PARAGRAPH) } } @@ -678,9 +682,9 @@ class BlockFormatter(editor: AztecText, if (start != end) { // we want to push line blocks as deep as possible, because they can't contain other block elements (e.g. headings) if (spanToApply is IAztecLineBlockSpan) { - applyLineBlock(blockElementType, boundsOfSelectedText.start, boundsOfSelectedText.endInclusive) + applyLineBlock(blockElementType, boundsOfSelectedText.first, boundsOfSelectedText.last) } else { - val delimiters = getTopBlockDelimiters(boundsOfSelectedText.start, boundsOfSelectedText.endInclusive) + val delimiters = getTopBlockDelimiters(boundsOfSelectedText.first, boundsOfSelectedText.last) for (i in 0 until delimiters.size - 1) { pushNewBlock(delimiters[i], delimiters[i + 1], blockElementType) } @@ -688,12 +692,12 @@ class BlockFormatter(editor: AztecText, editor.setSelection(editor.selectionStart) } else { - val startOfLine = boundsOfSelectedText.start - val endOfLine = boundsOfSelectedText.endInclusive + val startOfLine = boundsOfSelectedText.first + val endOfLine = boundsOfSelectedText.last // we can't add blocks around partial block elements (i.e. list items), everything must go inside - val isWithinPartialBlock = editableText.getSpans(boundsOfSelectedText.start, - boundsOfSelectedText.endInclusive, IAztecCompositeBlockSpan::class.java) + val isWithinPartialBlock = editableText.getSpans(boundsOfSelectedText.first, + boundsOfSelectedText.last, IAztecCompositeBlockSpan::class.java) .any { it.nestingLevel == nestingLevel - 1 } val startOfBlock = mergeWithBlockAbove(startOfLine, endOfLine, spanToApply, nestingLevel, isWithinPartialBlock, blockElementType) @@ -745,8 +749,7 @@ class BlockFormatter(editor: AztecText, // no similar blocks before us so, don't expand } else if (spansOnPreviousLine.nestingLevel != nestingLevel) { // other block is at a different nesting level so, don't expand - } else if (spansOnPreviousLine is AztecHeadingSpan - && spansOnPreviousLine.heading != (spanToApply as AztecHeadingSpan).heading) { + } else if (spansOnPreviousLine is AztecHeadingSpan && spanToApply is AztecHeadingSpan) { // Heading span is of different style so, don't expand } else if (!isWithinList) { // expand the start @@ -767,8 +770,7 @@ class BlockFormatter(editor: AztecText, // no similar blocks after us so, don't expand } else if (spanOnNextLine.nestingLevel != nestingLevel) { // other block is at a different nesting level so, don't expand - } else if (spanOnNextLine is AztecHeadingSpan - && spanOnNextLine.heading != (spanToApply as AztecHeadingSpan).heading) { + } else if (spanOnNextLine is AztecHeadingSpan && spanToApply is AztecHeadingSpan) { // Heading span is of different style so, don't expand } else if (!isWithinList) { // expand the end @@ -808,7 +810,7 @@ class BlockFormatter(editor: AztecText, for (i in lines.indices) { val lineLength = lines[i].length - val lineStart = (0..i - 1).sumBy { lines[it].length + 1 } + val lineStart = (0 until i).sumBy { lines[it].length + 1 } val lineEnd = (lineStart + lineLength).let { if ((start + it) != editableText.length) it + 1 else it // include the newline or not @@ -829,7 +831,7 @@ class BlockFormatter(editor: AztecText, val splitLength = lines[i].length val lineStart = start + (0 until i).sumBy { lines[it].length + 1 } - val lineEnd = Math.min(lineStart + splitLength + 1, end) // +1 to include the newline + val lineEnd = (lineStart + splitLength + 1).coerceAtMost(end) // +1 to include the newline val lineLength = lineEnd - lineStart if (lineLength == 0) continue @@ -846,7 +848,7 @@ class BlockFormatter(editor: AztecText, val splitLength = lines[i].length val lineStart = start + (0 until i).sumBy { lines[it].length + 1 } - val lineEnd = Math.min(lineStart + splitLength + 1, end) // +1 to include the newline + val lineEnd = (lineStart + splitLength + 1).coerceAtMost(end) // +1 to include the newline val lineLength = lineEnd - lineStart if (lineLength == 0) continue @@ -872,7 +874,7 @@ class BlockFormatter(editor: AztecText, } private fun liftListBlock(listSpan: Class, start: Int, end: Int) { - editableText.getSpans(start, end, listSpan).forEach { + editableText.getSpans(start, end, listSpan).forEach { it -> val wrapper = SpanWrapper(editableText, it) editableText.getSpans(wrapper.start, wrapper.end, AztecListItemSpan::class.java).forEach { editableText.removeSpan(it) } @@ -968,7 +970,7 @@ class BlockFormatter(editor: AztecText, val list = ArrayList() for (i in lines.indices) { - val lineStart = (0..i - 1).sumBy { lines[it].length + 1 } + val lineStart = (0 until i).sumBy { lines[it].length + 1 } val lineEnd = lineStart + lines[i].length if (lineStart >= lineEnd) { @@ -984,8 +986,8 @@ class BlockFormatter(editor: AztecText, * multiple lines (before), current partially or entirely selected */ if ((lineStart >= selStart && selEnd >= lineEnd) - || (lineStart <= selEnd && selEnd <= lineEnd) - || (lineStart <= selStart && selStart <= lineEnd)) { + || (selEnd in lineStart..lineEnd) + || (selStart in lineStart..lineEnd)) { list.add(i) } } @@ -1002,7 +1004,7 @@ class BlockFormatter(editor: AztecText, return false } - val start = (0..index - 1).sumBy { lines[it].length + 1 } + val start = (0 until index).sumBy { lines[it].length + 1 } val end = start + lines[index].length if (start >= end) { @@ -1012,20 +1014,20 @@ class BlockFormatter(editor: AztecText, val spans = editableText.getSpans(start, end, AztecHeadingSpan::class.java) for (span in spans) { - when (textFormat) { + return when (textFormat) { AztecTextFormat.FORMAT_HEADING_1 -> - return span.heading == AztecHeadingSpan.Heading.H1 + span.heading == AztecHeadingSpan.Heading.H1 AztecTextFormat.FORMAT_HEADING_2 -> - return span.heading == AztecHeadingSpan.Heading.H2 + span.heading == AztecHeadingSpan.Heading.H2 AztecTextFormat.FORMAT_HEADING_3 -> - return span.heading == AztecHeadingSpan.Heading.H3 + span.heading == AztecHeadingSpan.Heading.H3 AztecTextFormat.FORMAT_HEADING_4 -> - return span.heading == AztecHeadingSpan.Heading.H4 + span.heading == AztecHeadingSpan.Heading.H4 AztecTextFormat.FORMAT_HEADING_5 -> - return span.heading == AztecHeadingSpan.Heading.H5 + span.heading == AztecHeadingSpan.Heading.H5 AztecTextFormat.FORMAT_HEADING_6 -> - return span.heading == AztecHeadingSpan.Heading.H6 - else -> return false + span.heading == AztecHeadingSpan.Heading.H6 + else -> false } } @@ -1098,7 +1100,7 @@ class BlockFormatter(editor: AztecText, val list = ArrayList() for (i in lines.indices) { - val lineStart = (0..i - 1).sumBy { lines[it].length + 1 } + val lineStart = (0 until i).sumBy { lines[it].length + 1 } val lineEnd = lineStart + lines[i].length if (lineStart >= lineEnd) { @@ -1114,8 +1116,8 @@ class BlockFormatter(editor: AztecText, * multiple lines (before), current partially or entirely selected */ if ((lineStart >= selStart && selEnd >= lineEnd) - || (lineStart <= selEnd && selEnd <= lineEnd) - || (lineStart <= selStart && selStart <= lineEnd)) { + || (selEnd in lineStart..lineEnd) + || (selStart in lineStart..lineEnd)) { list.add(i) } } @@ -1131,7 +1133,7 @@ class BlockFormatter(editor: AztecText, return false } - val start = (0..index - 1).sumBy { lines[it].length + 1 } + val start = (0 until index).sumBy { lines[it].length + 1 } val end = start + lines[index].length if (start >= end) { @@ -1209,7 +1211,7 @@ class BlockFormatter(editor: AztecText, val spanStart = editableText.getSpanStart(heading) val spanEnd = editableText.getSpanEnd(heading) val spanFlags = editableText.getSpanFlags(heading) - val spanType = makeBlock(heading.textFormat, 0).map { it -> it.javaClass } + val spanType = makeBlock(heading.textFormat, 0).map { it.javaClass } removeBlockStyle(heading.textFormat, spanStart, spanEnd, spanType) editableText.setSpan(AztecPreformatSpan(heading.nestingLevel, heading.attributes, preformatStyle), spanStart, spanEnd, spanFlags) @@ -1229,7 +1231,7 @@ class BlockFormatter(editor: AztecText, val spanStart = editableText.getSpanStart(preformat) val spanEnd = editableText.getSpanEnd(preformat) val spanFlags = editableText.getSpanFlags(preformat) - val spanType = makeBlock(AztecTextFormat.FORMAT_PREFORMAT, 0).map { it -> it.javaClass } + val spanType = makeBlock(AztecTextFormat.FORMAT_PREFORMAT, 0).map { it.javaClass } removeBlockStyle(AztecTextFormat.FORMAT_PREFORMAT, spanStart, spanEnd, spanType) val headingSpan = createHeadingSpan( diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/handlers/HeadingHandler.kt b/aztec/src/main/kotlin/org/wordpress/aztec/handlers/HeadingHandler.kt index c1515fff6..4ee9737ed 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/handlers/HeadingHandler.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/handlers/HeadingHandler.kt @@ -65,7 +65,7 @@ class HeadingHandler(private val alignmentRendering: AlignmentRendering) : Block companion object { fun cloneHeading(text: Spannable, block: AztecHeadingSpan, alignmentRendering: AlignmentRendering, start: Int, end: Int) { - set(text, createHeadingSpan(block.nestingLevel, block.textFormat, block.attributes, alignmentRendering), start, end) + set(text, createHeadingSpan(block.nestingLevel, block.textFormat, block.attributes, alignmentRendering, block.headerStyle), start, end) } } } diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecHeadingSpan.kt b/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecHeadingSpan.kt index 8be42f052..7d64d6dea 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecHeadingSpan.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/spans/AztecHeadingSpan.kt @@ -18,8 +18,8 @@ fun createHeadingSpan(nestingLevel: Int, tag: String, attributes: AztecAttributes, alignmentRendering: AlignmentRendering, - headerStyle: BlockFormatter.HeaderStyle = BlockFormatter.HeaderStyle(0) -) : AztecHeadingSpan { + headerStyle: BlockFormatter.HeaderStyles = BlockFormatter.HeaderStyles(0, emptyMap()) +): AztecHeadingSpan { val textFormat = when (tag.toLowerCase(Locale.getDefault())) { "h1" -> AztecTextFormat.FORMAT_HEADING_1 "h2" -> AztecTextFormat.FORMAT_HEADING_2 @@ -36,8 +36,8 @@ fun createHeadingSpan(nestingLevel: Int, textFormat: ITextFormat, attributes: AztecAttributes, alignmentRendering: AlignmentRendering, - headerStyle: BlockFormatter.HeaderStyle = BlockFormatter.HeaderStyle(0) -) : AztecHeadingSpan = + headerStyle: BlockFormatter.HeaderStyles = BlockFormatter.HeaderStyles(0, emptyMap()) +): AztecHeadingSpan = when (alignmentRendering) { AlignmentRendering.SPAN_LEVEL -> AztecHeadingSpanAligned(nestingLevel, textFormat, attributes, headerStyle) AlignmentRendering.VIEW_LEVEL -> AztecHeadingSpan(nestingLevel, textFormat, attributes, headerStyle) @@ -55,7 +55,7 @@ class AztecHeadingSpanAligned( override var nestingLevel: Int, textFormat: ITextFormat, override var attributes: AztecAttributes, - override var headerStyle: BlockFormatter.HeaderStyle, + override var headerStyle: BlockFormatter.HeaderStyles, override var align: Layout.Alignment? = null ) : AztecHeadingSpan(nestingLevel, textFormat, attributes, headerStyle), IAztecAlignmentSpan @@ -63,7 +63,7 @@ open class AztecHeadingSpan( override var nestingLevel: Int, textFormat: ITextFormat, override var attributes: AztecAttributes, - open var headerStyle: BlockFormatter.HeaderStyle + open var headerStyle: BlockFormatter.HeaderStyles ) : MetricAffectingSpan(), IAztecLineBlockSpan, LineHeightSpan, UpdateLayout { override val TAG: String get() = heading.tag @@ -80,7 +80,7 @@ open class AztecHeadingSpan( lateinit var heading: Heading var previousFontMetrics: Paint.FontMetricsInt? = null - var previousTextScale: Float = 1.0f + private var previousHeadingSize: HeadingSize = HeadingSize.Scale(1.0f) var previousSpacing: Float? = null enum class Heading constructor(internal val scale: Float, internal val tag: String) { @@ -93,22 +93,24 @@ open class AztecHeadingSpan( } companion object { - private val SCALE_H1: Float = 1.73f - private val SCALE_H2: Float = 1.32f - private val SCALE_H3: Float = 1.02f - private val SCALE_H4: Float = 0.87f - private val SCALE_H5: Float = 0.72f - private val SCALE_H6: Float = 0.60f + private const val SCALE_H1: Float = 1.73f + private const val SCALE_H2: Float = 1.32f + private const val SCALE_H3: Float = 1.02f + private const val SCALE_H4: Float = 0.87f + private const val SCALE_H5: Float = 0.72f + private const val SCALE_H6: Float = 0.60f fun textFormatToHeading(textFormat: ITextFormat): Heading { - when (textFormat) { - AztecTextFormat.FORMAT_HEADING_1 -> return AztecHeadingSpan.Heading.H1 - AztecTextFormat.FORMAT_HEADING_2 -> return AztecHeadingSpan.Heading.H2 - AztecTextFormat.FORMAT_HEADING_3 -> return AztecHeadingSpan.Heading.H3 - AztecTextFormat.FORMAT_HEADING_4 -> return AztecHeadingSpan.Heading.H4 - AztecTextFormat.FORMAT_HEADING_5 -> return AztecHeadingSpan.Heading.H5 - AztecTextFormat.FORMAT_HEADING_6 -> return AztecHeadingSpan.Heading.H6 - else -> { return AztecHeadingSpan.Heading.H1 } + return when (textFormat) { + AztecTextFormat.FORMAT_HEADING_1 -> Heading.H1 + AztecTextFormat.FORMAT_HEADING_2 -> Heading.H2 + AztecTextFormat.FORMAT_HEADING_3 -> Heading.H3 + AztecTextFormat.FORMAT_HEADING_4 -> Heading.H4 + AztecTextFormat.FORMAT_HEADING_5 -> Heading.H5 + AztecTextFormat.FORMAT_HEADING_6 -> Heading.H6 + else -> { + Heading.H1 + } } } } @@ -134,14 +136,16 @@ open class AztecHeadingSpan( var addedTopPadding = false var addedBottomPadding = false + val verticalPadding = headerStyle.verticalPadding + if (start == spanStart || start < spanStart) { - fm.ascent -= headerStyle.verticalPadding - fm.top -= headerStyle.verticalPadding + fm.ascent -= verticalPadding + fm.top -= verticalPadding addedTopPadding = true } if (end == spanEnd || spanEnd < end) { - fm.descent += headerStyle.verticalPadding - fm.bottom += headerStyle.verticalPadding + fm.descent += verticalPadding + fm.bottom += verticalPadding addedBottomPadding = true } @@ -158,19 +162,53 @@ open class AztecHeadingSpan( } override fun updateDrawState(textPaint: TextPaint) { - textPaint.textSize *= heading.scale + when (val headingSize = getHeadingSize()) { + is HeadingSize.Scale -> { + textPaint.textSize *= heading.scale + } + is HeadingSize.Size -> { + textPaint.textSize = headingSize.value.toFloat() + } + } textPaint.isFakeBoldText = true + getHeadingColor()?.let { + textPaint.color = it + } } - override fun updateMeasureState(textPaint: TextPaint) { + override fun updateMeasureState(paint: TextPaint) { + val headingSize = getHeadingSize() // when font size changes - reset cached font metrics to reapply vertical padding - if (previousTextScale != heading.scale || previousSpacing != textPaint.fontSpacing) { + if (headingSize != previousHeadingSize || previousSpacing != paint.fontSpacing) { previousFontMetrics = null } - previousTextScale = heading.scale - previousSpacing = textPaint.fontSpacing + previousHeadingSize = headingSize + previousSpacing = paint.fontSpacing + when (headingSize) { + is HeadingSize.Scale -> { + paint.textSize *= heading.scale + } + is HeadingSize.Size -> { + paint.textSize = headingSize.value.toFloat() + } + } + getHeadingColor()?.let { + paint.color = it + } + } + + private fun getHeadingSize(): HeadingSize { + return headerStyle.styles[heading]?.fontSize?.takeIf { it > 0 }?.let { HeadingSize.Size(it) } + ?: HeadingSize.Scale(heading.scale) + } + + private fun getHeadingColor(): Int? { + return headerStyle.styles[heading]?.fontColor?.takeIf { it != 0 } + } - textPaint.textSize *= heading.scale + sealed class HeadingSize { + data class Scale(val value: Float) : HeadingSize() + data class Size(val value: Int) : HeadingSize() } override fun toString() = "AztecHeadingSpan : $TAG" diff --git a/aztec/src/main/kotlin/org/wordpress/aztec/spans/ParagraphSpan.kt b/aztec/src/main/kotlin/org/wordpress/aztec/spans/ParagraphSpan.kt index 729987a34..1860b0a77 100644 --- a/aztec/src/main/kotlin/org/wordpress/aztec/spans/ParagraphSpan.kt +++ b/aztec/src/main/kotlin/org/wordpress/aztec/spans/ParagraphSpan.kt @@ -1,23 +1,29 @@ package org.wordpress.aztec.spans +import android.graphics.Paint import android.text.Layout +import android.text.Spanned +import android.text.style.LineHeightSpan import org.wordpress.aztec.AlignmentRendering import org.wordpress.aztec.AztecAttributes import org.wordpress.aztec.AztecTextFormat import org.wordpress.aztec.ITextFormat +import org.wordpress.aztec.formatting.BlockFormatter fun createParagraphSpan(nestingLevel: Int, alignmentRendering: AlignmentRendering, - attributes: AztecAttributes = AztecAttributes()): IAztecBlockSpan = + attributes: AztecAttributes = AztecAttributes(), + paragraphStyle: BlockFormatter.ParagraphStyle = BlockFormatter.ParagraphStyle(0)): IAztecBlockSpan = when (alignmentRendering) { - AlignmentRendering.SPAN_LEVEL -> ParagraphSpanAligned(nestingLevel, attributes, null) - AlignmentRendering.VIEW_LEVEL -> ParagraphSpan(nestingLevel, attributes) + AlignmentRendering.SPAN_LEVEL -> ParagraphSpanAligned(nestingLevel, attributes, null, paragraphStyle) + AlignmentRendering.VIEW_LEVEL -> ParagraphSpan(nestingLevel, attributes, paragraphStyle) } fun createParagraphSpan(nestingLevel: Int, align: Layout.Alignment?, - attributes: AztecAttributes = AztecAttributes()): IAztecBlockSpan = - ParagraphSpanAligned(nestingLevel, attributes, align) + attributes: AztecAttributes = AztecAttributes(), + paragraphStyle: BlockFormatter.ParagraphStyle = BlockFormatter.ParagraphStyle(0)): IAztecBlockSpan = + ParagraphSpanAligned(nestingLevel, attributes, align, paragraphStyle) /** * We need to have two classes for handling alignment at either the Span-level (ParagraphSpanAligned) @@ -29,7 +35,48 @@ fun createParagraphSpan(nestingLevel: Int, */ open class ParagraphSpan( override var nestingLevel: Int, - override var attributes: AztecAttributes) : IAztecBlockSpan { + override var attributes: AztecAttributes, + var paragraphStyle: BlockFormatter.ParagraphStyle = BlockFormatter.ParagraphStyle(0)) + : IAztecBlockSpan, LineHeightSpan { + + private var removeTopPadding = false + + override fun chooseHeight(text: CharSequence, start: Int, end: Int, spanstartv: Int, lineHeight: Int, fm: Paint.FontMetricsInt) { + val spanned = text as Spanned + val spanStart = spanned.getSpanStart(this) + val spanEnd = spanned.getSpanEnd(this) + val previousLineBreak = if (start > 1) { + text.substring(start-1, start) == "\n" + } else { + false + } + val followingLineBreak = if (end < text.length) { + text.substring(end, end + 1) == "\n" + } else { + false + } + val isFirstLine = start <= spanStart || previousLineBreak + val isLastLine = spanEnd <= end || followingLineBreak + if (isFirstLine) { + removeTopPadding = true + fm.ascent -= paragraphStyle.verticalMargin + fm.top -= paragraphStyle.verticalMargin + } + if (isLastLine) { + fm.descent += paragraphStyle.verticalMargin + fm.bottom += paragraphStyle.verticalMargin + removeTopPadding = false + } + if (!isFirstLine && !isLastLine && removeTopPadding) { + removeTopPadding = false + if (fm.ascent + paragraphStyle.verticalMargin < 0) { + fm.ascent += paragraphStyle.verticalMargin + } + if (fm.top + paragraphStyle.verticalMargin < 0) { + fm.top += paragraphStyle.verticalMargin + } + } + } override var TAG: String = "p" @@ -41,4 +88,5 @@ open class ParagraphSpan( class ParagraphSpanAligned( nestingLevel: Int, attributes: AztecAttributes, - override var align: Layout.Alignment?) : ParagraphSpan(nestingLevel, attributes), IAztecAlignmentSpan + override var align: Layout.Alignment?, + paragraphStyle: BlockFormatter.ParagraphStyle) : ParagraphSpan(nestingLevel, attributes, paragraphStyle), IAztecAlignmentSpan diff --git a/aztec/src/main/res/values/attrs.xml b/aztec/src/main/res/values/attrs.xml index ba266b226..e1c1fec29 100644 --- a/aztec/src/main/res/values/attrs.xml +++ b/aztec/src/main/res/values/attrs.xml @@ -5,6 +5,7 @@ + @@ -35,6 +36,18 @@ + + + + + + + + + + + + diff --git a/aztec/src/main/res/values/dimens.xml b/aztec/src/main/res/values/dimens.xml index 410d28cf9..0b0af0646 100644 --- a/aztec/src/main/res/values/dimens.xml +++ b/aztec/src/main/res/values/dimens.xml @@ -21,6 +21,7 @@ 16dp 2dp 8dp + 8dp 8dp 0dp 1.0 diff --git a/media-placeholders/src/main/java/org/wordpress/aztec/placeholders/PlaceholderManager.kt b/media-placeholders/src/main/java/org/wordpress/aztec/placeholders/PlaceholderManager.kt index 84aa22739..457dad818 100644 --- a/media-placeholders/src/main/java/org/wordpress/aztec/placeholders/PlaceholderManager.kt +++ b/media-placeholders/src/main/java/org/wordpress/aztec/placeholders/PlaceholderManager.kt @@ -59,6 +59,12 @@ class PlaceholderManager( } fun onDestroy() { + positionToId.forEach { + container.findViewWithTag(it.uuid)?.let { placeholder -> + container.removeView(placeholder) + } + } + positionToId.clear() aztecText.contentChangeWatcher.unregisterObserver(this) adapters.values.forEach { it.onDestroy() } adapters.clear() @@ -85,7 +91,7 @@ class PlaceholderManager( val drawable = buildPlaceholderDrawable(adapter, attrs) aztecText.insertMediaSpan(AztecPlaceholderSpan(aztecText.context, drawable, 0, attrs, this, aztecText, adapter, TAG = htmlTag)) - insertContentOverSpanWithId(attrs.getValue(UUID_ATTRIBUTE), null) + insertContentOverSpanWithId(attrs.getValue(UUID_ATTRIBUTE)) } /** @@ -110,11 +116,11 @@ class PlaceholderManager( positionToId.filter { it.elementPosition >= selectionStart - 1 }.forEach { - insertContentOverSpanWithId(it.uuid, it.elementPosition) + insertContentOverSpanWithId(it.uuid) } } - private suspend fun insertContentOverSpanWithId(uuid: String, currentPosition: Int? = null) { + private suspend fun insertContentOverSpanWithId(uuid: String) { var aztecAttributes: AztecAttributes? = null val predicate = object : AztecText.AttributePredicate { override fun matches(attrs: Attributes): Boolean { @@ -127,10 +133,10 @@ class PlaceholderManager( } val targetPosition = aztecText.getElementPosition(predicate) ?: return - insertInPosition(aztecAttributes ?: return, targetPosition, currentPosition) + insertInPosition(aztecAttributes ?: return, targetPosition) } - private suspend fun insertInPosition(attrs: AztecAttributes, targetPosition: Int, currentPosition: Int? = null) { + private suspend fun insertInPosition(attrs: AztecAttributes, targetPosition: Int) { if (!validateAttributes(attrs)) { return } @@ -139,15 +145,6 @@ class PlaceholderManager( val textViewLayout: Layout = aztecText.layout val parentTextViewRect = Rect() val targetLineOffset = textViewLayout.getLineForOffset(targetPosition) - if (currentPosition != null) { - if (targetLineOffset != 0 && currentPosition == targetPosition) { - return - } else { - positionToId.removeAll { - it.uuid == uuid - } - } - } textViewLayout.getLineBounds(targetLineOffset, parentTextViewRect) val parentTextViewLocation = intArrayOf(0, 0) @@ -157,6 +154,10 @@ class PlaceholderManager( parentTextViewRect.top += parentTextViewTopAndBottomOffset parentTextViewRect.bottom += parentTextViewTopAndBottomOffset + positionToId.removeAll { + it.uuid == uuid + } + var box = container.findViewWithTag(uuid) val exists = box != null val adapter = adapters[type]!! @@ -168,7 +169,12 @@ class PlaceholderManager( parentTextViewRect.bottom - parentTextViewRect.top - 20 ) val padding = 10 - params.setMargins(parentTextViewRect.left + padding, parentTextViewRect.top + padding, parentTextViewRect.right - padding, parentTextViewRect.bottom - padding) + params.setMargins( + parentTextViewRect.left + padding + aztecText.paddingStart, + parentTextViewRect.top + padding, + parentTextViewRect.right - padding - aztecText.paddingEnd, + 0 + ) box.layoutParams = params box.tag = uuid box.setBackgroundColor(Color.TRANSPARENT) @@ -387,7 +393,12 @@ class PlaceholderManager( } else { height.ratio } - (ratio * calculateWidth(attrs, windowWidth)).toInt() + val result = (ratio * calculateWidth(attrs, windowWidth)).toInt() + if (height.limit != null && height.limit < result) { + height.limit + } else { + result + } } } } @@ -406,15 +417,20 @@ class PlaceholderManager( width.ratio > 1.0 -> 1.0f else -> width.ratio } - (safeRatio * windowWidth).toInt() + val result = (safeRatio * windowWidth).toInt() + if (width.limit != null && result > width.limit) { + width.limit + } else { + result + } } } } } sealed class Proportion { - data class Fixed(val value: Int) : Proportion() - data class Ratio(val ratio: Float) : Proportion() + data class Fixed(val value: Int, val limit: Int? = null) : Proportion() + data class Ratio(val ratio: Float, val limit: Int? = null) : Proportion() } }