|
1 | 1 | package org.wordpress.aztec.formatting |
2 | 2 |
|
| 3 | +import android.graphics.drawable.Drawable |
| 4 | +import android.text.SpannableStringBuilder |
| 5 | +import android.text.Spanned |
3 | 6 | import android.text.TextUtils |
| 7 | +import androidx.appcompat.content.res.AppCompatResources |
| 8 | +import org.wordpress.aztec.AztecAttributes |
4 | 9 | import org.wordpress.aztec.AztecText |
5 | 10 | import org.wordpress.aztec.AztecTextFormat |
| 11 | +import org.wordpress.aztec.Constants |
6 | 12 | import org.wordpress.aztec.ITextFormat |
| 13 | +import org.wordpress.aztec.R |
7 | 14 | import org.wordpress.aztec.spans.AztecHeadingSpan |
| 15 | +import org.wordpress.aztec.spans.AztecHorizontalRuleSpan |
| 16 | +import org.wordpress.aztec.spans.AztecImageSpan |
| 17 | +import org.wordpress.aztec.spans.AztecMediaClickableSpan |
| 18 | +import org.wordpress.aztec.spans.AztecMediaSpan |
| 19 | +import org.wordpress.aztec.spans.AztecVideoSpan |
| 20 | +import org.wordpress.aztec.spans.IAztecBlockSpan |
| 21 | +import org.wordpress.aztec.spans.IAztecNestable |
| 22 | +import org.wordpress.aztec.watchers.EndOfBufferMarkerAdder |
| 23 | +import org.xml.sax.Attributes |
8 | 24 |
|
9 | 25 | class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) { |
10 | 26 |
|
@@ -76,4 +92,138 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) { |
76 | 92 |
|
77 | 93 | return false |
78 | 94 | } |
| 95 | + fun applyHorizontalRule(inline: Boolean) { |
| 96 | + val nestingLevel = if (inline) { |
| 97 | + editor.removeInlineStylesFromRange(selectionStart, selectionEnd) |
| 98 | + editor.removeBlockStylesFromRange(selectionStart, selectionEnd, true) |
| 99 | + IAztecNestable.getNestingLevelAt(editableText, selectionStart) |
| 100 | + } else { |
| 101 | + 0 |
| 102 | + } |
| 103 | + |
| 104 | + val span = AztecHorizontalRuleSpan( |
| 105 | + editor.context, |
| 106 | + AppCompatResources.getDrawable(editor.context, R.drawable.img_hr)!!, |
| 107 | + nestingLevel, |
| 108 | + AztecAttributes(), |
| 109 | + editor |
| 110 | + ) |
| 111 | + |
| 112 | + val builder = SpannableStringBuilder(Constants.MAGIC_STRING) |
| 113 | + builder.setSpan(span, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) |
| 114 | + |
| 115 | + if (inline) { |
| 116 | + editableText.replace(selectionStart, selectionEnd, builder) |
| 117 | + val newSelectionPosition = editableText.indexOf(Constants.MAGIC_CHAR, selectionStart) + 1 |
| 118 | + editor.setSelection(newSelectionPosition) |
| 119 | + } else { |
| 120 | + builder.append("\n") |
| 121 | + insertSpanAfterBlock(builder) |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + fun insertVideo(inline: Boolean, drawable: Drawable?, attributes: Attributes, onVideoTappedListener: AztecText.OnVideoTappedListener?, |
| 126 | + onMediaDeletedListener: AztecText.OnMediaDeletedListener?) { |
| 127 | + val nestingLevel = if (inline) IAztecNestable.getNestingLevelAt(editableText, selectionStart) else 0 |
| 128 | + val span = AztecVideoSpan(editor.context, drawable, nestingLevel, AztecAttributes(attributes), onVideoTappedListener, |
| 129 | + onMediaDeletedListener, editor) |
| 130 | + if (inline) { |
| 131 | + insertMediaInline(span) |
| 132 | + } else { |
| 133 | + insertMediaAfterBlock(span) |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + fun insertImage(inline: Boolean, drawable: Drawable?, attributes: Attributes, onImageTappedListener: AztecText.OnImageTappedListener?, |
| 138 | + onMediaDeletedListener: AztecText.OnMediaDeletedListener?) { |
| 139 | + val nestingLevel = if (inline) IAztecNestable.getNestingLevelAt(editableText, selectionStart) else 0 |
| 140 | + val span = AztecImageSpan(editor.context, drawable, nestingLevel, AztecAttributes(attributes), onImageTappedListener, |
| 141 | + onMediaDeletedListener, editor) |
| 142 | + if (inline) { |
| 143 | + insertMediaInline(span) |
| 144 | + } else { |
| 145 | + insertMediaAfterBlock(span) |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + private fun insertMediaInline(span: AztecMediaSpan) { |
| 150 | + editor.removeInlineStylesFromRange(selectionStart, selectionEnd) |
| 151 | + |
| 152 | + val ssb = SpannableStringBuilder(Constants.IMG_STRING) |
| 153 | + |
| 154 | + buildClickableMediaSpan(ssb, span) |
| 155 | + |
| 156 | + // We need to be sure the cursor is placed correctly after media insertion |
| 157 | + // Note that media has '\n' around them when needed |
| 158 | + val isLastItem = selectionEnd == EndOfBufferMarkerAdder.safeLength(editor) |
| 159 | + editableText.replace(selectionStart, selectionEnd, ssb) |
| 160 | + |
| 161 | + setSelection(isLastItem, selectionEnd) |
| 162 | + } |
| 163 | + |
| 164 | + private fun insertMediaAfterBlock(span: AztecMediaSpan) { |
| 165 | + val ssb = SpannableStringBuilder(Constants.IMG_STRING) |
| 166 | + ssb.append("\n") |
| 167 | + buildClickableMediaSpan(ssb, span) |
| 168 | + insertSpanAfterBlock(ssb) |
| 169 | + } |
| 170 | + |
| 171 | + private fun insertSpanAfterBlock(ssb: SpannableStringBuilder) { |
| 172 | + val position = getEndOfBlock() |
| 173 | + // We need to be sure the cursor is placed correctly after media insertion |
| 174 | + // Note that media has '\n' around them when needed |
| 175 | + val isLastItem = position == EndOfBufferMarkerAdder.safeLength(editor) |
| 176 | + val insertedLength = ssb.length |
| 177 | + editableText.insert(position, ssb) |
| 178 | + val spans = editableText.getSpans(position, position + insertedLength, IAztecBlockSpan::class.java).filter { |
| 179 | + it !is AztecMediaSpan && editableText.getSpanStart(it) == position |
| 180 | + } |
| 181 | + spans.forEach { |
| 182 | + val spanStart = editableText.getSpanStart(it) |
| 183 | + val spanEnd = editableText.getSpanEnd(it) |
| 184 | + val spanFlags = editableText.getSpanFlags(it) |
| 185 | + editableText.removeSpan(it) |
| 186 | + if (spanStart + insertedLength < spanEnd) { |
| 187 | + editableText.setSpan(it, spanStart + insertedLength, spanEnd, spanFlags) |
| 188 | + } |
| 189 | + } |
| 190 | + setSelection(isLastItem, position) |
| 191 | + } |
| 192 | + |
| 193 | + private fun setSelection(isLastItem: Boolean, position: Int) { |
| 194 | + val newSelection = if (isLastItem) { |
| 195 | + EndOfBufferMarkerAdder.safeLength(editor) |
| 196 | + } else { |
| 197 | + if (position < EndOfBufferMarkerAdder.safeLength(editor)) position + 1 else position |
| 198 | + } |
| 199 | + editor.setSelection(newSelection) |
| 200 | + editor.isMediaAdded = true |
| 201 | + } |
| 202 | + |
| 203 | + private fun buildClickableMediaSpan(ssb: SpannableStringBuilder, span: AztecMediaSpan) { |
| 204 | + ssb.setSpan( |
| 205 | + span, |
| 206 | + 0, |
| 207 | + 1, |
| 208 | + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE |
| 209 | + ) |
| 210 | + |
| 211 | + ssb.setSpan( |
| 212 | + AztecMediaClickableSpan(span), |
| 213 | + 0, |
| 214 | + 1, |
| 215 | + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE |
| 216 | + ) |
| 217 | + } |
| 218 | + |
| 219 | + private fun getEndOfBlock(): Int { |
| 220 | + var position = 0 |
| 221 | + editableText.getSpans(selectionStart, selectionEnd, IAztecBlockSpan::class.java).forEach { |
| 222 | + val spanEnd = editableText.getSpanEnd(it) |
| 223 | + if (spanEnd > position) { |
| 224 | + position = spanEnd |
| 225 | + } |
| 226 | + } |
| 227 | + return position |
| 228 | + } |
79 | 229 | } |
0 commit comments