Skip to content

Commit 2e5fd65

Browse files
authored
Merge pull request #974 from wordpress-mobile/feature/add-media-and-hr-as-blocks
Make sure media and horizontal rule are only added after the currently selected block
2 parents 89c0ed9 + 3d14e18 commit 2e5fd65

File tree

3 files changed

+133
-38
lines changed

3 files changed

+133
-38
lines changed

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
254254
var isInCalypsoMode = true
255255
var isInGutenbergMode: Boolean = false
256256
val alignmentRendering: AlignmentRendering
257+
// If this field is true, the media and horizontal line are added inline. If it's false, they are added after the
258+
// current block.
259+
var shouldAddMediaInline: Boolean = true
257260

258261
var consumeHistoryEvent: Boolean = false
259262

@@ -369,6 +372,15 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
369372
isInGutenbergMode = isCompatibleWithGutenberg
370373
}
371374

375+
/**
376+
* Default behaviour is to add media and horizontal rule inline. They could be added inside lists and quotes.
377+
* If you call this method, this behaviour will be disabled and the media and horizontal rule will be automatically
378+
* added after the currently selected block. For example after the list in which you have your cursor.
379+
*/
380+
fun addMediaAfterBlocks() {
381+
this.shouldAddMediaInline = false
382+
}
383+
372384
// Newer AppCompatEditText returns Editable?, and using that would require changing all of Aztec to not use `text.`
373385
override fun getText(): Editable {
374386
return super.getText()!!
@@ -685,6 +697,9 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
685697
history.beforeTextChanged(this@AztecText)
686698
}
687699
wasStyleRemoved = blockFormatter.tryRemoveBlockStyleFromFirstLine()
700+
if (!shouldAddMediaInline) {
701+
blockFormatter.moveSelectionIfImageSelected()
702+
}
688703

689704
if (selectionStart == 0 || selectionEnd == 0) {
690705
deleteInlineStyleFromTheBeginning()
@@ -1153,7 +1168,9 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
11531168
AztecTextFormat.FORMAT_ALIGN_RIGHT -> return blockFormatter.toggleTextAlignment(textFormat)
11541169
AztecTextFormat.FORMAT_PREFORMAT -> blockFormatter.togglePreformat()
11551170
AztecTextFormat.FORMAT_QUOTE -> blockFormatter.toggleQuote()
1156-
AztecTextFormat.FORMAT_HORIZONTAL_RULE -> lineBlockFormatter.applyHorizontalRule()
1171+
AztecTextFormat.FORMAT_HORIZONTAL_RULE -> {
1172+
lineBlockFormatter.applyHorizontalRule(shouldAddMediaInline)
1173+
}
11571174
else -> {
11581175
plugins.filter { it is IToolbarButton && it.action.textFormats.contains(textFormat) }
11591176
.map { it as IToolbarButton }
@@ -1942,11 +1959,11 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
19421959
}
19431960

19441961
fun insertImage(drawable: Drawable?, attributes: Attributes) {
1945-
lineBlockFormatter.insertImage(drawable, attributes, onImageTappedListener, onMediaDeletedListener)
1962+
lineBlockFormatter.insertImage(shouldAddMediaInline, drawable, attributes, onImageTappedListener, onMediaDeletedListener)
19461963
}
19471964

19481965
fun insertVideo(drawable: Drawable?, attributes: Attributes) {
1949-
lineBlockFormatter.insertVideo(drawable, attributes, onVideoTappedListener, onMediaDeletedListener)
1966+
lineBlockFormatter.insertVideo(shouldAddMediaInline, drawable, attributes, onVideoTappedListener, onMediaDeletedListener)
19501967
}
19511968

19521969
fun removeMedia(attributePredicate: AttributePredicate) {

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import org.wordpress.aztec.handlers.BlockHandler
1616
import org.wordpress.aztec.handlers.HeadingHandler
1717
import org.wordpress.aztec.handlers.ListItemHandler
1818
import org.wordpress.aztec.spans.AztecHeadingSpan
19+
import org.wordpress.aztec.spans.AztecHorizontalRuleSpan
1920
import org.wordpress.aztec.spans.AztecListItemSpan
2021
import org.wordpress.aztec.spans.AztecListSpan
22+
import org.wordpress.aztec.spans.AztecMediaSpan
2123
import org.wordpress.aztec.spans.AztecOrderedListSpan
2224
import org.wordpress.aztec.spans.AztecPreformatSpan
2325
import org.wordpress.aztec.spans.AztecQuoteSpan
@@ -288,6 +290,23 @@ class BlockFormatter(editor: AztecText,
288290
}
289291
}
290292

293+
fun moveSelectionIfImageSelected() {
294+
if (selectionStart == selectionEnd &&
295+
(hasImageRightAfterSelection() || hasHorizontalRuleRightAfterSelection())) {
296+
editor.setSelection(selectionStart - 1)
297+
}
298+
}
299+
300+
private fun hasImageRightAfterSelection() =
301+
editableText.getSpans(selectionStart, selectionEnd, AztecMediaSpan::class.java).any {
302+
editableText.getSpanStart(it) == selectionStart
303+
}
304+
305+
private fun hasHorizontalRuleRightAfterSelection() =
306+
editableText.getSpans(selectionStart, selectionEnd, AztecHorizontalRuleSpan::class.java).any {
307+
editableText.getSpanStart(it) == selectionStart
308+
}
309+
291310
fun removeBlockStyle(textFormat: ITextFormat) {
292311
removeBlockStyle(textFormat, selectionStart, selectionEnd, makeBlock(textFormat, 0).map { it -> it.javaClass })
293312
}
@@ -422,7 +441,7 @@ class BlockFormatter(editor: AztecText,
422441
}
423442
}
424443

425-
fun getAlignment(textFormat: ITextFormat?, text: CharSequence) : Layout.Alignment? {
444+
fun getAlignment(textFormat: ITextFormat?, text: CharSequence): Layout.Alignment? {
426445
val direction = TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR
427446
val isRtl = direction.isRtl(text, 0, text.length)
428447

@@ -533,7 +552,7 @@ class BlockFormatter(editor: AztecText,
533552
return delimiters.distinct().sorted()
534553
}
535554

536-
private fun checkBound(bounds: HashMap<Int, Int>, key: Int, delimiters: ArrayList<Int>, lastIndex: Int) : Int {
555+
private fun checkBound(bounds: HashMap<Int, Int>, key: Int, delimiters: ArrayList<Int>, lastIndex: Int): Int {
537556
if (bounds[key]!! != bounds[lastIndex]!!) {
538557
if (bounds[key]!! < bounds[lastIndex]!!) {
539558
delimiters.add(key)
@@ -1033,7 +1052,7 @@ class BlockFormatter(editor: AztecText,
10331052
return editableText.getSpans(selStart, selEnd, IAztecAlignmentSpan::class.java)
10341053
.filter {
10351054
textFormat == null || it.align == getAlignment(textFormat,
1036-
editableText.substring(editableText.getSpanStart(it) until editableText.getSpanEnd(it)))
1055+
editableText.substring(editableText.getSpanStart(it) until editableText.getSpanEnd(it)))
10371056
}
10381057
.filter {
10391058
val spanStart = editableText.getSpanStart(it)

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

Lines changed: 91 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ import android.text.TextUtils
77
import androidx.appcompat.content.res.AppCompatResources
88
import org.wordpress.aztec.AztecAttributes
99
import org.wordpress.aztec.AztecText
10-
import org.wordpress.aztec.AztecText.OnImageTappedListener
11-
import org.wordpress.aztec.AztecText.OnVideoTappedListener
1210
import org.wordpress.aztec.AztecTextFormat
1311
import org.wordpress.aztec.Constants
1412
import org.wordpress.aztec.ITextFormat
@@ -19,10 +17,10 @@ import org.wordpress.aztec.spans.AztecImageSpan
1917
import org.wordpress.aztec.spans.AztecMediaClickableSpan
2018
import org.wordpress.aztec.spans.AztecMediaSpan
2119
import org.wordpress.aztec.spans.AztecVideoSpan
20+
import org.wordpress.aztec.spans.IAztecBlockSpan
2221
import org.wordpress.aztec.spans.IAztecNestable
2322
import org.wordpress.aztec.watchers.EndOfBufferMarkerAdder
2423
import org.xml.sax.Attributes
25-
import java.util.ArrayList
2624

2725
class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
2826

@@ -94,12 +92,14 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
9492

9593
return false
9694
}
97-
98-
fun applyHorizontalRule() {
99-
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
100-
editor.removeBlockStylesFromRange(selectionStart, selectionEnd, true)
101-
102-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, selectionStart)
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+
}
103103

104104
val span = AztecHorizontalRuleSpan(
105105
editor.context,
@@ -112,34 +112,95 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
112112
val builder = SpannableStringBuilder(Constants.MAGIC_STRING)
113113
builder.setSpan(span, 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
114114

115-
val start = selectionStart
116-
editableText.replace(start, selectionEnd, builder)
117-
118-
val newSelectionPosition = editableText.indexOf(Constants.MAGIC_CHAR, start) + 1
119-
editor.setSelection(newSelectionPosition)
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+
}
120123
}
121124

122-
fun insertVideo(drawable: Drawable?, attributes: Attributes, onVideoTappedListener: OnVideoTappedListener?,
125+
fun insertVideo(inline: Boolean, drawable: Drawable?, attributes: Attributes, onVideoTappedListener: AztecText.OnVideoTappedListener?,
123126
onMediaDeletedListener: AztecText.OnMediaDeletedListener?) {
124-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, selectionStart)
127+
val nestingLevel = if (inline) IAztecNestable.getNestingLevelAt(editableText, selectionStart) else 0
125128
val span = AztecVideoSpan(editor.context, drawable, nestingLevel, AztecAttributes(attributes), onVideoTappedListener,
126129
onMediaDeletedListener, editor)
127-
insertMedia(span)
130+
if (inline) {
131+
insertMediaInline(span)
132+
} else {
133+
insertMediaAfterBlock(span)
134+
}
128135
}
129136

130-
fun insertImage(drawable: Drawable?, attributes: Attributes, onImageTappedListener: OnImageTappedListener?,
137+
fun insertImage(inline: Boolean, drawable: Drawable?, attributes: Attributes, onImageTappedListener: AztecText.OnImageTappedListener?,
131138
onMediaDeletedListener: AztecText.OnMediaDeletedListener?) {
132-
val nestingLevel = IAztecNestable.getNestingLevelAt(editableText, selectionStart)
139+
val nestingLevel = if (inline) IAztecNestable.getNestingLevelAt(editableText, selectionStart) else 0
133140
val span = AztecImageSpan(editor.context, drawable, nestingLevel, AztecAttributes(attributes), onImageTappedListener,
134141
onMediaDeletedListener, editor)
135-
insertMedia(span)
142+
if (inline) {
143+
insertMediaInline(span)
144+
} else {
145+
insertMediaAfterBlock(span)
146+
}
136147
}
137148

138-
private fun insertMedia(span: AztecMediaSpan) {
149+
private fun insertMediaInline(span: AztecMediaSpan) {
139150
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
140151

141152
val ssb = SpannableStringBuilder(Constants.IMG_STRING)
142153

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) {
143204
ssb.setSpan(
144205
span,
145206
0,
@@ -153,18 +214,16 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
153214
1,
154215
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
155216
)
217+
}
156218

157-
// We need to be sure the cursor is placed correctly after media insertion
158-
// Note that media has '\n' around them when needed
159-
val isLastItem = selectionEnd == EndOfBufferMarkerAdder.safeLength(editor)
160-
editableText.replace(selectionStart, selectionEnd, ssb)
161-
162-
val newSelection = if (isLastItem) {
163-
EndOfBufferMarkerAdder.safeLength(editor)
164-
} else {
165-
if (selectionEnd < EndOfBufferMarkerAdder.safeLength(editor)) selectionEnd + 1 else selectionEnd
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+
}
166226
}
167-
editor.setSelection(newSelection)
168-
editor.isMediaAdded = true
227+
return position
169228
}
170229
}

0 commit comments

Comments
 (0)