Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 48 additions & 25 deletions aztec/src/main/kotlin/org/wordpress/aztec/AztecText.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ import org.wordpress.aztec.handlers.ListItemHandler
import org.wordpress.aztec.handlers.PreformatHandler
import org.wordpress.aztec.handlers.QuoteHandler
import org.wordpress.aztec.plugins.IAztecPlugin
import org.wordpress.aztec.plugins.IClipboardPastePlugin
import org.wordpress.aztec.plugins.IOnDrawPlugin
import org.wordpress.aztec.plugins.ITextPastePlugin
import org.wordpress.aztec.plugins.IToolbarButton
import org.wordpress.aztec.source.Format
import org.wordpress.aztec.source.SourceViewEditText
Expand Down Expand Up @@ -259,6 +259,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
var isInCalypsoMode = true
var isInGutenbergMode: Boolean = false
val alignmentRendering: AlignmentRendering

// If this field is true, the media and horizontal line are added inline. If it's false, they are added after the
// current block.
var shouldAddMediaInline: Boolean = true
Expand Down Expand Up @@ -353,8 +354,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
}

interface OnAztecKeyListener {
fun onEnterKey(text: Spannable, firedAfterTextChanged: Boolean, selStart: Int, selEnd: Int) : Boolean
fun onBackspaceKey() : Boolean
fun onEnterKey(text: Spannable, firedAfterTextChanged: Boolean, selStart: Int, selEnd: Int): Boolean
fun onBackspaceKey(): Boolean
}

interface OnLinkTappedListener {
Expand Down Expand Up @@ -435,11 +436,11 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
commentsVisible = styles.getBoolean(R.styleable.AztecText_commentsVisible, commentsVisible)

verticalParagraphPadding = styles.getDimensionPixelSize(R.styleable.AztecText_blockVerticalPadding,
resources.getDimensionPixelSize(R.dimen.block_vertical_padding))
resources.getDimensionPixelSize(R.dimen.block_vertical_padding))
verticalParagraphMargin = styles.getDimensionPixelSize(R.styleable.AztecText_paragraphVerticalMargin,
resources.getDimensionPixelSize(R.dimen.block_vertical_margin))
resources.getDimensionPixelSize(R.dimen.block_vertical_margin))
verticalHeadingMargin = styles.getDimensionPixelSize(R.styleable.AztecText_headingVerticalPadding,
resources.getDimensionPixelSize(R.dimen.heading_vertical_padding))
resources.getDimensionPixelSize(R.dimen.heading_vertical_padding))

inlineFormatter = InlineFormatter(this,
InlineFormatter.CodeStyle(
Expand Down Expand Up @@ -597,7 +598,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
isViewInitialized = true
}

private fun <T>selectionHasExactlyOneMarker(start: Int, end: Int, type: Class<T>): Boolean {
private fun <T> selectionHasExactlyOneMarker(start: Int, end: Int, type: Class<T>): Boolean {
val spanFound: Array<T> = editableText.getSpans(
start,
end,
Expand Down Expand Up @@ -670,11 +671,11 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
// problem is fixed at the Android OS level as described in the following url
// https://android-review.googlesource.com/c/platform/frameworks/base/+/634929
val dynamicLayoutCrashPreventer = InputFilter { source, start, end, dest, dstart, dend ->
var temp : CharSequence? = null
var temp: CharSequence? = null
if (!bypassCrashPreventerInputFilter && dend < dest.length && source != Constants.NEWLINE_STRING) {

// if there are any images right after the destination position, hack the text
val spans = dest.getSpans(dend, dend+1, AztecImageSpan::class.java)
val spans = dest.getSpans(dend, dend + 1, AztecImageSpan::class.java)
if (spans.isNotEmpty()) {

// prevent this filter from running recursively
Expand Down Expand Up @@ -730,7 +731,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
}

private fun isCleanStringEmpty(text: CharSequence): Boolean {
if ( isInGutenbergMode ) {
if (isInGutenbergMode) {
return (text.count() == 1 && text[0] == Constants.END_OF_BUFFER_MARKER)
} else {
return text.count() == 0
Expand Down Expand Up @@ -1045,7 +1046,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
this.onSelectionChangedListener = onSelectionChangedListener
}

fun getAztecKeyListener() : OnAztecKeyListener? {
fun getAztecKeyListener(): OnAztecKeyListener? {
return this.onAztecKeyListener
}

Expand Down Expand Up @@ -1823,7 +1824,8 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
} else {
return super.onTextContextMenuItem(id)
}
} else -> return super.onTextContextMenuItem(id)
}
else -> return super.onTextContextMenuItem(id)
}

return true
Expand Down Expand Up @@ -1900,21 +1902,42 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
enableTextChangedListener()

if (clip.itemCount > 0) {
val textToPaste = if (asPlainText) clip.getItemAt(0).coerceToText(context).toString()
else clip.getItemAt(0).coerceToHtmlText(AztecParser(alignmentRendering, plugins))

val oldHtml = toPlainHtml().replace("<aztec_cursor>", "")
val pastedHtmlText = plugins.filterIsInstance<ITextPastePlugin>().fold(textToPaste) { acc, plugin ->
if (selectedText.isNullOrEmpty()) {
plugin.toHtml(acc)
} else {
plugin.toHtml(selectedText, acc)
val firstItem = clip.getItemAt(0)
val itemToPaste = when {
!firstItem.text.isNullOrEmpty() -> {
val textToPaste = if (asPlainText) clip.getItemAt(0).coerceToText(context).toString()
else clip.getItemAt(0).coerceToHtmlText(AztecParser(alignmentRendering, plugins))
IClipboardPastePlugin.PastedItem.HtmlText(textToPaste)
}
firstItem.uri != null -> {
IClipboardPastePlugin.PastedItem.Url(firstItem.uri)
}
firstItem.intent != null -> {
IClipboardPastePlugin.PastedItem.PastedIntent(firstItem.intent)
}
else -> {
null
}
}
val newHtml = oldHtml.replace(Constants.REPLACEMENT_MARKER_STRING, pastedHtmlText + "<" + AztecCursorSpan.AZTEC_CURSOR_TAG + ">")
if (itemToPaste != null) {
val oldHtml = toPlainHtml().replace("<aztec_cursor>", "")
val pastedHtmlText: String = plugins.filterIsInstance<IClipboardPastePlugin<*>>()
.fold(null as? String?) { acc, plugin ->
plugin.itemToHtml(itemToPaste, acc ?: selectedText?.takeIf { it.isNotBlank() }) ?: acc
} ?: when (itemToPaste) {
is IClipboardPastePlugin.PastedItem.HtmlText -> itemToPaste.text
is IClipboardPastePlugin.PastedItem.Url -> itemToPaste.uri.path
is IClipboardPastePlugin.PastedItem.PastedIntent -> itemToPaste.intent.toString()
}

fromHtml(newHtml, false)
inlineFormatter.joinStyleSpans(0, length())
val newHtml = oldHtml.replace(
Constants.REPLACEMENT_MARKER_STRING,
pastedHtmlText + "<" + AztecCursorSpan.AZTEC_CURSOR_TAG + ">"
)

fromHtml(newHtml, false)
inlineFormatter.joinStyleSpans(0, length())
}
}
contentChangeWatcher.notifyContentChanged()
}
Expand Down Expand Up @@ -1964,7 +1987,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
}

@SuppressLint("InflateParams")
fun showLinkDialog(presetUrl: String = "", presetAnchor: String = "", presetOpenInNewWindow: String = "" ) {
fun showLinkDialog(presetUrl: String = "", presetAnchor: String = "", presetOpenInNewWindow: String = "") {
val urlAndAnchor = linkFormatter.getSelectedUrlWithAnchor()

val url = if (TextUtils.isEmpty(presetUrl)) urlAndAnchor.first else presetUrl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package org.wordpress.aztec.plugins

import android.content.Intent
import android.net.Uri

/**
* Use this plugin in order to override the default item paste behaviour. An example is overriding the paste so that
* you can handle pasted image URLs as images over the selected text.
*/
interface IClipboardPastePlugin<T : IClipboardPastePlugin.PastedItem> : IAztecPlugin {
fun toHtml(pastedItem: T, selectedText: String? = null): String

/**
* This method is called when text is pasted into the editor. If text is selected, the default behaviour
* is to replace the selected text with the pasted item but it can be changed by overriding this method.
* If text is not selected, this returned object of this method is inserted into the text.
* This method should return HTML (plain text is OK if you don't apply any changes to the pasted text).
* @param pastedItem clipboard item pasted over selected text
* @param selectedText currently selected text
* @return html of the result
*/
fun itemToHtml(pastedItem: PastedItem, selectedText: String? = null): String? {
return when {
pastedItem is PastedItem.HtmlText && this is ITextPastePlugin -> this.toHtml(pastedItem, selectedText)
pastedItem is PastedItem.Url && this is IUriPastePlugin -> this.toHtml(pastedItem, selectedText)
pastedItem is PastedItem.PastedIntent && this is IIntentPastePlugin -> this.toHtml(pastedItem, selectedText)
else -> null
}
}

interface ITextPastePlugin : IClipboardPastePlugin<PastedItem.HtmlText> {
/**
* Override this method if you only need to handle the pasted text and not other types. If returned value is
* null, it will be ignored and the default behaviour will take over.
* @param pastedItem pasted text
* @return value of the pasted HTML
*/
override fun toHtml(pastedItem: PastedItem.HtmlText, selectedText: String?): String
}

interface IUriPastePlugin : IClipboardPastePlugin<PastedItem.Url> {
/**
* Override this method to handle pasted URIs. If returned value is null, it will be ignored and the default
* behaviour will take over.
* @param pastedItem pasted URI
* @return HTML representation of an URI
*/
override fun toHtml(pastedItem: PastedItem.Url, selectedText: String?): String
}

interface IIntentPastePlugin : IClipboardPastePlugin<PastedItem.PastedIntent> {
/**
* Override this method to handle pasted intents. If returned value is null, it will be ignored and the default
* behaviour will take over.
* @param pastedItem Pasted intent
* @return HTML representation of an intent
*/
override fun toHtml(pastedItem: PastedItem.PastedIntent, selectedText: String?): String
}

/**
* Pasted items supported by Clipboard
*/
sealed class PastedItem {
data class HtmlText(val text: String) : PastedItem()
data class Url(val uri: Uri) : PastedItem()
data class PastedIntent(val intent: Intent) : PastedItem()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,19 @@ import android.util.Patterns
* This plugin overrides the paste logic of URLs in the AztecText. The purpose is to make sure inserted links are
* treated as HTML links.
*/
class UrlPastePlugin : ITextPastePlugin {
class UrlPastePlugin : IClipboardPastePlugin.ITextPastePlugin {
/**
* If the pasted text is a link, make sure it's wrapped with the `a` tag so that it's rendered as a link.
*/
override fun toHtml(pastedText: String): String {
return if (Patterns.WEB_URL.matcher(pastedText).matches()) {
"<a href=\"$pastedText\">$pastedText</a>"
override fun toHtml(pastedItem: IClipboardPastePlugin.PastedItem.HtmlText, selectedText: String?): String {
return if (Patterns.WEB_URL.matcher(pastedItem.text).matches()) {
if (selectedText != null) {
"<a href=\"${pastedItem.text}\">$selectedText</a>"
} else {
"<a href=\"${pastedItem.text}\">${pastedItem.text}</a>"
}
} else {
pastedText
}
}

/**
* If the pasted text is a link, make sure the selected text is wrapped with `a` tag and not removed.
*/
override fun toHtml(selectedText: String, pastedText: String): String {
return if (Patterns.WEB_URL.matcher(pastedText).matches()) {
"<a href=\"$pastedText\">$selectedText</a>"
} else {
pastedText
pastedItem.text
}
}
}
Expand Down