Skip to content

Commit f0ad7cc

Browse files
committed
Implement backspace soft override
1 parent 2f7e982 commit f0ad7cc

File tree

5 files changed

+211
-1
lines changed

5 files changed

+211
-1
lines changed

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ open class Aztec private constructor(
3636
onMediaDeletedListeners.forEach { it.beforeMediaDeleted(attrs) }
3737
}
3838
}
39+
private var beforeBackSpaceListeners: MutableList<AztecText.BeforeBackSpaceListener> = mutableListOf()
40+
private val beforeBackSpaceListener = object : AztecText.BeforeBackSpaceListener {
41+
override fun shouldOverrideBackSpace(position: Int): Boolean {
42+
return beforeBackSpaceListeners.any { it.shouldOverrideBackSpace(position) }
43+
}
44+
}
3945
private var onVideoInfoRequestedListener: AztecText.OnVideoInfoRequestedListener? = null
4046
private var onLinkTappedListener: AztecText.OnLinkTappedListener? = null
4147
private var isLinkTapEnabled: Boolean = false
@@ -145,6 +151,12 @@ open class Aztec private constructor(
145151
return this
146152
}
147153

154+
fun addBeforeBackSpaceListener(beforeBackSpaceListener: AztecText.BeforeBackSpaceListener): Aztec {
155+
this.beforeBackSpaceListeners.add(beforeBackSpaceListener)
156+
initBeforeBackSpaceListener()
157+
return this
158+
}
159+
148160
fun setOnVideoInfoRequestedListener(onVideoInfoRequestedListener: AztecText.OnVideoInfoRequestedListener): Aztec {
149161
this.onVideoInfoRequestedListener = onVideoInfoRequestedListener
150162
initVideoInfoRequestedListener()
@@ -253,6 +265,10 @@ open class Aztec private constructor(
253265
visualEditor.setOnMediaDeletedListener(onMediaDeletedListener)
254266
}
255267

268+
private fun initBeforeBackSpaceListener() {
269+
visualEditor.setBeforeBackSpaceListener(beforeBackSpaceListener)
270+
}
271+
256272
private fun initVideoInfoRequestedListener() {
257273
if (onVideoInfoRequestedListener != null) {
258274
visualEditor.setOnVideoInfoRequestedListener(onVideoInfoRequestedListener!!)

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
249249
private var onVideoTappedListener: OnVideoTappedListener? = null
250250
private var onAudioTappedListener: OnAudioTappedListener? = null
251251
private var onMediaDeletedListener: OnMediaDeletedListener? = null
252+
private var beforeBackSpaceListener: BeforeBackSpaceListener? = null
252253
private var onVideoInfoRequestedListener: OnVideoInfoRequestedListener? = null
253254
private var onAztecKeyListener: OnAztecKeyListener? = null
254255
private var onVisibilityChangeListener: OnVisibilityChangeListener? = null
@@ -354,6 +355,16 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
354355
fun beforeMediaDeleted(attrs: AztecAttributes) {}
355356
}
356357

358+
/**
359+
* Listens to keyboard events and calls the `shouldOverrideBackSpace` before each backspace event.
360+
*/
361+
interface BeforeBackSpaceListener {
362+
/**
363+
* Return true if you want to not process backspace event in the given position.
364+
*/
365+
fun shouldOverrideBackSpace(position: Int): Boolean
366+
}
367+
357368
interface OnVideoInfoRequestedListener {
358369
fun onVideoInfoRequested(attrs: AztecAttributes)
359370
}
@@ -659,7 +670,7 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
659670
}
660671

661672
override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection {
662-
val baseInputConnection = requireNotNull(super.onCreateInputConnection(outAttrs))
673+
val baseInputConnection = requireNotNull(super.onCreateInputConnection(outAttrs)).wrapWithBackSpaceHandler()
663674
return if (shouldOverridePredictiveTextBehavior()) {
664675
AppLog.d(AppLog.T.EDITOR, "Overriding predictive text behavior on Samsung device with Samsung Keyboard with API 33")
665676
SamsungInputConnection(this, baseInputConnection)
@@ -668,6 +679,26 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
668679
}
669680
}
670681

682+
private fun InputConnection.wrapWithBackSpaceHandler(): InputConnection {
683+
return DeleteOverrideInputConnection(this) { beforeLength, afterLength ->
684+
val triggerDelete = beforeBackSpaceListener?.let { listener ->
685+
if (beforeLength == 1 && afterLength == 0 && selectionStart > 0) {
686+
val isLinebreak = editableText[(selectionStart - 1).coerceAtLeast(0)] == '\n'
687+
val from = if (isLinebreak) {
688+
selectionStart - 2
689+
} else {
690+
selectionStart - 1
691+
}
692+
val isImg = editableText[(from).coerceAtLeast(0)] == Constants.IMG_CHAR
693+
!(isImg && listener.shouldOverrideBackSpace(from))
694+
} else {
695+
true
696+
}
697+
}
698+
triggerDelete ?: true
699+
}
700+
}
701+
671702
private fun shouldOverridePredictiveTextBehavior(): Boolean {
672703
val currentKeyboard = Settings.Secure.getString(context.contentResolver, Settings.Secure.DEFAULT_INPUT_METHOD)
673704
return Build.MANUFACTURER.lowercase(Locale.US) == "samsung" && Build.VERSION.SDK_INT >= 33 &&
@@ -1104,6 +1135,10 @@ open class AztecText : AppCompatEditText, TextWatcher, UnknownHtmlSpan.OnUnknown
11041135
this.onMediaDeletedListener = listener
11051136
}
11061137

1138+
fun setBeforeBackSpaceListener(listener: BeforeBackSpaceListener) {
1139+
this.beforeBackSpaceListener = listener
1140+
}
1141+
11071142
fun setOnVideoInfoRequestedListener(listener: OnVideoInfoRequestedListener) {
11081143
this.onVideoInfoRequestedListener = listener
11091144
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.wordpress.aztec
2+
3+
import android.view.inputmethod.InputConnection
4+
5+
/**
6+
* Wrapper around proprietary Samsung InputConnection. Forwards all the calls to it, except for getExtractedText and
7+
* some custom logic in commitText
8+
*/
9+
class DeleteOverrideInputConnection(
10+
inputConnection: InputConnection,
11+
private val shouldDeleteSurroundingText: (beforeLength: Int, afterLength: Int) -> Boolean
12+
) : InputConnectionWrapper(inputConnection) {
13+
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
14+
return shouldDeleteSurroundingText(beforeLength, afterLength)
15+
&& super.deleteSurroundingText(beforeLength, afterLength)
16+
}
17+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package org.wordpress.aztec
2+
3+
import android.os.Build
4+
import android.os.Bundle
5+
import android.os.Handler
6+
import android.view.KeyEvent
7+
import android.view.inputmethod.CompletionInfo
8+
import android.view.inputmethod.CorrectionInfo
9+
import android.view.inputmethod.ExtractedText
10+
import android.view.inputmethod.ExtractedTextRequest
11+
import android.view.inputmethod.InputConnection
12+
import android.view.inputmethod.InputContentInfo
13+
import androidx.annotation.RequiresApi
14+
15+
/**
16+
* Wrapper around proprietary Samsung InputConnection. Forwards all the calls to it, except for getExtractedText and
17+
* some custom logic in commitText
18+
*/
19+
abstract class InputConnectionWrapper(private val inputConnection: InputConnection) : InputConnection {
20+
override fun beginBatchEdit(): Boolean {
21+
return inputConnection.beginBatchEdit()
22+
}
23+
24+
override fun endBatchEdit(): Boolean {
25+
return inputConnection.endBatchEdit()
26+
}
27+
28+
override fun clearMetaKeyStates(states: Int): Boolean {
29+
return inputConnection.clearMetaKeyStates(states)
30+
}
31+
32+
override fun sendKeyEvent(event: KeyEvent?): Boolean {
33+
return inputConnection.sendKeyEvent(event)
34+
}
35+
36+
override fun commitCompletion(text: CompletionInfo?): Boolean {
37+
return inputConnection.commitCompletion(text)
38+
}
39+
40+
override fun commitCorrection(correctionInfo: CorrectionInfo?): Boolean {
41+
return inputConnection.commitCorrection(correctionInfo)
42+
}
43+
44+
override fun performEditorAction(actionCode: Int): Boolean {
45+
return inputConnection.performEditorAction(actionCode)
46+
}
47+
48+
override fun performContextMenuAction(id: Int): Boolean {
49+
return inputConnection.performContextMenuAction(id)
50+
}
51+
52+
override fun getExtractedText(request: ExtractedTextRequest?, flags: Int): ExtractedText? {
53+
return inputConnection.getExtractedText(request, flags)
54+
}
55+
56+
override fun performPrivateCommand(action: String?, data: Bundle?): Boolean {
57+
return inputConnection.performPrivateCommand(action, data)
58+
}
59+
60+
override fun setComposingText(text: CharSequence?, newCursorPosition: Int): Boolean {
61+
return inputConnection.setComposingText(text, newCursorPosition)
62+
}
63+
64+
override fun commitText(text: CharSequence?, newCursorPosition: Int): Boolean {
65+
return inputConnection.commitText(text, newCursorPosition)
66+
}
67+
68+
@RequiresApi(Build.VERSION_CODES.N_MR1)
69+
override fun commitContent(inputContentInfo: InputContentInfo, flags: Int, opts: Bundle?): Boolean {
70+
return inputConnection.commitContent(inputContentInfo, flags, opts)
71+
}
72+
73+
override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
74+
return inputConnection.deleteSurroundingText(beforeLength, afterLength)
75+
}
76+
77+
override fun requestCursorUpdates(cursorUpdateMode: Int): Boolean {
78+
return inputConnection.requestCursorUpdates(cursorUpdateMode)
79+
}
80+
81+
override fun reportFullscreenMode(enabled: Boolean): Boolean {
82+
return inputConnection.reportFullscreenMode(enabled)
83+
}
84+
85+
override fun setSelection(start: Int, end: Int): Boolean {
86+
return inputConnection.setSelection(start, end)
87+
}
88+
89+
override fun finishComposingText(): Boolean {
90+
return inputConnection.finishComposingText()
91+
}
92+
93+
override fun setComposingRegion(start: Int, end: Int): Boolean {
94+
return inputConnection.setComposingRegion(start, end)
95+
}
96+
97+
override fun deleteSurroundingTextInCodePoints(beforeLength: Int, afterLength: Int): Boolean {
98+
return inputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength)
99+
}
100+
101+
override fun getCursorCapsMode(reqModes: Int): Int {
102+
return inputConnection.getCursorCapsMode(reqModes)
103+
}
104+
105+
override fun getSelectedText(flags: Int): CharSequence? {
106+
return inputConnection.getSelectedText(flags)
107+
}
108+
109+
override fun getTextAfterCursor(length: Int, flags: Int): CharSequence {
110+
return inputConnection.getTextAfterCursor(length, flags)
111+
}
112+
113+
override fun getTextBeforeCursor(length: Int, flags: Int): CharSequence {
114+
return inputConnection.getTextBeforeCursor(length, flags)
115+
}
116+
117+
override fun getHandler(): Handler? {
118+
return inputConnection.handler
119+
}
120+
121+
override fun closeConnection() {
122+
inputConnection.closeConnection()
123+
}
124+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package org.wordpress.aztec.placeholders
2+
3+
import org.wordpress.aztec.AztecText
4+
5+
/**
6+
* This class overrides the backspace event and stops backspace if the previous item is a placeholder span. This is
7+
* useful to show some kind of a dialog that will let the user decide if they want to really remove the placeholder.
8+
*/
9+
class PlaceholderBackspaceListener(private val visualEditor: AztecText, private val predicate: (span: AztecPlaceholderSpan) -> Boolean) : AztecText.BeforeBackSpaceListener {
10+
override fun shouldOverrideBackSpace(position: Int): Boolean {
11+
val editableText = visualEditor.editableText
12+
13+
return editableText.getSpans(position, position + 1, AztecPlaceholderSpan::class.java).any {
14+
predicate(it)
15+
}
16+
}
17+
}
18+

0 commit comments

Comments
 (0)