Skip to content

Commit d369093

Browse files
committed
Merge branch 'trunk' into feature/implement-backspace-override
2 parents bc7ef7d + 1717409 commit d369093

File tree

5 files changed

+170
-64
lines changed

5 files changed

+170
-64
lines changed

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,18 +2196,15 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
21962196
lineBlockFormatter.insertVideo(shouldAddMediaInline, drawable, attributes, onVideoTappedListener, onMediaDeletedListener)
21972197
}
21982198

2199-
fun removeMedia(notifyContentChange: Boolean = true, predicate: (Attributes) -> Boolean) {
2199+
fun removeMedia(predicate: (Attributes) -> Boolean) {
22002200
removeMedia(object : AttributePredicate {
22012201
override fun matches(attrs: Attributes): Boolean {
22022202
return predicate(attrs)
22032203
}
2204-
}, notifyContentChange)
2204+
})
22052205
}
22062206

2207-
fun removeMedia(attributePredicate: AttributePredicate, notifyContentChange: Boolean = true) {
2208-
if (!notifyContentChange) {
2209-
disableTextChangedListener()
2210-
}
2207+
fun removeMedia(attributePredicate: AttributePredicate) {
22112208
text.getSpans(0, text.length, AztecMediaSpan::class.java)
22122209
.filter {
22132210
attributePredicate.matches(it.attributes)
@@ -2255,8 +2252,33 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
22552252
}
22562253
mediaSpan.onMediaDeleted()
22572254
}
2258-
if (!notifyContentChange) {
2259-
enableTextChangedListener()
2255+
}
2256+
2257+
fun replaceMediaSpan(aztecMediaSpan: AztecMediaSpan, predicate: (Attributes) -> Boolean) {
2258+
replaceMediaSpan(object : AttributePredicate {
2259+
override fun matches(attrs: Attributes): Boolean {
2260+
return predicate(attrs)
2261+
}
2262+
}, aztecMediaSpan)
2263+
}
2264+
2265+
fun replaceMediaSpan(attributePredicate: AttributePredicate, aztecMediaSpan: AztecMediaSpan) {
2266+
history.beforeTextChanged(this@AztecText)
2267+
text.getSpans(0, text.length, AztecMediaSpan::class.java).firstOrNull {
2268+
attributePredicate.matches(it.attributes)
2269+
}?.let { mediaSpan ->
2270+
mediaSpan.beforeMediaDeleted()
2271+
val start = text.getSpanStart(mediaSpan)
2272+
val end = text.getSpanEnd(mediaSpan)
2273+
2274+
val clickableSpan = text.getSpans(start, end, AztecMediaClickableSpan::class.java).firstOrNull()
2275+
2276+
text.removeSpan(clickableSpan)
2277+
text.removeSpan(mediaSpan)
2278+
mediaSpan.onMediaDeleted()
2279+
aztecMediaSpan.onMediaDeletedListener = onMediaDeletedListener
2280+
lineBlockFormatter.insertMediaSpanOverCurrentChar(aztecMediaSpan, start)
2281+
contentChangeWatcher.notifyContentChanged()
22602282
}
22612283
}
22622284

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,24 @@ class LineBlockFormatter(editor: AztecText) : AztecFormatter(editor) {
150150
}
151151
}
152152

153+
fun insertMediaSpanOverCurrentChar(span: AztecMediaSpan, position: Int) {
154+
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
155+
156+
editor.editableText.setSpan(
157+
span,
158+
position,
159+
position + 1,
160+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
161+
)
162+
163+
editor.editableText.setSpan(
164+
AztecMediaClickableSpan(span),
165+
position,
166+
position + 1,
167+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
168+
)
169+
}
170+
153171
private fun insertMediaInline(span: AztecMediaSpan) {
154172
editor.removeInlineStylesFromRange(selectionStart, selectionEnd)
155173

media-placeholders/src/main/java/org/wordpress/aztec/placeholders/ImageWithCaptionAdapter.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,17 @@ class ImageWithCaptionAdapter(
8282
private const val SRC_ATTRIBUTE = "src"
8383

8484
suspend fun insertImageWithCaption(placeholderManager: PlaceholderManager, src: String, caption: String) {
85-
placeholderManager.insertOrUpdateItem(ADAPTER_TYPE) { currentAttributes, type ->
85+
placeholderManager.insertOrUpdateItem(ADAPTER_TYPE) { currentAttributes, type, placeAtStart ->
8686
if (currentAttributes == null || type != ADAPTER_TYPE) {
8787
mapOf(SRC_ATTRIBUTE to src, CAPTION_ATTRIBUTE to caption)
8888
} else {
8989
val currentCaption = currentAttributes[CAPTION_ATTRIBUTE]
90-
mapOf(SRC_ATTRIBUTE to src, CAPTION_ATTRIBUTE to "$caption - $currentCaption")
90+
val newCaption = if (placeAtStart) {
91+
"$caption - $currentCaption"
92+
} else {
93+
"$currentCaption - $caption"
94+
}
95+
mapOf(SRC_ATTRIBUTE to src, CAPTION_ATTRIBUTE to newCaption)
9196
}
9297
}
9398
}

media-placeholders/src/main/java/org/wordpress/aztec/placeholders/PlaceholderManager.kt

Lines changed: 82 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable
77
import android.text.Editable
88
import android.text.Layout
99
import android.text.Spanned
10-
import android.util.Log
1110
import android.view.MotionEvent
1211
import android.view.View
1312
import android.view.ViewTreeObserver
@@ -26,6 +25,7 @@ import org.wordpress.aztec.AztecContentChangeWatcher
2625
import org.wordpress.aztec.AztecText
2726
import org.wordpress.aztec.Constants
2827
import org.wordpress.aztec.Html
28+
import org.wordpress.aztec.plugins.html2visual.IHtmlPreprocessor
2929
import org.wordpress.aztec.plugins.html2visual.IHtmlTagHandler
3030
import org.wordpress.aztec.spans.AztecMediaClickableSpan
3131
import org.xml.sax.Attributes
@@ -53,7 +53,8 @@ class PlaceholderManager(
5353
Html.MediaCallback,
5454
AztecText.OnMediaDeletedListener,
5555
AztecText.OnVisibilityChangeListener,
56-
CoroutineScope {
56+
CoroutineScope,
57+
IHtmlPreprocessor {
5758
private val adapters = mutableMapOf<String, PlaceholderAdapter>()
5859
private val positionToIdMutex = Mutex()
5960
private val positionToId = mutableSetOf<Placeholder>()
@@ -110,37 +111,29 @@ class PlaceholderManager(
110111
* @param shouldMergeItem this method should return true when the previous type is compatible and should be updated
111112
* @param updateItem function to update current parameters with new params
112113
*/
113-
suspend fun insertOrUpdateItem(type: String, shouldMergeItem: (currentItemType: String) -> Boolean = { true }, updateItem: (currentAttributes: Map<String, String>?, currentType: String?) -> Map<String, String>) {
114-
val previousIndex = (aztecText.selectionStart - 1).coerceAtLeast(0)
115-
val indexBeforePrevious = (aztecText.selectionStart - 2).coerceAtLeast(0)
116-
val from = if (aztecText.editableText[previousIndex] == Constants.IMG_CHAR) {
117-
previousIndex
118-
} else if (aztecText.editableText[previousIndex] == '\n') {
119-
indexBeforePrevious
120-
} else {
121-
aztecText.selectionStart
122-
}
123-
val editableText = aztecText.editableText
124-
val currentItem = editableText.getSpans(
125-
from,
126-
aztecText.selectionStart,
127-
AztecPlaceholderSpan::class.java
128-
).lastOrNull()
129-
val currentType = currentItem?.attributes?.getValue(TYPE_ATTRIBUTE)
114+
suspend fun insertOrUpdateItem(
115+
type: String,
116+
shouldMergeItem: (currentItemType: String) -> Boolean = { true },
117+
updateItem: (
118+
currentAttributes: Map<String, String>?,
119+
currentType: String?,
120+
placeAtStart: Boolean
121+
) -> Map<String, String>
122+
) {
123+
val targetItem = getTargetItem()
124+
val targetSpan = targetItem?.span
125+
val currentType = targetSpan?.attributes?.getValue(TYPE_ATTRIBUTE)
130126
if (currentType != null && shouldMergeItem(currentType)) {
131127
val adapter = adapters[type]
132128
?: throw IllegalArgumentException("Adapter for inserted type not found. Register it with `registerAdapter` method")
133129
val currentAttributes = mutableMapOf<String, String>()
134-
val uuid = currentItem.attributes.getValue(UUID_ATTRIBUTE)
135-
for (i in 0 until currentItem.attributes.length) {
136-
val name = currentItem.attributes.getQName(i)
137-
val value = currentItem.attributes.getValue(name)
130+
val uuid = targetSpan.attributes.getValue(UUID_ATTRIBUTE)
131+
for (i in 0 until targetSpan.attributes.length) {
132+
val name = targetSpan.attributes.getQName(i)
133+
val value = targetSpan.attributes.getValue(name)
138134
currentAttributes[name] = value
139135
}
140-
val updatedAttributes = updateItem(currentAttributes, currentType)
141-
removeItem(false) { aztecAttributes ->
142-
aztecAttributes.getValue(UUID_ATTRIBUTE) == uuid
143-
}
136+
val updatedAttributes = updateItem(currentAttributes, currentType, targetItem.placeAtStart)
144137
val attrs = AztecAttributes().apply {
145138
updatedAttributes.forEach { (key, value) ->
146139
setValue(key, value)
@@ -149,25 +142,66 @@ class PlaceholderManager(
149142
attrs.setValue(UUID_ATTRIBUTE, uuid)
150143
attrs.setValue(TYPE_ATTRIBUTE, type)
151144
val drawable = buildPlaceholderDrawable(adapter, attrs)
152-
aztecText.insertMediaSpan(AztecPlaceholderSpan(aztecText.context, drawable, 0, attrs,
153-
this, aztecText, WeakReference(adapter), TAG = htmlTag))
145+
val span = AztecPlaceholderSpan(aztecText.context, drawable, 0, attrs,
146+
this, aztecText, WeakReference(adapter), TAG = htmlTag)
147+
aztecText.replaceMediaSpan(span) { attributes ->
148+
attributes.getValue(UUID_ATTRIBUTE) == uuid
149+
}
154150
insertContentOverSpanWithId(uuid)
155151
} else {
156-
insertItem(type, *updateItem(null, null).toList().toTypedArray())
152+
insertItem(type, *updateItem(null, null, false).toList().toTypedArray())
157153
}
158154
}
159155

156+
private data class TargetItem(val span: AztecPlaceholderSpan, val placeAtStart: Boolean)
157+
158+
private fun getTargetItem(): TargetItem? {
159+
if (aztecText.length() == 0) {
160+
return null
161+
}
162+
val limitLength = aztecText.length() - 1
163+
val selectionStart = aztecText.selectionStart
164+
val selectionStartMinusOne = (selectionStart - 1).coerceIn(0, limitLength)
165+
val selectionStartMinusTwo = (selectionStart - 2).coerceIn(0, limitLength)
166+
val selectionEnd = aztecText.selectionEnd
167+
val selectionEndPlusOne = (selectionStart + 1).coerceIn(0, limitLength)
168+
val selectionEndPlusTwo = (selectionStart + 2).coerceIn(0, limitLength)
169+
val editableText = aztecText.editableText
170+
var placeAtStart = false
171+
val (from, to) = if (editableText[selectionStartMinusOne] == Constants.IMG_CHAR) {
172+
selectionStartMinusOne to selectionStart
173+
} else if (editableText[selectionStartMinusOne] == '\n' && editableText[selectionStartMinusTwo] == Constants.IMG_CHAR) {
174+
selectionStartMinusTwo to selectionStart
175+
} else if (editableText[selectionEndPlusOne] == Constants.IMG_CHAR) {
176+
placeAtStart = true
177+
selectionEndPlusOne to (selectionEndPlusOne + 1).coerceIn(0, limitLength)
178+
} else if (editableText[selectionEndPlusOne] == '\n' && editableText[selectionEndPlusTwo] == Constants.IMG_CHAR) {
179+
placeAtStart = true
180+
selectionEndPlusTwo to (selectionEndPlusTwo + 1).coerceIn(0, limitLength)
181+
} else {
182+
selectionStart to selectionEnd
183+
}
184+
return editableText.getSpans(
185+
from,
186+
to,
187+
AztecPlaceholderSpan::class.java
188+
).map { TargetItem(it, placeAtStart) }.lastOrNull()
189+
}
190+
160191
/**
161192
* Call this method to remove a placeholder from both the AztecText and the overlaying layer programatically.
162193
* @param predicate determines whether a span should be removed
163194
*/
164-
fun removeItem(notifyContentChange: Boolean = true, predicate: (Attributes) -> Boolean) {
165-
aztecText.removeMedia(notifyContentChange) { predicate(it) }
195+
fun removeItem(predicate: (Attributes) -> Boolean) {
196+
aztecText.removeMedia { predicate(it) }
166197
}
167198

168199
private suspend fun buildPlaceholderDrawable(adapter: PlaceholderAdapter, attrs: AztecAttributes): Drawable {
169200
val drawable = ContextCompat.getDrawable(aztecText.context, android.R.color.transparent)!!
170-
updateDrawableBounds(adapter, attrs, drawable)
201+
val editorWidth = if (aztecText.width > 0) {
202+
aztecText.width - aztecText.paddingStart - aztecText.paddingEnd
203+
} else aztecText.maxImagesWidth
204+
drawable.setBounds(0, 0, adapter.calculateWidth(attrs, editorWidth), adapter.calculateHeight(attrs, editorWidth))
171205
return drawable
172206
}
173207

@@ -340,6 +374,12 @@ class PlaceholderManager(
340374
override fun handleTag(opening: Boolean, tag: String, output: Editable, attributes: Attributes, nestingLevel: Int): Boolean {
341375
if (opening) {
342376
val type = attributes.getValue(TYPE_ATTRIBUTE)
377+
attributes.getValue(UUID_ATTRIBUTE)?.also { uuid ->
378+
container.findViewWithTag<View>(uuid)?.let {
379+
it.visibility = View.GONE
380+
container.removeView(it)
381+
}
382+
}
343383
val adapter = adapters[type] ?: return false
344384
val aztecAttributes = AztecAttributes(attributes)
345385
aztecAttributes.setValue(UUID_ATTRIBUTE, generateUuid())
@@ -396,35 +436,23 @@ class PlaceholderManager(
396436
spans.forEach {
397437
val type = it.attributes.getValue(TYPE_ATTRIBUTE)
398438
val adapter = adapters[type] ?: return@forEach
399-
updateDrawableBounds(adapter, it.attributes, it.drawable)
439+
it.drawable = buildPlaceholderDrawable(adapter, it.attributes)
400440
aztecText.refreshText(false)
401441
insertInPosition(it.attributes, aztecText.editableText.getSpanStart(it))
402442
}
403443
}
404444
}
405445
}
406446

407-
private suspend fun updateDrawableBounds(adapter: PlaceholderAdapter, attrs: AztecAttributes, drawable: Drawable?) {
408-
val editorWidth = if (aztecText.width > 0) {
409-
aztecText.width - aztecText.paddingStart - aztecText.paddingEnd - EDITOR_INNER_PADDING
410-
} else aztecText.maxImagesWidth
411-
if (drawable?.bounds?.right != editorWidth) {
412-
drawable?.setBounds(0, 0, adapter.calculateWidth(attrs, editorWidth), adapter.calculateHeight(attrs, editorWidth))
413-
}
414-
}
415-
416447
private suspend fun clearAllViews() {
417-
Log.d("vojta", "Before clearing all with lock")
418448
positionToIdMutex.withLock {
419-
Log.d("vojta", "Clearing all with lock")
420449
for (placeholder in positionToId) {
421450
container.findViewWithTag<View>(placeholder.uuid)?.let {
422451
it.visibility = View.GONE
423452
container.removeView(it)
424453
}
425454
}
426455
positionToId.clear()
427-
Log.d("vojta", "Cleared all with lock")
428456
}
429457
}
430458

@@ -553,9 +581,17 @@ class PlaceholderManager(
553581
data class Placeholder(val elementPosition: Int, val uuid: String)
554582

555583
companion object {
584+
private const val TAG = "PlaceholderManager"
556585
private const val DEFAULT_HTML_TAG = "placeholder"
557586
private const val UUID_ATTRIBUTE = "uuid"
558587
private const val TYPE_ATTRIBUTE = "type"
559588
private const val EDITOR_INNER_PADDING = 20
560589
}
590+
591+
override fun beforeHtmlProcessed(source: String): String {
592+
runBlocking {
593+
clearAllViews()
594+
}
595+
return source
596+
}
561597
}

0 commit comments

Comments
 (0)