@openeditor/react-native-prose-editor 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +160 -0
- package/README.md +180 -0
- package/android/build.gradle +69 -0
- package/android/consumer-rules.pro +18 -0
- package/android/src/androidTest/AndroidManifest.xml +8 -0
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +53 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +241 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +338 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +789 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +350 -0
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +5 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.json +2236 -0
- package/android/src/main/assets/editor-icons/MaterialIcons.ttf +0 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +50 -0
- package/android/src/main/java/com/apollohg/editor/EditorAddons.kt +135 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +4527 -0
- package/android/src/main/java/com/apollohg/editor/EditorHeightBehavior.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +999 -0
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +474 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +3137 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +561 -0
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +1413 -0
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +133 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +315 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +2172 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +399 -0
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +482 -0
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +137 -0
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +321 -0
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +4202 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +2581 -0
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +27 -0
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +197 -0
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +84 -0
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +544 -0
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +461 -0
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +2054 -0
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +1367 -0
- package/app.plugin.js +62 -0
- package/dist/EditorTheme.d.ts +113 -0
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +191 -0
- package/dist/EditorToolbar.js +1095 -0
- package/dist/NativeEditorBridge.d.ts +325 -0
- package/dist/NativeEditorBridge.js +933 -0
- package/dist/NativeProseViewer.d.ts +53 -0
- package/dist/NativeProseViewer.js +480 -0
- package/dist/NativeRichTextEditor.d.ts +183 -0
- package/dist/NativeRichTextEditor.js +2574 -0
- package/dist/YjsCollaboration.d.ts +90 -0
- package/dist/YjsCollaboration.js +743 -0
- package/dist/addons.d.ts +100 -0
- package/dist/addons.js +82 -0
- package/dist/heightCache.d.ts +6 -0
- package/dist/heightCache.js +45 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -0
- package/dist/schemas.d.ts +37 -0
- package/dist/schemas.js +263 -0
- package/dist/useNativeEditor.d.ts +42 -0
- package/dist/useNativeEditor.js +124 -0
- package/expo-module.config.json +9 -0
- package/ios/EditorAddons.swift +327 -0
- package/ios/EditorCore.xcframework/Info.plist +44 -0
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/EditorLayoutManager.swift +774 -0
- package/ios/EditorTheme.swift +515 -0
- package/ios/Generated_editor_core.swift +1513 -0
- package/ios/NativeEditorExpoView.swift +3593 -0
- package/ios/NativeEditorModule.swift +624 -0
- package/ios/NativeProseViewerExpoView.swift +276 -0
- package/ios/PositionBridge.swift +558 -0
- package/ios/ReactNativeProseEditor.podspec +49 -0
- package/ios/RenderBridge.swift +1708 -0
- package/ios/RichTextEditorView.swift +6150 -0
- package/ios/Tests/NativePerformanceTests.swift +687 -0
- package/ios/Tests/PositionBridgeTests.swift +706 -0
- package/ios/Tests/RenderBridgeTests.swift +2236 -0
- package/ios/Tests/RichTextEditorViewTests.swift +6548 -0
- package/ios/editor_coreFFI/editor_coreFFI.h +1289 -0
- package/ios/editor_coreFFI/module.modulemap +7 -0
- package/ios/editor_coreFFI.h +904 -0
- package/ios/editor_coreFFI.modulemap +7 -0
- package/package.json +74 -0
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2563 -0
|
@@ -0,0 +1,3137 @@
|
|
|
1
|
+
package com.openeditor.editor
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.ContextWrapper
|
|
6
|
+
import android.graphics.Rect
|
|
7
|
+
import android.graphics.RectF
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
10
|
+
import android.os.SystemClock
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import android.view.Gravity
|
|
13
|
+
import android.view.MotionEvent
|
|
14
|
+
import android.view.View
|
|
15
|
+
import android.view.ViewConfiguration
|
|
16
|
+
import android.view.ViewGroup
|
|
17
|
+
import android.view.ViewTreeObserver
|
|
18
|
+
import android.view.Window
|
|
19
|
+
import android.view.inputmethod.InputMethodManager
|
|
20
|
+
import android.widget.FrameLayout
|
|
21
|
+
import androidx.core.view.ViewCompat
|
|
22
|
+
import androidx.core.view.WindowInsetsCompat
|
|
23
|
+
import expo.modules.kotlin.AppContext
|
|
24
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
25
|
+
import expo.modules.kotlin.views.ExpoView
|
|
26
|
+
import org.json.JSONArray
|
|
27
|
+
import org.json.JSONObject
|
|
28
|
+
import java.lang.ref.WeakReference
|
|
29
|
+
import java.util.concurrent.CountDownLatch
|
|
30
|
+
import java.util.concurrent.TimeUnit
|
|
31
|
+
import java.util.WeakHashMap
|
|
32
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
33
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
34
|
+
import uniffi.editor_core.*
|
|
35
|
+
|
|
36
|
+
private const val DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS = 250L
|
|
37
|
+
private const val OUTSIDE_TAP_GESTURE_CONFIRM_DELAY_MS = 150L
|
|
38
|
+
|
|
39
|
+
internal enum class NativeEditorOutsideTapDecision {
|
|
40
|
+
IGNORE,
|
|
41
|
+
PRESERVE_FOCUS,
|
|
42
|
+
OUTSIDE_EDITOR
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private class WeakNativeEditorExpoView private constructor(
|
|
46
|
+
val view: WeakReference<NativeEditorExpoView?>
|
|
47
|
+
) {
|
|
48
|
+
constructor(view: NativeEditorExpoView) : this(WeakReference(view))
|
|
49
|
+
|
|
50
|
+
companion object {
|
|
51
|
+
fun cleared(): WeakNativeEditorExpoView =
|
|
52
|
+
WeakNativeEditorExpoView(WeakReference<NativeEditorExpoView?>(null))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
internal object NativeEditorViewRegistry {
|
|
57
|
+
private data class CommandPreparationSnapshot(
|
|
58
|
+
val view: NativeEditorExpoView?,
|
|
59
|
+
val isDetached: Boolean,
|
|
60
|
+
val isDestroyed: Boolean
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
private val viewsByEditorId = mutableMapOf<Long, WeakNativeEditorExpoView>()
|
|
64
|
+
private val detachedEditorOwnersByEditorId = mutableMapOf<Long, WeakNativeEditorExpoView>()
|
|
65
|
+
private val destroyedEditorIds = mutableSetOf<Long>()
|
|
66
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
67
|
+
|
|
68
|
+
@Synchronized
|
|
69
|
+
fun markEditorCreated(editorId: Long) {
|
|
70
|
+
if (editorId == 0L) return
|
|
71
|
+
destroyedEditorIds.remove(editorId)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Synchronized
|
|
75
|
+
fun register(editorId: Long, view: NativeEditorExpoView): Boolean {
|
|
76
|
+
if (editorId == 0L) return false
|
|
77
|
+
if (destroyedEditorIds.contains(editorId)) return false
|
|
78
|
+
viewsByEditorId[editorId] = WeakNativeEditorExpoView(view)
|
|
79
|
+
detachedEditorOwnersByEditorId.remove(editorId)
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Synchronized
|
|
84
|
+
fun unregister(
|
|
85
|
+
editorId: Long,
|
|
86
|
+
view: NativeEditorExpoView,
|
|
87
|
+
blockCommandsUntilRegistered: Boolean = false
|
|
88
|
+
) {
|
|
89
|
+
if (editorId == 0L) return
|
|
90
|
+
val registeredView = viewsByEditorId[editorId]?.view?.get()
|
|
91
|
+
if (registeredView === view) {
|
|
92
|
+
viewsByEditorId.remove(editorId)
|
|
93
|
+
}
|
|
94
|
+
if (blockCommandsUntilRegistered) {
|
|
95
|
+
detachedEditorOwnersByEditorId[editorId] = WeakNativeEditorExpoView(view)
|
|
96
|
+
} else {
|
|
97
|
+
val detachedOwner = detachedEditorOwnersByEditorId[editorId]?.view?.get()
|
|
98
|
+
if (registeredView === view || detachedOwner === view) {
|
|
99
|
+
detachedEditorOwnersByEditorId.remove(editorId)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
@Synchronized
|
|
105
|
+
fun isDestroyed(editorId: Long): Boolean = destroyedEditorIds.contains(editorId)
|
|
106
|
+
|
|
107
|
+
@Synchronized
|
|
108
|
+
internal fun forceDetachedOwnerClearedForTesting(editorId: Long) {
|
|
109
|
+
detachedEditorOwnersByEditorId[editorId] = WeakNativeEditorExpoView.cleared()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fun invalidateDestroyedEditor(editorId: Long) {
|
|
113
|
+
if (editorId == 0L) return
|
|
114
|
+
val affectedViews = synchronized(this) {
|
|
115
|
+
destroyedEditorIds.add(editorId)
|
|
116
|
+
val views = listOfNotNull(
|
|
117
|
+
viewsByEditorId.remove(editorId)?.view?.get(),
|
|
118
|
+
detachedEditorOwnersByEditorId.remove(editorId)?.view?.get()
|
|
119
|
+
).distinct()
|
|
120
|
+
views
|
|
121
|
+
}
|
|
122
|
+
if (affectedViews.isEmpty()) return
|
|
123
|
+
val invalidate = Runnable {
|
|
124
|
+
affectedViews.forEach { view ->
|
|
125
|
+
view.handleEditorDestroyed(editorId)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
129
|
+
invalidate.run()
|
|
130
|
+
} else {
|
|
131
|
+
val latch = CountDownLatch(1)
|
|
132
|
+
val posted = mainHandler.post {
|
|
133
|
+
try {
|
|
134
|
+
invalidate.run()
|
|
135
|
+
} finally {
|
|
136
|
+
latch.countDown()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!posted) return
|
|
140
|
+
try {
|
|
141
|
+
latch.await(DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
142
|
+
} catch (_: InterruptedException) {
|
|
143
|
+
Thread.currentThread().interrupt()
|
|
144
|
+
} catch (_: Throwable) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fun prepareForCommandJSON(editorId: Long): String {
|
|
151
|
+
val prepare = {
|
|
152
|
+
val snapshot = synchronized(this) {
|
|
153
|
+
val isDestroyed = destroyedEditorIds.contains(editorId)
|
|
154
|
+
if (isDestroyed) {
|
|
155
|
+
return@synchronized CommandPreparationSnapshot(
|
|
156
|
+
view = null,
|
|
157
|
+
isDetached = false,
|
|
158
|
+
isDestroyed = true
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
val candidate = viewsByEditorId[editorId]?.view?.get()
|
|
162
|
+
if (candidate == null) {
|
|
163
|
+
viewsByEditorId.remove(editorId)
|
|
164
|
+
}
|
|
165
|
+
val detachedOwner = detachedEditorOwnersByEditorId[editorId]?.view?.get()
|
|
166
|
+
val isDetached = if (detachedOwner == null) {
|
|
167
|
+
detachedEditorOwnersByEditorId.remove(editorId)
|
|
168
|
+
false
|
|
169
|
+
} else {
|
|
170
|
+
true
|
|
171
|
+
}
|
|
172
|
+
CommandPreparationSnapshot(
|
|
173
|
+
view = candidate,
|
|
174
|
+
isDetached = isDetached,
|
|
175
|
+
isDestroyed = false
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
snapshot.view?.prepareForEditorCommandJSON()
|
|
179
|
+
?: commandPreparationJSON(
|
|
180
|
+
ready = !snapshot.isDetached && !snapshot.isDestroyed,
|
|
181
|
+
blockedReason = if (snapshot.isDestroyed) {
|
|
182
|
+
"destroyed"
|
|
183
|
+
} else if (snapshot.isDetached) {
|
|
184
|
+
"detached"
|
|
185
|
+
} else {
|
|
186
|
+
null
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
192
|
+
return prepare()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
val result = AtomicReference(commandPreparationJSON(ready = false, blockedReason = "unknown"))
|
|
196
|
+
val state = AtomicInteger(PREFLIGHT_STATE_QUEUED)
|
|
197
|
+
val latch = CountDownLatch(1)
|
|
198
|
+
if (!mainHandler.post {
|
|
199
|
+
try {
|
|
200
|
+
if (state.compareAndSet(PREFLIGHT_STATE_QUEUED, PREFLIGHT_STATE_RUNNING)) {
|
|
201
|
+
result.set(prepare())
|
|
202
|
+
state.set(PREFLIGHT_STATE_DONE)
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
latch.countDown()
|
|
206
|
+
}
|
|
207
|
+
}) {
|
|
208
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
if (!latch.await(DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
|
212
|
+
if (state.compareAndSet(PREFLIGHT_STATE_QUEUED, PREFLIGHT_STATE_CANCELLED)) {
|
|
213
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
214
|
+
}
|
|
215
|
+
if (state.get() == PREFLIGHT_STATE_RUNNING) {
|
|
216
|
+
latch.await()
|
|
217
|
+
return result.get()
|
|
218
|
+
}
|
|
219
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
220
|
+
}
|
|
221
|
+
} catch (_: InterruptedException) {
|
|
222
|
+
var interrupted = true
|
|
223
|
+
if (state.compareAndSet(PREFLIGHT_STATE_QUEUED, PREFLIGHT_STATE_CANCELLED)) {
|
|
224
|
+
Thread.currentThread().interrupt()
|
|
225
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
226
|
+
}
|
|
227
|
+
while (state.get() == PREFLIGHT_STATE_RUNNING) {
|
|
228
|
+
try {
|
|
229
|
+
latch.await()
|
|
230
|
+
break
|
|
231
|
+
} catch (_: InterruptedException) {
|
|
232
|
+
interrupted = true
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (interrupted) {
|
|
236
|
+
Thread.currentThread().interrupt()
|
|
237
|
+
}
|
|
238
|
+
return if (state.get() == PREFLIGHT_STATE_DONE) {
|
|
239
|
+
result.get()
|
|
240
|
+
} else {
|
|
241
|
+
commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result.get()
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
fun commandPreparationJSON(
|
|
248
|
+
ready: Boolean,
|
|
249
|
+
updateJSON: String? = null,
|
|
250
|
+
blockedReason: String? = null
|
|
251
|
+
): String {
|
|
252
|
+
return JSONObject().apply {
|
|
253
|
+
put("ready", ready)
|
|
254
|
+
if (updateJSON != null) {
|
|
255
|
+
put("updateJSON", updateJSON)
|
|
256
|
+
}
|
|
257
|
+
if (!ready && blockedReason != null) {
|
|
258
|
+
put("blockedReason", blockedReason)
|
|
259
|
+
}
|
|
260
|
+
}.toString()
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private const val PREFLIGHT_STATE_QUEUED = 0
|
|
264
|
+
private const val PREFLIGHT_STATE_RUNNING = 1
|
|
265
|
+
private const val PREFLIGHT_STATE_CANCELLED = 2
|
|
266
|
+
private const val PREFLIGHT_STATE_DONE = 3
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private object NativeEditorOutsideTapDispatcher {
|
|
270
|
+
private val dispatchers = WeakHashMap<Window, OutsideTapTouchDispatcher>()
|
|
271
|
+
|
|
272
|
+
fun register(window: Window, view: NativeEditorExpoView): Boolean {
|
|
273
|
+
val host = contentRootFor(window)
|
|
274
|
+
if (host == null) {
|
|
275
|
+
view.traceOutsideTap("register skipped missing content root")
|
|
276
|
+
return false
|
|
277
|
+
}
|
|
278
|
+
val previousDispatcher = dispatchers[window]
|
|
279
|
+
val dispatcher = if (previousDispatcher?.host === host) {
|
|
280
|
+
previousDispatcher
|
|
281
|
+
} else {
|
|
282
|
+
OutsideTapTouchDispatcher(host).also { nextDispatcher ->
|
|
283
|
+
previousDispatcher?.transferViewsTo(nextDispatcher)
|
|
284
|
+
previousDispatcher?.detach()
|
|
285
|
+
dispatchers[window] = nextDispatcher
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
dispatchers[window] = dispatcher
|
|
289
|
+
dispatcher.add(view)
|
|
290
|
+
view.traceOutsideTap(
|
|
291
|
+
"register overlayAttached=${dispatcher.isAttached()} " +
|
|
292
|
+
"host=${host.javaClass.name} " +
|
|
293
|
+
"activeViews=${dispatcher.liveViews().size}"
|
|
294
|
+
)
|
|
295
|
+
return dispatcher.isAttached()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
fun unregister(window: Window, view: NativeEditorExpoView) {
|
|
299
|
+
val dispatcher = dispatchers[window] ?: return
|
|
300
|
+
if (!dispatcher.remove(view)) return
|
|
301
|
+
dispatcher.detach()
|
|
302
|
+
dispatchers.remove(window)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private fun contentRootFor(window: Window): ViewGroup? {
|
|
306
|
+
val decorView = window.decorView
|
|
307
|
+
return decorView.findViewById<View>(android.R.id.content) as? ViewGroup
|
|
308
|
+
?: decorView as? ViewGroup
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private class OutsideTapTouchDispatcher(
|
|
312
|
+
val host: ViewGroup
|
|
313
|
+
) : View.OnTouchListener {
|
|
314
|
+
private data class OutsideTapCandidate(
|
|
315
|
+
val view: WeakReference<NativeEditorExpoView>,
|
|
316
|
+
val downRawX: Float,
|
|
317
|
+
val downRawY: Float,
|
|
318
|
+
val editorRectOnDown: Rect?,
|
|
319
|
+
val confirm: Runnable
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
private val views = mutableListOf<WeakReference<NativeEditorExpoView>>()
|
|
323
|
+
private val pendingOutsideTapCandidates = mutableListOf<OutsideTapCandidate>()
|
|
324
|
+
private val touchSlopPx = ViewConfiguration.get(host.context).scaledTouchSlop
|
|
325
|
+
private val scrollChangedListener = ViewTreeObserver.OnScrollChangedListener {
|
|
326
|
+
cancelPendingOutsideTapCandidates("scroll")
|
|
327
|
+
}
|
|
328
|
+
private var scrollListenerTreeObserver: ViewTreeObserver? = null
|
|
329
|
+
private var attachmentGeneration = 0
|
|
330
|
+
private val observerView = View(host.context).apply {
|
|
331
|
+
isClickable = false
|
|
332
|
+
isFocusable = false
|
|
333
|
+
isFocusableInTouchMode = false
|
|
334
|
+
importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
init {
|
|
338
|
+
observerView.setOnTouchListener(this)
|
|
339
|
+
attach()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fun add(view: NativeEditorExpoView) {
|
|
343
|
+
prune()
|
|
344
|
+
attach()
|
|
345
|
+
if (views.any { it.get() === view }) return
|
|
346
|
+
views.add(WeakReference(view))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fun liveViews(): List<NativeEditorExpoView> {
|
|
350
|
+
prune()
|
|
351
|
+
return views.mapNotNull { it.get() }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fun transferViewsTo(target: OutsideTapTouchDispatcher) {
|
|
355
|
+
liveViews().forEach { target.add(it) }
|
|
356
|
+
views.clear()
|
|
357
|
+
cancelPendingOutsideTapCandidates("transfer")
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
fun remove(view: NativeEditorExpoView): Boolean {
|
|
361
|
+
cancelPendingOutsideTapCandidatesFor(view, "remove view")
|
|
362
|
+
views.removeAll { it.get()?.let { candidate -> candidate === view } != false }
|
|
363
|
+
return views.isEmpty()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
override fun onTouch(view: View, event: MotionEvent): Boolean {
|
|
367
|
+
val activeViews = liveViews()
|
|
368
|
+
if (activeViews.isEmpty()) {
|
|
369
|
+
return false
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
when (event.actionMasked) {
|
|
373
|
+
MotionEvent.ACTION_DOWN -> handleActionDown(activeViews, event)
|
|
374
|
+
MotionEvent.ACTION_MOVE -> {
|
|
375
|
+
if (hasMovedBeyondTapSlop(event)) {
|
|
376
|
+
cancelPendingOutsideTapCandidates("move")
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
MotionEvent.ACTION_UP -> {
|
|
380
|
+
if (hasMovedBeyondTapSlop(event)) {
|
|
381
|
+
cancelPendingOutsideTapCandidates("up moved")
|
|
382
|
+
} else {
|
|
383
|
+
confirmPendingOutsideTapCandidates("up")
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
MotionEvent.ACTION_CANCEL -> cancelPendingOutsideTapCandidates("cancel")
|
|
387
|
+
}
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private fun handleActionDown(activeViews: List<NativeEditorExpoView>, event: MotionEvent) {
|
|
392
|
+
cancelPendingOutsideTapCandidates("new down")
|
|
393
|
+
val decisions = activeViews.map { view ->
|
|
394
|
+
view to view.prepareOutsideTapDecisionForWindowEvent(event)
|
|
395
|
+
}
|
|
396
|
+
decisions.forEach { (view, decision) ->
|
|
397
|
+
view.traceOutsideTap(
|
|
398
|
+
"dispatch overlay action=${event.action} raw=${event.rawX.toInt()},${event.rawY.toInt()} decision=$decision"
|
|
399
|
+
)
|
|
400
|
+
if (decision == NativeEditorOutsideTapDecision.OUTSIDE_EDITOR) {
|
|
401
|
+
scheduleOutsideTapCandidate(view, event)
|
|
402
|
+
} else {
|
|
403
|
+
view.handleOutsideTapDecisionFromWindowDispatcher(decision)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
private fun scheduleOutsideTapCandidate(view: NativeEditorExpoView, event: MotionEvent) {
|
|
409
|
+
val editorRect = Rect()
|
|
410
|
+
val editorRectOnDown = if (
|
|
411
|
+
view.richTextView.editorEditText.getGlobalVisibleRect(editorRect) &&
|
|
412
|
+
!editorRect.isEmpty
|
|
413
|
+
) {
|
|
414
|
+
editorRect
|
|
415
|
+
} else {
|
|
416
|
+
null
|
|
417
|
+
}
|
|
418
|
+
val viewRef = WeakReference(view)
|
|
419
|
+
lateinit var candidate: OutsideTapCandidate
|
|
420
|
+
val confirm = Runnable {
|
|
421
|
+
confirmOutsideTapCandidate(candidate, "delay")
|
|
422
|
+
}
|
|
423
|
+
candidate = OutsideTapCandidate(
|
|
424
|
+
view = viewRef,
|
|
425
|
+
downRawX = event.rawX,
|
|
426
|
+
downRawY = event.rawY,
|
|
427
|
+
editorRectOnDown = editorRectOnDown,
|
|
428
|
+
confirm = confirm
|
|
429
|
+
)
|
|
430
|
+
pendingOutsideTapCandidates.add(candidate)
|
|
431
|
+
ensureScrollListener()
|
|
432
|
+
view.traceOutsideTap("candidate outside tap")
|
|
433
|
+
observerView.postDelayed(confirm, OUTSIDE_TAP_GESTURE_CONFIRM_DELAY_MS)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private fun confirmPendingOutsideTapCandidates(reason: String) {
|
|
437
|
+
val candidates = pendingOutsideTapCandidates.toList()
|
|
438
|
+
candidates.forEach { candidate ->
|
|
439
|
+
confirmOutsideTapCandidate(candidate, reason)
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun confirmOutsideTapCandidate(candidate: OutsideTapCandidate, reason: String) {
|
|
444
|
+
if (!pendingOutsideTapCandidates.remove(candidate)) return
|
|
445
|
+
removeScrollListenerIfIdle()
|
|
446
|
+
observerView.removeCallbacks(candidate.confirm)
|
|
447
|
+
val view = candidate.view.get() ?: return
|
|
448
|
+
if (editorMovedBeyondTapSlop(view, candidate)) {
|
|
449
|
+
view.traceOutsideTap("cancel outside tap candidate reason=$reason moved")
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
view.traceOutsideTap("confirm outside tap candidate reason=$reason")
|
|
453
|
+
view.handleOutsideTapDecisionFromWindowDispatcher(NativeEditorOutsideTapDecision.OUTSIDE_EDITOR)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private fun hasMovedBeyondTapSlop(event: MotionEvent): Boolean =
|
|
457
|
+
pendingOutsideTapCandidates.any { candidate ->
|
|
458
|
+
val dx = event.rawX - candidate.downRawX
|
|
459
|
+
val dy = event.rawY - candidate.downRawY
|
|
460
|
+
dx * dx + dy * dy > touchSlopPx * touchSlopPx
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private fun editorMovedBeyondTapSlop(
|
|
464
|
+
view: NativeEditorExpoView,
|
|
465
|
+
candidate: OutsideTapCandidate
|
|
466
|
+
): Boolean {
|
|
467
|
+
val editorRectOnDown = candidate.editorRectOnDown ?: return false
|
|
468
|
+
val currentRect = Rect()
|
|
469
|
+
if (!view.richTextView.editorEditText.getGlobalVisibleRect(currentRect)) {
|
|
470
|
+
return true
|
|
471
|
+
}
|
|
472
|
+
val dx = currentRect.left - editorRectOnDown.left
|
|
473
|
+
val dy = currentRect.top - editorRectOnDown.top
|
|
474
|
+
return dx * dx + dy * dy > touchSlopPx * touchSlopPx
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private fun cancelPendingOutsideTapCandidatesFor(view: NativeEditorExpoView, reason: String) {
|
|
478
|
+
val candidates = pendingOutsideTapCandidates.toList()
|
|
479
|
+
candidates.forEach { candidate ->
|
|
480
|
+
if (candidate.view.get() === view) {
|
|
481
|
+
pendingOutsideTapCandidates.remove(candidate)
|
|
482
|
+
observerView.removeCallbacks(candidate.confirm)
|
|
483
|
+
view.traceOutsideTap("cancel outside tap candidate reason=$reason")
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
removeScrollListenerIfIdle()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private fun cancelPendingOutsideTapCandidates(reason: String) {
|
|
490
|
+
val candidates = pendingOutsideTapCandidates.toList()
|
|
491
|
+
pendingOutsideTapCandidates.clear()
|
|
492
|
+
removeScrollListener()
|
|
493
|
+
candidates.forEach { candidate ->
|
|
494
|
+
observerView.removeCallbacks(candidate.confirm)
|
|
495
|
+
candidate.view.get()?.traceOutsideTap("cancel outside tap candidate reason=$reason")
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private fun ensureScrollListener() {
|
|
500
|
+
val activeObserver = scrollListenerTreeObserver
|
|
501
|
+
if (activeObserver?.isAlive == true && activeObserver === host.viewTreeObserver) {
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
removeScrollListener()
|
|
505
|
+
val nextObserver = host.viewTreeObserver
|
|
506
|
+
if (nextObserver.isAlive) {
|
|
507
|
+
nextObserver.addOnScrollChangedListener(scrollChangedListener)
|
|
508
|
+
scrollListenerTreeObserver = nextObserver
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private fun removeScrollListenerIfIdle() {
|
|
513
|
+
if (pendingOutsideTapCandidates.isEmpty()) {
|
|
514
|
+
removeScrollListener()
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private fun removeScrollListener() {
|
|
519
|
+
val observer = scrollListenerTreeObserver
|
|
520
|
+
if (observer?.isAlive == true) {
|
|
521
|
+
observer.removeOnScrollChangedListener(scrollChangedListener)
|
|
522
|
+
}
|
|
523
|
+
scrollListenerTreeObserver = null
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
fun isAttached(): Boolean = observerView.parent === host
|
|
527
|
+
|
|
528
|
+
fun detach() {
|
|
529
|
+
attachmentGeneration += 1
|
|
530
|
+
val generation = attachmentGeneration
|
|
531
|
+
cancelPendingOutsideTapCandidates("detach")
|
|
532
|
+
val parent = observerView.parent as? ViewGroup ?: return
|
|
533
|
+
observerView.visibility = View.GONE
|
|
534
|
+
parent.post {
|
|
535
|
+
if (generation != attachmentGeneration) return@post
|
|
536
|
+
if (observerView.parent === parent) {
|
|
537
|
+
parent.removeView(observerView)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private fun attach() {
|
|
543
|
+
attachmentGeneration += 1
|
|
544
|
+
observerView.visibility = View.VISIBLE
|
|
545
|
+
val parent = observerView.parent as? ViewGroup
|
|
546
|
+
if (parent !== host) {
|
|
547
|
+
if (parent != null) {
|
|
548
|
+
parent.removeView(observerView)
|
|
549
|
+
}
|
|
550
|
+
host.addView(
|
|
551
|
+
observerView,
|
|
552
|
+
ViewGroup.LayoutParams(
|
|
553
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
554
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
555
|
+
)
|
|
556
|
+
)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (host.indexOfChild(observerView) != host.childCount - 1) {
|
|
560
|
+
val generation = attachmentGeneration
|
|
561
|
+
host.post {
|
|
562
|
+
if (generation != attachmentGeneration) return@post
|
|
563
|
+
if (observerView.parent === host && host.indexOfChild(observerView) != host.childCount - 1) {
|
|
564
|
+
observerView.bringToFront()
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private fun prune() {
|
|
571
|
+
views.removeAll { it.get() == null }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Expo Modules wrapper view that hosts a [RichTextEditorView] and bridges
|
|
578
|
+
* editor events to React Native via [EventDispatcher].
|
|
579
|
+
*
|
|
580
|
+
* Registered as the native view component in [NativeEditorModule].
|
|
581
|
+
*/
|
|
582
|
+
class NativeEditorExpoView(
|
|
583
|
+
context: Context,
|
|
584
|
+
appContext: AppContext
|
|
585
|
+
) : ExpoView(context, appContext), EditorEditText.EditorListener {
|
|
586
|
+
|
|
587
|
+
private enum class ToolbarPlacement {
|
|
588
|
+
KEYBOARD,
|
|
589
|
+
INLINE;
|
|
590
|
+
|
|
591
|
+
companion object {
|
|
592
|
+
fun fromRaw(raw: String?): ToolbarPlacement =
|
|
593
|
+
if (raw == "inline") INLINE else KEYBOARD
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private sealed class PendingNativeAction {
|
|
598
|
+
data class ToolbarItemPress(val item: NativeToolbarItem) : PendingNativeAction()
|
|
599
|
+
data class MentionSuggestionSelect(val suggestion: NativeMentionSuggestion) : PendingNativeAction()
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private data class PendingNativeActionScope(
|
|
603
|
+
val editorId: Long,
|
|
604
|
+
val documentVersion: Int?,
|
|
605
|
+
val allowedDocumentVersion: Int?,
|
|
606
|
+
val hadFocus: Boolean,
|
|
607
|
+
val hadVisibleToolbar: Boolean,
|
|
608
|
+
val selectionAnchor: Int?,
|
|
609
|
+
val selectionHead: Int?,
|
|
610
|
+
val mentionAnchor: Int? = null,
|
|
611
|
+
val mentionHead: Int? = null,
|
|
612
|
+
val mentionQuery: String? = null
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
private data class PendingEditorUpdateEvent(
|
|
616
|
+
val editorId: Long,
|
|
617
|
+
val updateJSON: String
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
val richTextView: RichTextEditorView = RichTextEditorView(context)
|
|
621
|
+
private val keyboardToolbarView = EditorKeyboardToolbarView(context)
|
|
622
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
623
|
+
|
|
624
|
+
private val onEditorUpdate by EventDispatcher<Map<String, Any>>()
|
|
625
|
+
private val onSelectionChange by EventDispatcher<Map<String, Any>>()
|
|
626
|
+
private val onFocusChange by EventDispatcher<Map<String, Any>>()
|
|
627
|
+
private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
|
|
628
|
+
private val onEditorReady by EventDispatcher<Map<String, Any>>()
|
|
629
|
+
@Suppress("unused")
|
|
630
|
+
private val onToolbarAction by EventDispatcher<Map<String, Any>>()
|
|
631
|
+
@Suppress("unused")
|
|
632
|
+
private val onAddonEvent by EventDispatcher<Map<String, Any>>()
|
|
633
|
+
|
|
634
|
+
/** Guard flag: when true, editor updates originated from JS and should not echo back. */
|
|
635
|
+
var isApplyingJSUpdate = false
|
|
636
|
+
internal var blockEditorUpdatePreflightForTesting = false
|
|
637
|
+
internal var blockThemePreflightForTesting = false
|
|
638
|
+
internal var onToolbarActionForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
639
|
+
internal var onAddonEventForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
640
|
+
internal var onSelectionChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
641
|
+
internal var onFocusChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
642
|
+
internal var onContentHeightChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
643
|
+
internal var onEditorUpdateForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
644
|
+
internal var onEditorReadyForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
645
|
+
internal var onOutsideTapTraceForTesting: ((String) -> Unit)? = null
|
|
646
|
+
internal var onRefreshToolbarStateFromEditorSelectionForTesting: (() -> String?)? = null
|
|
647
|
+
internal var onBeforePrepareForEditorCommandForTesting: (() -> Unit)? = null
|
|
648
|
+
private var isAttachedToNativeWindow = false
|
|
649
|
+
private var didApplyAutoFocus = false
|
|
650
|
+
private var heightBehavior = EditorHeightBehavior.FIXED
|
|
651
|
+
private var lastEmittedContentHeight = 0
|
|
652
|
+
private var lastEmittedContentHeightEditorId: Long? = null
|
|
653
|
+
private var outsideTapWindow: Window? = null
|
|
654
|
+
private var pendingOutsideTapHandlerInstallRetry: Runnable? = null
|
|
655
|
+
private var toolbarFramesInWindow: List<RectF> = emptyList()
|
|
656
|
+
private var lastToolbarTouchUptimeMs: Long? = null
|
|
657
|
+
private var editorFocusedForOutsideTapOverrideForTesting: Boolean? = null
|
|
658
|
+
private var pendingOutsideTapBlur: Runnable? = null
|
|
659
|
+
private var pendingKeyboardDismiss: Runnable? = null
|
|
660
|
+
private var pendingToolbarRefocus: Runnable? = null
|
|
661
|
+
private var pendingToolbarRefocusEditorId: Long? = null
|
|
662
|
+
private var pendingToolbarRefocusGeneration = 0
|
|
663
|
+
private var pendingKeyboardToolbarDetachGeneration = 0
|
|
664
|
+
private var autoFocusRequested = false
|
|
665
|
+
private var addons = NativeEditorAddons(null)
|
|
666
|
+
private var mentionQueryState: MentionQueryState? = null
|
|
667
|
+
private var lastMentionEventJson: String? = null
|
|
668
|
+
private var lastMentionEventEditorId: Long? = null
|
|
669
|
+
private var lastThemeJson: String? = null
|
|
670
|
+
private var pendingThemeJson: String? = null
|
|
671
|
+
private var hasPendingTheme = false
|
|
672
|
+
private var pendingThemeRetryScheduled = false
|
|
673
|
+
private var pendingThemeRetryEditorId: Long? = null
|
|
674
|
+
private var pendingThemeRetryGeneration = 0
|
|
675
|
+
private var pendingThemeRetryAttempts = 0
|
|
676
|
+
private var lastAddonsJson: String? = null
|
|
677
|
+
private var lastRemoteSelectionsJson: String? = null
|
|
678
|
+
private var lastToolbarItemsJson: String? = null
|
|
679
|
+
private var lastToolbarFrameJson: String? = null
|
|
680
|
+
private var lastDocumentVersion: Int? = null
|
|
681
|
+
private var toolbarState = NativeToolbarState.empty
|
|
682
|
+
private var showsToolbar = true
|
|
683
|
+
private var toolbarPlacement = ToolbarPlacement.KEYBOARD
|
|
684
|
+
private var currentImeBottom = 0
|
|
685
|
+
private var pendingEditorUpdateJson: String? = null
|
|
686
|
+
private var pendingEditorUpdateEditorId: Long? = null
|
|
687
|
+
private var pendingEditorUpdateRevision = 0
|
|
688
|
+
private var appliedEditorUpdateRevision = 0
|
|
689
|
+
private var pendingEditorResetUpdateJson: String? = null
|
|
690
|
+
private var pendingEditorResetUpdateEditorId: Long? = null
|
|
691
|
+
private var pendingEditorResetUpdateRevision = 0
|
|
692
|
+
private var appliedEditorResetUpdateRevision = 0
|
|
693
|
+
private var lastEditorResetUpdateJsonProp: String? = null
|
|
694
|
+
private var lastEditorResetUpdateEditorIdProp: Long? = null
|
|
695
|
+
private var pendingEditorUpdateRetryScheduled = false
|
|
696
|
+
private var pendingEditorUpdateRetryEditorId: Long? = null
|
|
697
|
+
private var pendingEditorUpdateRetryGeneration = 0
|
|
698
|
+
private var pendingEditorUpdateRetryAttempts = 0
|
|
699
|
+
private var pendingEditorUpdateForcedRecoveryAttempted = false
|
|
700
|
+
private var pendingViewCommandUpdateJson: String? = null
|
|
701
|
+
private var pendingViewCommandUpdateEditorId: Long? = null
|
|
702
|
+
private var pendingViewCommandUpdateRetryScheduled = false
|
|
703
|
+
private var pendingViewCommandUpdateRetryGeneration = 0
|
|
704
|
+
private var pendingViewCommandUpdateRetryAttempts = 0
|
|
705
|
+
private var pendingPreflightWakeScheduled = false
|
|
706
|
+
private var pendingPreflightWakeGeneration = 0
|
|
707
|
+
private var pendingBlurRetry: Runnable? = null
|
|
708
|
+
private var pendingBlurRetryEditorId: Long? = null
|
|
709
|
+
private var pendingBlurRetryGeneration = 0
|
|
710
|
+
private var pendingBlurRetryAttempts = 0
|
|
711
|
+
private var pendingDetachPreflightRetryScheduled = false
|
|
712
|
+
private var pendingDetachPreflightRetryEditorId: Long? = null
|
|
713
|
+
private var pendingDetachPreflightRetryGeneration = 0
|
|
714
|
+
private var pendingDetachPreflightRetryAttempts = 0
|
|
715
|
+
private var pendingNativeAction: PendingNativeAction? = null
|
|
716
|
+
private var pendingNativeActionScope: PendingNativeActionScope? = null
|
|
717
|
+
private var pendingNativeActionRetryScheduled = false
|
|
718
|
+
private var pendingNativeActionRetryEditorId: Long? = null
|
|
719
|
+
private var pendingNativeActionRetryGeneration = 0
|
|
720
|
+
private var pendingNativeActionRetryAttempts = 0
|
|
721
|
+
private var lastReadyEditorId: Long? = null
|
|
722
|
+
private val pendingEditorUpdateEvents = java.util.ArrayDeque<PendingEditorUpdateEvent>()
|
|
723
|
+
private var pendingEditorUpdateDispatchGeneration = 0
|
|
724
|
+
private var pendingEditorUpdateDispatchScheduled = false
|
|
725
|
+
|
|
726
|
+
init {
|
|
727
|
+
addView(richTextView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
728
|
+
richTextView.editorEditText.editorListener = this
|
|
729
|
+
richTextView.onBeforeDetachedFromWindow = {
|
|
730
|
+
prepareForDetachFromWindow()
|
|
731
|
+
}
|
|
732
|
+
keyboardToolbarView.onPressItem = { item ->
|
|
733
|
+
handleToolbarItemPress(item)
|
|
734
|
+
}
|
|
735
|
+
keyboardToolbarView.onSelectMentionSuggestion = { suggestion ->
|
|
736
|
+
insertMentionSuggestion(suggestion)
|
|
737
|
+
}
|
|
738
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
739
|
+
ViewCompat.setOnApplyWindowInsetsListener(keyboardToolbarView) { _, insets ->
|
|
740
|
+
currentImeBottom = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
741
|
+
updateKeyboardToolbarLayout()
|
|
742
|
+
updateAttachedKeyboardToolbarForInsets()
|
|
743
|
+
insets
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Observe EditText focus changes.
|
|
747
|
+
richTextView.editorEditText.setOnFocusChangeListener { _, hasFocus ->
|
|
748
|
+
if (hasFocus) {
|
|
749
|
+
cancelPendingToolbarRefocus()
|
|
750
|
+
installOutsideTapBlurHandlerIfNeeded()
|
|
751
|
+
scheduleOutsideTapBlurHandlerInstallRetry()
|
|
752
|
+
refreshMentionQuery()
|
|
753
|
+
} else {
|
|
754
|
+
if (consumeToolbarFocusPreservationForBlur()) {
|
|
755
|
+
scheduleToolbarRefocus()
|
|
756
|
+
return@setOnFocusChangeListener
|
|
757
|
+
}
|
|
758
|
+
uninstallOutsideTapBlurHandler()
|
|
759
|
+
clearMentionQueryState()
|
|
760
|
+
clearPendingNativeActionRetry()
|
|
761
|
+
}
|
|
762
|
+
updateKeyboardToolbarVisibility()
|
|
763
|
+
val event = mapOf<String, Any>(
|
|
764
|
+
"isFocused" to hasFocus,
|
|
765
|
+
"editorId" to richTextView.editorId
|
|
766
|
+
)
|
|
767
|
+
onFocusChangeForTesting?.invoke(event) ?: onFocusChange(event)
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
fun setEditorId(id: Long) {
|
|
772
|
+
if (id != 0L && NativeEditorViewRegistry.isDestroyed(id)) {
|
|
773
|
+
setEditorId(0L)
|
|
774
|
+
return
|
|
775
|
+
}
|
|
776
|
+
val previousEditorId = richTextView.editorId
|
|
777
|
+
if (previousEditorId != id) {
|
|
778
|
+
invalidateAutoGrowContentHeightEmission()
|
|
779
|
+
}
|
|
780
|
+
if (previousEditorId == id && richTextView.editorEditText.editorId == id) {
|
|
781
|
+
if (id != 0L && isAttachedToNativeWindow) {
|
|
782
|
+
if (!NativeEditorViewRegistry.register(id, this)) {
|
|
783
|
+
handleEditorDestroyed(id)
|
|
784
|
+
return
|
|
785
|
+
}
|
|
786
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
787
|
+
applyPendingEditorUpdateIfNeeded()
|
|
788
|
+
applyPendingThemeIfNeeded()
|
|
789
|
+
refreshReadyStateIfSettled()
|
|
790
|
+
applyAutoFocusIfNeeded()
|
|
791
|
+
} else if (id != 0L) {
|
|
792
|
+
NativeEditorViewRegistry.unregister(
|
|
793
|
+
id,
|
|
794
|
+
this,
|
|
795
|
+
blockCommandsUntilRegistered = true
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
return
|
|
799
|
+
}
|
|
800
|
+
if (previousEditorId != id) {
|
|
801
|
+
NativeEditorViewRegistry.unregister(previousEditorId, this)
|
|
802
|
+
lastDocumentVersion = null
|
|
803
|
+
cancelPendingToolbarRefocus()
|
|
804
|
+
cancelPendingEditorUpdateRetry()
|
|
805
|
+
if (pendingEditorUpdateEditorId != null && pendingEditorUpdateEditorId != id) {
|
|
806
|
+
clearPendingEditorUpdateState()
|
|
807
|
+
}
|
|
808
|
+
if (pendingEditorResetUpdateEditorId != null && pendingEditorResetUpdateEditorId != id) {
|
|
809
|
+
clearPendingEditorResetUpdateState()
|
|
810
|
+
}
|
|
811
|
+
appliedEditorUpdateRevision = 0
|
|
812
|
+
appliedEditorResetUpdateRevision = 0
|
|
813
|
+
clearPendingViewCommandUpdateRetry()
|
|
814
|
+
cancelPendingThemeRetry()
|
|
815
|
+
if (hasPendingTheme) {
|
|
816
|
+
pendingThemeRetryEditorId = id
|
|
817
|
+
}
|
|
818
|
+
cancelPendingBlurRetry()
|
|
819
|
+
clearPendingNativeActionRetry()
|
|
820
|
+
clearMentionQueryState(resetLastEvent = true)
|
|
821
|
+
lastReadyEditorId = null
|
|
822
|
+
}
|
|
823
|
+
if (!isAttachedToNativeWindow) {
|
|
824
|
+
richTextView.setEditorIdWhileDetached(id)
|
|
825
|
+
if (id != 0L) {
|
|
826
|
+
NativeEditorViewRegistry.unregister(
|
|
827
|
+
id,
|
|
828
|
+
this,
|
|
829
|
+
blockCommandsUntilRegistered = true
|
|
830
|
+
)
|
|
831
|
+
} else {
|
|
832
|
+
toolbarState = NativeToolbarState.empty
|
|
833
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
834
|
+
}
|
|
835
|
+
return
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (hasPendingEditorResetUpdateForEditor(id) || hasPendingEditorUpdateForEditor(id)) {
|
|
839
|
+
richTextView.setEditorIdWhileDetached(id)
|
|
840
|
+
richTextView.rebindEditorIfNeeded(notifyListener = false)
|
|
841
|
+
} else {
|
|
842
|
+
richTextView.editorId = id
|
|
843
|
+
}
|
|
844
|
+
if (id != 0L) {
|
|
845
|
+
if (!NativeEditorViewRegistry.register(id, this)) {
|
|
846
|
+
handleEditorDestroyed(id)
|
|
847
|
+
return
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
toolbarState = NativeToolbarState.empty
|
|
851
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
852
|
+
}
|
|
853
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
854
|
+
applyPendingEditorUpdateIfNeeded()
|
|
855
|
+
applyPendingThemeIfNeeded()
|
|
856
|
+
refreshReadyStateIfSettled()
|
|
857
|
+
applyAutoFocusIfNeeded()
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
fun setThemeJson(themeJson: String?) {
|
|
861
|
+
if (lastThemeJson == themeJson && !hasPendingTheme) return
|
|
862
|
+
pendingThemeJson = themeJson
|
|
863
|
+
hasPendingTheme = true
|
|
864
|
+
pendingThemeRetryEditorId = richTextView.editorId
|
|
865
|
+
pendingThemeRetryAttempts = 0
|
|
866
|
+
applyPendingThemeIfNeeded()
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
private fun applyThemeJson(themeJson: String?) {
|
|
870
|
+
if (lastThemeJson == themeJson) return
|
|
871
|
+
lastThemeJson = themeJson
|
|
872
|
+
val theme = EditorTheme.fromJson(themeJson)
|
|
873
|
+
richTextView.applyTheme(theme)
|
|
874
|
+
keyboardToolbarView.applyTheme(theme?.toolbar)
|
|
875
|
+
keyboardToolbarView.applyMentionTheme(theme?.mentions ?: addons.mentions?.theme)
|
|
876
|
+
keyboardToolbarView.requestLayout()
|
|
877
|
+
updateKeyboardToolbarLayout()
|
|
878
|
+
updateEditorViewportInset(forceMeasureToolbar = true)
|
|
879
|
+
post {
|
|
880
|
+
updateKeyboardToolbarLayout()
|
|
881
|
+
updateEditorViewportInset(forceMeasureToolbar = true)
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
fun setHeightBehavior(rawHeightBehavior: String) {
|
|
886
|
+
val nextBehavior = EditorHeightBehavior.fromRaw(rawHeightBehavior)
|
|
887
|
+
if (heightBehavior == nextBehavior) return
|
|
888
|
+
heightBehavior = nextBehavior
|
|
889
|
+
if (nextBehavior != EditorHeightBehavior.AUTO_GROW) {
|
|
890
|
+
lastEmittedContentHeight = 0
|
|
891
|
+
lastEmittedContentHeightEditorId = null
|
|
892
|
+
}
|
|
893
|
+
richTextView.setHeightBehavior(nextBehavior)
|
|
894
|
+
val params = richTextView.layoutParams as LayoutParams
|
|
895
|
+
params.width = LayoutParams.MATCH_PARENT
|
|
896
|
+
params.height = if (nextBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
897
|
+
LayoutParams.WRAP_CONTENT
|
|
898
|
+
} else {
|
|
899
|
+
LayoutParams.MATCH_PARENT
|
|
900
|
+
}
|
|
901
|
+
richTextView.layoutParams = params
|
|
902
|
+
requestLayout()
|
|
903
|
+
if (nextBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
904
|
+
post { emitContentHeightIfNeeded(force = true) }
|
|
905
|
+
}
|
|
906
|
+
updateEditorViewportInset()
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private fun invalidateAutoGrowContentHeightEmission() {
|
|
910
|
+
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) return
|
|
911
|
+
lastEmittedContentHeight = 0
|
|
912
|
+
lastEmittedContentHeightEditorId = null
|
|
913
|
+
requestLayout()
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
fun setAddonsJson(addonsJson: String?) {
|
|
917
|
+
if (lastAddonsJson == addonsJson) return
|
|
918
|
+
clearPendingNativeActionRetry()
|
|
919
|
+
lastAddonsJson = addonsJson
|
|
920
|
+
addons = NativeEditorAddons.fromJson(addonsJson)
|
|
921
|
+
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: addons.mentions?.theme)
|
|
922
|
+
refreshMentionQuery()
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
fun setRemoteSelectionsJson(remoteSelectionsJson: String?) {
|
|
926
|
+
if (lastRemoteSelectionsJson == remoteSelectionsJson) return
|
|
927
|
+
lastRemoteSelectionsJson = remoteSelectionsJson
|
|
928
|
+
richTextView.setRemoteSelections(
|
|
929
|
+
RemoteSelectionDecoration.fromJson(context, remoteSelectionsJson)
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
fun setAutoFocus(autoFocus: Boolean) {
|
|
934
|
+
autoFocusRequested = autoFocus
|
|
935
|
+
applyAutoFocusIfNeeded()
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private fun applyAutoFocusIfNeeded() {
|
|
939
|
+
if (!autoFocusRequested || didApplyAutoFocus || !canFocusCurrentEditor()) return
|
|
940
|
+
didApplyAutoFocus = true
|
|
941
|
+
focus()
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
fun setAutoCapitalize(autoCapitalize: String?) {
|
|
945
|
+
richTextView.editorEditText.setAutoCapitalize(autoCapitalize)
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
fun setAutoCorrect(autoCorrect: Boolean?) {
|
|
949
|
+
richTextView.editorEditText.setAutoCorrect(autoCorrect)
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
fun setKeyboardType(keyboardType: String?) {
|
|
953
|
+
richTextView.editorEditText.setKeyboardType(keyboardType)
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
fun setEditable(editable: Boolean) {
|
|
957
|
+
if (richTextView.editorEditText.isEditable == editable) return
|
|
958
|
+
if (!editable) {
|
|
959
|
+
cancelPendingToolbarRefocus()
|
|
960
|
+
clearPendingNativeActionRetry()
|
|
961
|
+
}
|
|
962
|
+
richTextView.editorEditText.isEditable = editable
|
|
963
|
+
updateKeyboardToolbarVisibility()
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
fun setShowToolbar(showToolbar: Boolean) {
|
|
967
|
+
if (showsToolbar == showToolbar) return
|
|
968
|
+
if (!showToolbar) {
|
|
969
|
+
cancelPendingToolbarRefocus()
|
|
970
|
+
clearPendingNativeActionRetry()
|
|
971
|
+
}
|
|
972
|
+
showsToolbar = showToolbar
|
|
973
|
+
updateKeyboardToolbarVisibility()
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
fun setToolbarPlacement(rawToolbarPlacement: String?) {
|
|
977
|
+
val nextPlacement = ToolbarPlacement.fromRaw(rawToolbarPlacement)
|
|
978
|
+
if (toolbarPlacement == nextPlacement) return
|
|
979
|
+
if (nextPlacement != ToolbarPlacement.KEYBOARD) {
|
|
980
|
+
cancelPendingToolbarRefocus()
|
|
981
|
+
clearPendingNativeActionRetry()
|
|
982
|
+
}
|
|
983
|
+
toolbarPlacement = nextPlacement
|
|
984
|
+
updateKeyboardToolbarVisibility()
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
fun setAllowImageResizing(allowImageResizing: Boolean) {
|
|
988
|
+
richTextView.setImageResizingEnabled(allowImageResizing)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
fun setToolbarItemsJson(toolbarItemsJson: String?) {
|
|
992
|
+
if (lastToolbarItemsJson == toolbarItemsJson) return
|
|
993
|
+
clearPendingNativeActionRetry()
|
|
994
|
+
lastToolbarItemsJson = toolbarItemsJson
|
|
995
|
+
keyboardToolbarView.setItems(NativeToolbarItem.fromJson(toolbarItemsJson))
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
fun setToolbarFrameJson(toolbarFrameJson: String?) {
|
|
999
|
+
if (lastToolbarFrameJson == toolbarFrameJson) return
|
|
1000
|
+
lastToolbarFrameJson = toolbarFrameJson
|
|
1001
|
+
if (toolbarFrameJson.isNullOrBlank()) {
|
|
1002
|
+
toolbarFramesInWindow = emptyList()
|
|
1003
|
+
return
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
toolbarFramesInWindow = try {
|
|
1007
|
+
val json = JSONObject(toolbarFrameJson)
|
|
1008
|
+
val frames = json.optJSONArray("frames")
|
|
1009
|
+
if (frames != null) {
|
|
1010
|
+
buildList {
|
|
1011
|
+
for (index in 0 until frames.length()) {
|
|
1012
|
+
frames.optJSONObject(index)?.toToolbarFrame()?.let { add(it) }
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
} else {
|
|
1016
|
+
listOfNotNull(json.toToolbarFrame())
|
|
1017
|
+
}
|
|
1018
|
+
} catch (_: Throwable) {
|
|
1019
|
+
emptyList()
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
private fun JSONObject.toToolbarFrame(): RectF? {
|
|
1024
|
+
val x = optDouble("x", Double.NaN)
|
|
1025
|
+
val y = optDouble("y", Double.NaN)
|
|
1026
|
+
val width = optDouble("width", Double.NaN)
|
|
1027
|
+
val height = optDouble("height", Double.NaN)
|
|
1028
|
+
if (
|
|
1029
|
+
x.isNaN() || x.isInfinite() ||
|
|
1030
|
+
y.isNaN() || y.isInfinite() ||
|
|
1031
|
+
width.isNaN() || width.isInfinite() ||
|
|
1032
|
+
height.isNaN() || height.isInfinite()
|
|
1033
|
+
) {
|
|
1034
|
+
return null
|
|
1035
|
+
}
|
|
1036
|
+
if (width <= 0.0 || height <= 0.0) {
|
|
1037
|
+
return null
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return RectF(
|
|
1041
|
+
x.toFloat(),
|
|
1042
|
+
y.toFloat(),
|
|
1043
|
+
(x + width).toFloat(),
|
|
1044
|
+
(y + height).toFloat()
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
fun setPendingEditorUpdateJson(editorUpdateJson: String?) {
|
|
1049
|
+
pendingEditorUpdateJson = editorUpdateJson
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
fun setPendingEditorUpdateEditorId(editorUpdateEditorId: Long?) {
|
|
1053
|
+
pendingEditorUpdateEditorId = editorUpdateEditorId
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
fun setPendingEditorUpdateRevision(editorUpdateRevision: Int) {
|
|
1057
|
+
if (pendingEditorUpdateRevision != editorUpdateRevision) {
|
|
1058
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1059
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1060
|
+
}
|
|
1061
|
+
pendingEditorUpdateRevision = editorUpdateRevision
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
fun setPendingEditorResetUpdateJson(editorResetUpdateJson: String?) {
|
|
1065
|
+
lastEditorResetUpdateJsonProp = editorResetUpdateJson
|
|
1066
|
+
pendingEditorResetUpdateJson = editorResetUpdateJson
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
fun setPendingEditorResetUpdateEditorId(editorResetUpdateEditorId: Long?) {
|
|
1070
|
+
lastEditorResetUpdateEditorIdProp = editorResetUpdateEditorId
|
|
1071
|
+
pendingEditorResetUpdateEditorId = editorResetUpdateEditorId
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
fun setPendingEditorResetUpdateRevision(editorResetUpdateRevision: Int) {
|
|
1075
|
+
if (pendingEditorResetUpdateRevision != editorResetUpdateRevision) {
|
|
1076
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1077
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1078
|
+
}
|
|
1079
|
+
if (editorResetUpdateRevision != 0 && pendingEditorResetUpdateJson == null) {
|
|
1080
|
+
pendingEditorResetUpdateJson = lastEditorResetUpdateJsonProp
|
|
1081
|
+
}
|
|
1082
|
+
if (editorResetUpdateRevision != 0 && pendingEditorResetUpdateEditorId == null) {
|
|
1083
|
+
pendingEditorResetUpdateEditorId = lastEditorResetUpdateEditorIdProp
|
|
1084
|
+
}
|
|
1085
|
+
pendingEditorResetUpdateRevision = editorResetUpdateRevision
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private fun hasPendingEditorUpdateForEditor(editorId: Long): Boolean =
|
|
1089
|
+
pendingEditorUpdateJson != null &&
|
|
1090
|
+
pendingEditorUpdateRevision != 0 &&
|
|
1091
|
+
pendingEditorUpdateRevision != appliedEditorUpdateRevision &&
|
|
1092
|
+
pendingEditorUpdateEditorId == editorId
|
|
1093
|
+
|
|
1094
|
+
private fun hasPendingEditorResetUpdateForEditor(editorId: Long): Boolean =
|
|
1095
|
+
pendingEditorResetUpdateJson != null &&
|
|
1096
|
+
pendingEditorResetUpdateRevision != 0 &&
|
|
1097
|
+
pendingEditorResetUpdateRevision != appliedEditorResetUpdateRevision &&
|
|
1098
|
+
pendingEditorResetUpdateEditorId == editorId
|
|
1099
|
+
|
|
1100
|
+
private fun hasPendingEditorUpdateForCurrentEditor(): Boolean =
|
|
1101
|
+
hasPendingEditorUpdateForEditor(richTextView.editorId)
|
|
1102
|
+
|
|
1103
|
+
private fun hasPendingEditorResetUpdateForCurrentEditor(): Boolean =
|
|
1104
|
+
hasPendingEditorResetUpdateForEditor(richTextView.editorId)
|
|
1105
|
+
|
|
1106
|
+
private fun pendingEditorUpdateCommandPreparationJSON(): String =
|
|
1107
|
+
NativeEditorViewRegistry.commandPreparationJSON(
|
|
1108
|
+
ready = false,
|
|
1109
|
+
blockedReason = "pendingUpdate"
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
private fun shouldBlockEditorCommandForPendingUpdate(): Boolean =
|
|
1113
|
+
hasPendingEditorResetUpdateForCurrentEditor() || hasPendingEditorUpdateForCurrentEditor()
|
|
1114
|
+
|
|
1115
|
+
private fun refreshReadyStateIfSettled() {
|
|
1116
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1117
|
+
if (hasPendingEditorResetUpdateForCurrentEditor()) return
|
|
1118
|
+
if (hasPendingEditorUpdateForCurrentEditor()) return
|
|
1119
|
+
if (!isAttachedToNativeWindow) return
|
|
1120
|
+
if (richTextView.editorEditText.editorId != richTextView.editorId) return
|
|
1121
|
+
refreshToolbarStateFromEditorSelection()
|
|
1122
|
+
refreshMentionQuery()
|
|
1123
|
+
emitEditorReadyIfNeeded()
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
fun applyPendingEditorResetUpdateIfNeeded() {
|
|
1127
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1128
|
+
if (pendingEditorResetUpdateRevision == 0) return
|
|
1129
|
+
val revision = pendingEditorResetUpdateRevision
|
|
1130
|
+
val editorId = richTextView.editorId
|
|
1131
|
+
val expectedEditorId = pendingEditorResetUpdateEditorId
|
|
1132
|
+
if (expectedEditorId == null) return
|
|
1133
|
+
if (expectedEditorId != editorId) return
|
|
1134
|
+
if (pendingEditorResetUpdateJson == null) {
|
|
1135
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1136
|
+
refreshReadyStateIfSettled()
|
|
1137
|
+
return
|
|
1138
|
+
}
|
|
1139
|
+
val updateJson = pendingEditorResetUpdateJson ?: return
|
|
1140
|
+
if (revision == appliedEditorResetUpdateRevision) {
|
|
1141
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1142
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1143
|
+
refreshReadyStateIfSettled()
|
|
1144
|
+
return
|
|
1145
|
+
}
|
|
1146
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return
|
|
1147
|
+
val apply = Runnable {
|
|
1148
|
+
if (editorId != richTextView.editorId) return@Runnable
|
|
1149
|
+
if (expectedEditorId != richTextView.editorId) return@Runnable
|
|
1150
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return@Runnable
|
|
1151
|
+
if (revision != pendingEditorResetUpdateRevision) return@Runnable
|
|
1152
|
+
if (revision == appliedEditorResetUpdateRevision) {
|
|
1153
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1154
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1155
|
+
refreshReadyStateIfSettled()
|
|
1156
|
+
return@Runnable
|
|
1157
|
+
}
|
|
1158
|
+
if (applyEditorResetUpdate(updateJson)) {
|
|
1159
|
+
appliedEditorResetUpdateRevision = revision
|
|
1160
|
+
clearPendingEditorResetUpdateState(resetAppliedRevision = false)
|
|
1161
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1162
|
+
refreshReadyStateIfSettled()
|
|
1163
|
+
} else {
|
|
1164
|
+
schedulePendingEditorUpdateRetry()
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
1168
|
+
apply.run()
|
|
1169
|
+
} else if (!post(apply)) {
|
|
1170
|
+
richTextView.post(apply)
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
fun applyPendingEditorUpdateIfNeeded() {
|
|
1175
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1176
|
+
if (pendingEditorUpdateRevision == 0) return
|
|
1177
|
+
val revision = pendingEditorUpdateRevision
|
|
1178
|
+
val editorId = richTextView.editorId
|
|
1179
|
+
val expectedEditorId = pendingEditorUpdateEditorId
|
|
1180
|
+
if (expectedEditorId == null) return
|
|
1181
|
+
if (expectedEditorId != editorId) return
|
|
1182
|
+
if (pendingEditorUpdateJson == null) {
|
|
1183
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
1184
|
+
refreshReadyStateIfSettled()
|
|
1185
|
+
return
|
|
1186
|
+
}
|
|
1187
|
+
val updateJson = pendingEditorUpdateJson ?: return
|
|
1188
|
+
if (pendingEditorUpdateRevision == appliedEditorUpdateRevision) {
|
|
1189
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
1190
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1191
|
+
refreshReadyStateIfSettled()
|
|
1192
|
+
return
|
|
1193
|
+
}
|
|
1194
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return
|
|
1195
|
+
val apply = Runnable {
|
|
1196
|
+
if (editorId != richTextView.editorId) return@Runnable
|
|
1197
|
+
if (expectedEditorId != richTextView.editorId) return@Runnable
|
|
1198
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return@Runnable
|
|
1199
|
+
if (revision != pendingEditorUpdateRevision) return@Runnable
|
|
1200
|
+
if (revision == appliedEditorUpdateRevision) {
|
|
1201
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
1202
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1203
|
+
refreshReadyStateIfSettled()
|
|
1204
|
+
return@Runnable
|
|
1205
|
+
}
|
|
1206
|
+
if (applyEditorUpdate(updateJson, scheduleViewCommandRetry = false)) {
|
|
1207
|
+
appliedEditorUpdateRevision = revision
|
|
1208
|
+
pendingEditorUpdateJson = null
|
|
1209
|
+
pendingEditorUpdateEditorId = null
|
|
1210
|
+
pendingEditorUpdateRevision = 0
|
|
1211
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1212
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1213
|
+
cancelPendingEditorUpdateRetry()
|
|
1214
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
1215
|
+
refreshReadyStateIfSettled()
|
|
1216
|
+
} else {
|
|
1217
|
+
schedulePendingEditorUpdateRetry()
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
1221
|
+
apply.run()
|
|
1222
|
+
} else if (!post(apply)) {
|
|
1223
|
+
richTextView.post(apply)
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
private fun clearPendingEditorUpdateState(resetAppliedRevision: Boolean = true) {
|
|
1228
|
+
pendingEditorUpdateJson = null
|
|
1229
|
+
pendingEditorUpdateEditorId = null
|
|
1230
|
+
pendingEditorUpdateRevision = 0
|
|
1231
|
+
if (resetAppliedRevision) {
|
|
1232
|
+
appliedEditorUpdateRevision = 0
|
|
1233
|
+
}
|
|
1234
|
+
cancelPendingEditorUpdateRetry()
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
private fun clearPendingEditorResetUpdateState(resetAppliedRevision: Boolean = true) {
|
|
1238
|
+
pendingEditorResetUpdateJson = null
|
|
1239
|
+
pendingEditorResetUpdateEditorId = null
|
|
1240
|
+
pendingEditorResetUpdateRevision = 0
|
|
1241
|
+
if (resetAppliedRevision) {
|
|
1242
|
+
appliedEditorResetUpdateRevision = 0
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
private fun cancelPendingEditorUpdateRetry() {
|
|
1247
|
+
pendingEditorUpdateRetryScheduled = false
|
|
1248
|
+
pendingEditorUpdateRetryEditorId = null
|
|
1249
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1250
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1251
|
+
pendingEditorUpdateRetryGeneration += 1
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
private fun schedulePendingEditorUpdateRetry() {
|
|
1255
|
+
if (pendingEditorUpdateRetryScheduled) return
|
|
1256
|
+
val pastFastRetryBudget =
|
|
1257
|
+
pendingEditorUpdateRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS
|
|
1258
|
+
if (
|
|
1259
|
+
pastFastRetryBudget &&
|
|
1260
|
+
!pendingEditorUpdateForcedRecoveryAttempted &&
|
|
1261
|
+
richTextView.editorId != 0L &&
|
|
1262
|
+
richTextView.editorEditText.editorId == richTextView.editorId
|
|
1263
|
+
) {
|
|
1264
|
+
pendingEditorUpdateForcedRecoveryAttempted = true
|
|
1265
|
+
richTextView.editorEditText.discardTransientNativeInputForExternalRecovery()
|
|
1266
|
+
}
|
|
1267
|
+
if (!pastFastRetryBudget) {
|
|
1268
|
+
pendingEditorUpdateRetryAttempts += 1
|
|
1269
|
+
}
|
|
1270
|
+
pendingEditorUpdateRetryEditorId = richTextView.editorId
|
|
1271
|
+
pendingEditorUpdateRetryScheduled = true
|
|
1272
|
+
pendingEditorUpdateRetryGeneration += 1
|
|
1273
|
+
val retryGeneration = pendingEditorUpdateRetryGeneration
|
|
1274
|
+
val delayMs = if (pastFastRetryBudget) {
|
|
1275
|
+
PENDING_UPDATE_RECOVERY_RETRY_DELAY_MS
|
|
1276
|
+
} else {
|
|
1277
|
+
NATIVE_ACTION_RETRY_DELAY_MS * pendingEditorUpdateRetryAttempts
|
|
1278
|
+
}
|
|
1279
|
+
val retry = Runnable {
|
|
1280
|
+
if (retryGeneration != pendingEditorUpdateRetryGeneration) return@Runnable
|
|
1281
|
+
if (pendingEditorUpdateRetryEditorId != richTextView.editorId) {
|
|
1282
|
+
clearPendingEditorUpdateState()
|
|
1283
|
+
return@Runnable
|
|
1284
|
+
}
|
|
1285
|
+
pendingEditorUpdateRetryScheduled = false
|
|
1286
|
+
pendingEditorUpdateRetryEditorId = null
|
|
1287
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
1288
|
+
applyPendingEditorUpdateIfNeeded()
|
|
1289
|
+
}
|
|
1290
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
private fun clearPendingThemeRetry() {
|
|
1294
|
+
pendingThemeJson = null
|
|
1295
|
+
hasPendingTheme = false
|
|
1296
|
+
cancelPendingThemeRetry()
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private fun cancelPendingThemeRetry() {
|
|
1300
|
+
pendingThemeRetryScheduled = false
|
|
1301
|
+
pendingThemeRetryEditorId = null
|
|
1302
|
+
pendingThemeRetryAttempts = 0
|
|
1303
|
+
pendingThemeRetryGeneration += 1
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
private fun applyPendingThemeIfNeeded() {
|
|
1307
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1308
|
+
if (!hasPendingTheme) return
|
|
1309
|
+
val themeJson = pendingThemeJson
|
|
1310
|
+
val editorId = richTextView.editorId
|
|
1311
|
+
if (pendingThemeRetryEditorId != editorId) {
|
|
1312
|
+
pendingThemeRetryEditorId = editorId
|
|
1313
|
+
}
|
|
1314
|
+
if (
|
|
1315
|
+
blockThemePreflightForTesting ||
|
|
1316
|
+
!richTextView.editorEditText.prepareForExternalEditorUpdate()
|
|
1317
|
+
) {
|
|
1318
|
+
schedulePendingThemeRetry()
|
|
1319
|
+
return
|
|
1320
|
+
}
|
|
1321
|
+
pendingThemeJson = null
|
|
1322
|
+
hasPendingTheme = false
|
|
1323
|
+
cancelPendingThemeRetry()
|
|
1324
|
+
applyThemeJson(themeJson)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private fun schedulePendingThemeRetry() {
|
|
1328
|
+
if (pendingThemeRetryScheduled) return
|
|
1329
|
+
if (pendingThemeRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS) return
|
|
1330
|
+
pendingThemeRetryAttempts += 1
|
|
1331
|
+
pendingThemeRetryEditorId = richTextView.editorId
|
|
1332
|
+
pendingThemeRetryScheduled = true
|
|
1333
|
+
pendingThemeRetryGeneration += 1
|
|
1334
|
+
val retryGeneration = pendingThemeRetryGeneration
|
|
1335
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingThemeRetryAttempts
|
|
1336
|
+
val retry = Runnable {
|
|
1337
|
+
if (retryGeneration != pendingThemeRetryGeneration) return@Runnable
|
|
1338
|
+
if (pendingThemeRetryEditorId != richTextView.editorId) {
|
|
1339
|
+
clearPendingThemeRetry()
|
|
1340
|
+
return@Runnable
|
|
1341
|
+
}
|
|
1342
|
+
pendingThemeRetryScheduled = false
|
|
1343
|
+
applyPendingThemeIfNeeded()
|
|
1344
|
+
}
|
|
1345
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private fun clearPendingViewCommandUpdateRetry() {
|
|
1349
|
+
pendingViewCommandUpdateJson = null
|
|
1350
|
+
pendingViewCommandUpdateEditorId = null
|
|
1351
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1352
|
+
pendingViewCommandUpdateRetryAttempts = 0
|
|
1353
|
+
pendingViewCommandUpdateRetryGeneration += 1
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
private fun scheduleViewCommandUpdateRetry(updateJson: String) {
|
|
1357
|
+
if (pendingViewCommandUpdateJson != updateJson) {
|
|
1358
|
+
pendingViewCommandUpdateRetryAttempts = 0
|
|
1359
|
+
}
|
|
1360
|
+
pendingViewCommandUpdateJson = updateJson
|
|
1361
|
+
pendingViewCommandUpdateEditorId = richTextView.editorId
|
|
1362
|
+
if (pendingViewCommandUpdateRetryScheduled) return
|
|
1363
|
+
if (pendingViewCommandUpdateRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS) return
|
|
1364
|
+
pendingViewCommandUpdateRetryAttempts += 1
|
|
1365
|
+
pendingViewCommandUpdateRetryScheduled = true
|
|
1366
|
+
pendingViewCommandUpdateRetryGeneration += 1
|
|
1367
|
+
val retryGeneration = pendingViewCommandUpdateRetryGeneration
|
|
1368
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingViewCommandUpdateRetryAttempts
|
|
1369
|
+
val retry = Runnable {
|
|
1370
|
+
if (retryGeneration != pendingViewCommandUpdateRetryGeneration) return@Runnable
|
|
1371
|
+
val retryJson = pendingViewCommandUpdateJson ?: run {
|
|
1372
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1373
|
+
return@Runnable
|
|
1374
|
+
}
|
|
1375
|
+
if (pendingViewCommandUpdateEditorId != richTextView.editorId || richTextView.editorId == 0L) {
|
|
1376
|
+
clearPendingViewCommandUpdateRetry()
|
|
1377
|
+
return@Runnable
|
|
1378
|
+
}
|
|
1379
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
1380
|
+
clearPendingViewCommandUpdateRetry()
|
|
1381
|
+
return@Runnable
|
|
1382
|
+
}
|
|
1383
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1384
|
+
if (applyEditorUpdate(retryJson, scheduleViewCommandRetry = true)) {
|
|
1385
|
+
clearPendingViewCommandUpdateRetry()
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
private fun schedulePendingPreflightWake() {
|
|
1392
|
+
if (pendingPreflightWakeScheduled) return
|
|
1393
|
+
pendingPreflightWakeScheduled = true
|
|
1394
|
+
pendingPreflightWakeGeneration += 1
|
|
1395
|
+
val wakeGeneration = pendingPreflightWakeGeneration
|
|
1396
|
+
mainHandler.post {
|
|
1397
|
+
if (wakeGeneration != pendingPreflightWakeGeneration) return@post
|
|
1398
|
+
pendingPreflightWakeScheduled = false
|
|
1399
|
+
wakePendingPreflightWork()
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private fun cancelPendingPreflightWake() {
|
|
1404
|
+
pendingPreflightWakeScheduled = false
|
|
1405
|
+
pendingPreflightWakeGeneration += 1
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private fun wakePendingPreflightWork() {
|
|
1409
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
1410
|
+
schedulePendingPreflightWake()
|
|
1411
|
+
return
|
|
1412
|
+
}
|
|
1413
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1414
|
+
if (pendingEditorResetUpdateJson != null) {
|
|
1415
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
1416
|
+
}
|
|
1417
|
+
if (pendingEditorUpdateJson != null) {
|
|
1418
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1419
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1420
|
+
applyPendingEditorUpdateIfNeeded()
|
|
1421
|
+
}
|
|
1422
|
+
if (hasPendingTheme) {
|
|
1423
|
+
pendingThemeRetryAttempts = 0
|
|
1424
|
+
applyPendingThemeIfNeeded()
|
|
1425
|
+
}
|
|
1426
|
+
pendingViewCommandUpdateJson?.let { updateJson ->
|
|
1427
|
+
pendingViewCommandUpdateRetryAttempts = 0
|
|
1428
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1429
|
+
pendingViewCommandUpdateRetryGeneration += 1
|
|
1430
|
+
if (applyEditorUpdate(updateJson, scheduleViewCommandRetry = true)) {
|
|
1431
|
+
clearPendingViewCommandUpdateRetry()
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
retryPendingNativeActionFromWake()
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
private fun clearPendingNativeActionRetry() {
|
|
1438
|
+
pendingNativeAction = null
|
|
1439
|
+
pendingNativeActionScope = null
|
|
1440
|
+
pendingNativeActionRetryEditorId = null
|
|
1441
|
+
pendingNativeActionRetryScheduled = false
|
|
1442
|
+
pendingNativeActionRetryAttempts = 0
|
|
1443
|
+
pendingNativeActionRetryGeneration += 1
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
private fun currentNativeActionScope(action: PendingNativeAction): PendingNativeActionScope {
|
|
1447
|
+
val selection = richTextView.editorEditText.currentScalarSelection()
|
|
1448
|
+
val mentionScope = when (action) {
|
|
1449
|
+
is PendingNativeAction.MentionSuggestionSelect ->
|
|
1450
|
+
mentionQueryState ?: addons.mentions?.let { currentMentionQueryState(it.trigger) }
|
|
1451
|
+
is PendingNativeAction.ToolbarItemPress -> null
|
|
1452
|
+
}
|
|
1453
|
+
return PendingNativeActionScope(
|
|
1454
|
+
editorId = richTextView.editorId,
|
|
1455
|
+
documentVersion = lastDocumentVersion,
|
|
1456
|
+
allowedDocumentVersion = documentVersionFromUpdateJSON(pendingEditorUpdateJson),
|
|
1457
|
+
hadFocus = isEditorEffectivelyFocusedForNativeAction(),
|
|
1458
|
+
hadVisibleToolbar = isNativeActionToolbarVisible(action),
|
|
1459
|
+
selectionAnchor = selection?.first,
|
|
1460
|
+
selectionHead = selection?.second,
|
|
1461
|
+
mentionAnchor = mentionScope?.anchor,
|
|
1462
|
+
mentionHead = mentionScope?.head,
|
|
1463
|
+
mentionQuery = mentionScope?.query
|
|
1464
|
+
)
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
private fun isPendingNativeActionScopeCurrent(
|
|
1468
|
+
action: PendingNativeAction,
|
|
1469
|
+
scope: PendingNativeActionScope
|
|
1470
|
+
): Boolean {
|
|
1471
|
+
if (scope.editorId != richTextView.editorId) return false
|
|
1472
|
+
if (scope.hadFocus != isEditorEffectivelyFocusedForNativeAction()) return false
|
|
1473
|
+
if (scope.hadVisibleToolbar != isNativeActionToolbarVisible(action)) return false
|
|
1474
|
+
if (
|
|
1475
|
+
scope.documentVersion != lastDocumentVersion &&
|
|
1476
|
+
(scope.allowedDocumentVersion == null || scope.allowedDocumentVersion != lastDocumentVersion)
|
|
1477
|
+
) {
|
|
1478
|
+
return false
|
|
1479
|
+
}
|
|
1480
|
+
val selection = richTextView.editorEditText.currentScalarSelection()
|
|
1481
|
+
if (scope.selectionAnchor != selection?.first || scope.selectionHead != selection?.second) {
|
|
1482
|
+
return false
|
|
1483
|
+
}
|
|
1484
|
+
if (action is PendingNativeAction.MentionSuggestionSelect) {
|
|
1485
|
+
val mentions = addons.mentions ?: return false
|
|
1486
|
+
val currentQuery = currentMentionQueryState(mentions.trigger) ?: return false
|
|
1487
|
+
if (
|
|
1488
|
+
scope.mentionAnchor != currentQuery.anchor ||
|
|
1489
|
+
scope.mentionHead != currentQuery.head ||
|
|
1490
|
+
scope.mentionQuery != currentQuery.query
|
|
1491
|
+
) {
|
|
1492
|
+
return false
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
return true
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
private fun isNativeActionToolbarVisible(action: PendingNativeAction): Boolean {
|
|
1499
|
+
if (!showsToolbar || toolbarPlacement != ToolbarPlacement.KEYBOARD) return false
|
|
1500
|
+
if (keyboardToolbarView.parent == null || keyboardToolbarView.visibility != View.VISIBLE) return false
|
|
1501
|
+
if (action is PendingNativeAction.MentionSuggestionSelect) {
|
|
1502
|
+
return keyboardToolbarView.isShowingMentionSuggestions
|
|
1503
|
+
}
|
|
1504
|
+
return true
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
private fun isEditorEffectivelyFocusedForNativeAction(): Boolean =
|
|
1508
|
+
richTextView.editorEditText.hasFocus() ||
|
|
1509
|
+
(pendingToolbarRefocus != null && pendingToolbarRefocusEditorId == richTextView.editorId)
|
|
1510
|
+
|
|
1511
|
+
private fun clearPendingNativeActionRetryIfScopeChanged() {
|
|
1512
|
+
val action = pendingNativeAction ?: return
|
|
1513
|
+
val scope = pendingNativeActionScope ?: return
|
|
1514
|
+
if (!isPendingNativeActionScopeCurrent(action, scope)) {
|
|
1515
|
+
clearPendingNativeActionRetry()
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
private fun schedulePendingNativeActionRetry(action: PendingNativeAction) {
|
|
1520
|
+
val isSameAction = pendingNativeAction == action
|
|
1521
|
+
if (isSameAction) {
|
|
1522
|
+
pendingNativeActionRetryAttempts += 1
|
|
1523
|
+
} else {
|
|
1524
|
+
pendingNativeActionRetryAttempts = 1
|
|
1525
|
+
pendingNativeActionScope = currentNativeActionScope(action)
|
|
1526
|
+
}
|
|
1527
|
+
if (pendingNativeActionRetryAttempts > MAX_NATIVE_ACTION_RETRY_ATTEMPTS) {
|
|
1528
|
+
pendingNativeAction = action
|
|
1529
|
+
pendingNativeActionRetryEditorId = richTextView.editorId
|
|
1530
|
+
pendingNativeActionRetryScheduled = false
|
|
1531
|
+
return
|
|
1532
|
+
}
|
|
1533
|
+
pendingNativeAction = action
|
|
1534
|
+
pendingNativeActionRetryEditorId = richTextView.editorId
|
|
1535
|
+
if (pendingNativeActionRetryScheduled) return
|
|
1536
|
+
pendingNativeActionRetryScheduled = true
|
|
1537
|
+
pendingNativeActionRetryGeneration += 1
|
|
1538
|
+
val retryGeneration = pendingNativeActionRetryGeneration
|
|
1539
|
+
val retry = Runnable {
|
|
1540
|
+
if (retryGeneration != pendingNativeActionRetryGeneration) return@Runnable
|
|
1541
|
+
val retryAction = pendingNativeAction ?: run {
|
|
1542
|
+
pendingNativeActionRetryScheduled = false
|
|
1543
|
+
return@Runnable
|
|
1544
|
+
}
|
|
1545
|
+
val retryScope = pendingNativeActionScope ?: run {
|
|
1546
|
+
clearPendingNativeActionRetry()
|
|
1547
|
+
return@Runnable
|
|
1548
|
+
}
|
|
1549
|
+
if (pendingNativeActionRetryEditorId != richTextView.editorId || richTextView.editorId == 0L) {
|
|
1550
|
+
clearPendingNativeActionRetry()
|
|
1551
|
+
return@Runnable
|
|
1552
|
+
}
|
|
1553
|
+
if (!isPendingNativeActionScopeCurrent(retryAction, retryScope)) {
|
|
1554
|
+
clearPendingNativeActionRetry()
|
|
1555
|
+
return@Runnable
|
|
1556
|
+
}
|
|
1557
|
+
pendingNativeActionRetryScheduled = false
|
|
1558
|
+
val allowNextRetry = pendingNativeActionRetryAttempts < MAX_NATIVE_ACTION_RETRY_ATTEMPTS
|
|
1559
|
+
when (retryAction) {
|
|
1560
|
+
is PendingNativeAction.ToolbarItemPress ->
|
|
1561
|
+
handleToolbarItemPress(retryAction.item, allowPreflightRetry = allowNextRetry)
|
|
1562
|
+
is PendingNativeAction.MentionSuggestionSelect ->
|
|
1563
|
+
insertMentionSuggestion(retryAction.suggestion, allowPreflightRetry = allowNextRetry)
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
mainHandler.postDelayed(retry, NATIVE_ACTION_RETRY_DELAY_MS)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
private fun retryPendingNativeActionFromWake() {
|
|
1570
|
+
val action = pendingNativeAction ?: return
|
|
1571
|
+
val scope = pendingNativeActionScope ?: run {
|
|
1572
|
+
clearPendingNativeActionRetry()
|
|
1573
|
+
return
|
|
1574
|
+
}
|
|
1575
|
+
if (!isPendingNativeActionScopeCurrent(action, scope)) {
|
|
1576
|
+
clearPendingNativeActionRetry()
|
|
1577
|
+
return
|
|
1578
|
+
}
|
|
1579
|
+
pendingNativeActionRetryAttempts = 0
|
|
1580
|
+
pendingNativeActionRetryScheduled = false
|
|
1581
|
+
when (action) {
|
|
1582
|
+
is PendingNativeAction.ToolbarItemPress ->
|
|
1583
|
+
handleToolbarItemPress(action.item, allowPreflightRetry = true)
|
|
1584
|
+
is PendingNativeAction.MentionSuggestionSelect ->
|
|
1585
|
+
insertMentionSuggestion(action.suggestion, allowPreflightRetry = true)
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
private fun documentVersionFromUpdateJSON(updateJSON: String?): Int? =
|
|
1590
|
+
try {
|
|
1591
|
+
if (updateJSON == null) null
|
|
1592
|
+
else {
|
|
1593
|
+
val version = JSONObject(updateJSON).optInt("documentVersion", Int.MIN_VALUE)
|
|
1594
|
+
version.takeIf { it != Int.MIN_VALUE }
|
|
1595
|
+
}
|
|
1596
|
+
} catch (_: Throwable) {
|
|
1597
|
+
null
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
private fun noteDocumentVersionFromUpdateJSON(updateJSON: String?) {
|
|
1601
|
+
documentVersionFromUpdateJSON(updateJSON)?.let { version ->
|
|
1602
|
+
lastDocumentVersion = version
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
private fun addPreflightUpdateToEvent(
|
|
1607
|
+
event: MutableMap<String, Any>,
|
|
1608
|
+
updateJSON: String?
|
|
1609
|
+
) {
|
|
1610
|
+
if (updateJSON == null) return
|
|
1611
|
+
event["updateJson"] = updateJSON
|
|
1612
|
+
documentVersionFromUpdateJSON(updateJSON)?.let { version ->
|
|
1613
|
+
event["documentVersion"] = version
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
private fun emitAddonEvent(payload: Map<String, Any>) {
|
|
1618
|
+
onAddonEventForTesting?.invoke(payload) ?: onAddonEvent(payload)
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
private fun canFocusCurrentEditor(): Boolean {
|
|
1622
|
+
val editorId = richTextView.editorId
|
|
1623
|
+
return editorId != 0L &&
|
|
1624
|
+
isAttachedToNativeWindow &&
|
|
1625
|
+
!NativeEditorViewRegistry.isDestroyed(editorId)
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
fun focus() {
|
|
1629
|
+
focusInternal(cancelPendingOutsideTapBlur = true)
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private fun focusInternal(cancelPendingOutsideTapBlur: Boolean) {
|
|
1633
|
+
if (!canFocusCurrentEditor()) return
|
|
1634
|
+
if (cancelPendingOutsideTapBlur) {
|
|
1635
|
+
cancelPendingOutsideTapBlur()
|
|
1636
|
+
}
|
|
1637
|
+
cancelPendingKeyboardDismiss()
|
|
1638
|
+
cancelPendingBlurRetry()
|
|
1639
|
+
richTextView.editorEditText.requestFocus()
|
|
1640
|
+
richTextView.editorEditText.post {
|
|
1641
|
+
if (!canFocusCurrentEditor()) return@post
|
|
1642
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1643
|
+
imm?.showSoftInput(richTextView.editorEditText, InputMethodManager.SHOW_IMPLICIT)
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
fun blur() {
|
|
1648
|
+
cancelPendingOutsideTapBlur()
|
|
1649
|
+
cancelPendingKeyboardDismiss()
|
|
1650
|
+
cancelPendingToolbarRefocus()
|
|
1651
|
+
clearRecentToolbarTouch()
|
|
1652
|
+
performBlur(deferKeyboardDismiss = false, allowRetry = true)
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
private fun performBlur(deferKeyboardDismiss: Boolean, allowRetry: Boolean) {
|
|
1656
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1657
|
+
if (!richTextView.editorEditText.prepareForExternalEditorUpdate()) {
|
|
1658
|
+
if (allowRetry && pendingBlurRetryAttempts < MAX_PENDING_UPDATE_RETRY_ATTEMPTS) {
|
|
1659
|
+
schedulePendingBlurRetry(deferKeyboardDismiss)
|
|
1660
|
+
return
|
|
1661
|
+
}
|
|
1662
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1663
|
+
richTextView.editorEditText.restoreAuthorizedTextIfNeeded()
|
|
1664
|
+
}
|
|
1665
|
+
completeBlur(deferKeyboardDismiss)
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
private fun completeBlur(deferKeyboardDismiss: Boolean) {
|
|
1669
|
+
cancelPendingBlurRetry()
|
|
1670
|
+
traceOutsideTap(
|
|
1671
|
+
"complete blur deferKeyboardDismiss=$deferKeyboardDismiss focusedBefore=${richTextView.editorEditText.hasFocus()}"
|
|
1672
|
+
)
|
|
1673
|
+
richTextView.editorEditText.clearFocus()
|
|
1674
|
+
traceOutsideTap("complete blur focusedAfter=${richTextView.editorEditText.hasFocus()}")
|
|
1675
|
+
if (deferKeyboardDismiss) {
|
|
1676
|
+
val dismiss = Runnable {
|
|
1677
|
+
pendingKeyboardDismiss = null
|
|
1678
|
+
if (!richTextView.editorEditText.hasFocus()) {
|
|
1679
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1680
|
+
imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
pendingKeyboardDismiss = dismiss
|
|
1684
|
+
richTextView.editorEditText.post(dismiss)
|
|
1685
|
+
return
|
|
1686
|
+
}
|
|
1687
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1688
|
+
imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
private fun schedulePendingBlurRetry(deferKeyboardDismiss: Boolean) {
|
|
1692
|
+
pendingBlurRetry?.let {
|
|
1693
|
+
mainHandler.removeCallbacks(it)
|
|
1694
|
+
pendingBlurRetry = null
|
|
1695
|
+
}
|
|
1696
|
+
pendingBlurRetryAttempts += 1
|
|
1697
|
+
pendingBlurRetryEditorId = richTextView.editorId
|
|
1698
|
+
pendingBlurRetryGeneration += 1
|
|
1699
|
+
val retryGeneration = pendingBlurRetryGeneration
|
|
1700
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingBlurRetryAttempts
|
|
1701
|
+
val retry = Runnable {
|
|
1702
|
+
pendingBlurRetry = null
|
|
1703
|
+
if (retryGeneration != pendingBlurRetryGeneration) return@Runnable
|
|
1704
|
+
if (pendingBlurRetryEditorId != richTextView.editorId) {
|
|
1705
|
+
pendingBlurRetryEditorId = null
|
|
1706
|
+
return@Runnable
|
|
1707
|
+
}
|
|
1708
|
+
pendingBlurRetryEditorId = null
|
|
1709
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return@Runnable
|
|
1710
|
+
performBlur(deferKeyboardDismiss, allowRetry = true)
|
|
1711
|
+
}
|
|
1712
|
+
pendingBlurRetry = retry
|
|
1713
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
private fun blurWithDeferredKeyboardDismiss() {
|
|
1717
|
+
cancelPendingKeyboardDismiss()
|
|
1718
|
+
cancelPendingToolbarRefocus()
|
|
1719
|
+
clearRecentToolbarTouch()
|
|
1720
|
+
performBlur(deferKeyboardDismiss = true, allowRetry = true)
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
private fun scheduleToolbarRefocus() {
|
|
1724
|
+
cancelPendingToolbarRefocus()
|
|
1725
|
+
val editorId = richTextView.editorId
|
|
1726
|
+
pendingToolbarRefocusEditorId = editorId
|
|
1727
|
+
pendingToolbarRefocusGeneration += 1
|
|
1728
|
+
val refocusGeneration = pendingToolbarRefocusGeneration
|
|
1729
|
+
val refocus = Runnable {
|
|
1730
|
+
pendingToolbarRefocus = null
|
|
1731
|
+
if (refocusGeneration != pendingToolbarRefocusGeneration) return@Runnable
|
|
1732
|
+
if (pendingToolbarRefocusEditorId != richTextView.editorId) return@Runnable
|
|
1733
|
+
pendingToolbarRefocusEditorId = null
|
|
1734
|
+
focusInternal(cancelPendingOutsideTapBlur = false)
|
|
1735
|
+
}
|
|
1736
|
+
pendingToolbarRefocus = refocus
|
|
1737
|
+
richTextView.editorEditText.post(refocus)
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
private fun cancelPendingToolbarRefocus() {
|
|
1741
|
+
pendingToolbarRefocus?.let {
|
|
1742
|
+
richTextView.editorEditText.removeCallbacks(it)
|
|
1743
|
+
pendingToolbarRefocus = null
|
|
1744
|
+
}
|
|
1745
|
+
pendingToolbarRefocusEditorId = null
|
|
1746
|
+
pendingToolbarRefocusGeneration += 1
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
private fun scheduleOutsideTapBlur() {
|
|
1750
|
+
cancelPendingOutsideTapBlur()
|
|
1751
|
+
traceOutsideTap("schedule outside blur focused=${richTextView.editorEditText.hasFocus()}")
|
|
1752
|
+
val blur = Runnable {
|
|
1753
|
+
pendingOutsideTapBlur = null
|
|
1754
|
+
traceOutsideTap("run outside blur focused=${richTextView.editorEditText.hasFocus()}")
|
|
1755
|
+
if (richTextView.editorEditText.hasFocus()) {
|
|
1756
|
+
blurWithDeferredKeyboardDismiss()
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
pendingOutsideTapBlur = blur
|
|
1760
|
+
richTextView.editorEditText.postDelayed(blur, OUTSIDE_TAP_BLUR_DELAY_MS)
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
private fun cancelPendingOutsideTapBlur() {
|
|
1764
|
+
pendingOutsideTapBlur?.let {
|
|
1765
|
+
traceOutsideTap("cancel outside blur")
|
|
1766
|
+
richTextView.editorEditText.removeCallbacks(it)
|
|
1767
|
+
pendingOutsideTapBlur = null
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private fun cancelPendingKeyboardDismiss() {
|
|
1772
|
+
pendingKeyboardDismiss?.let {
|
|
1773
|
+
richTextView.editorEditText.removeCallbacks(it)
|
|
1774
|
+
pendingKeyboardDismiss = null
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
private fun cancelPendingBlurRetry() {
|
|
1779
|
+
pendingBlurRetry?.let {
|
|
1780
|
+
mainHandler.removeCallbacks(it)
|
|
1781
|
+
pendingBlurRetry = null
|
|
1782
|
+
}
|
|
1783
|
+
pendingBlurRetryEditorId = null
|
|
1784
|
+
pendingBlurRetryAttempts = 0
|
|
1785
|
+
pendingBlurRetryGeneration += 1
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
fun getCaretRectJson(): String? {
|
|
1789
|
+
if (width <= 0 || height <= 0) return null
|
|
1790
|
+
val rect = richTextView.caretRect() ?: return null
|
|
1791
|
+
val density = resources.displayMetrics.density
|
|
1792
|
+
return JSONObject()
|
|
1793
|
+
.put("x", rect.left / density)
|
|
1794
|
+
.put("y", rect.top / density)
|
|
1795
|
+
.put("width", rect.width() / density)
|
|
1796
|
+
.put("height", rect.height() / density)
|
|
1797
|
+
.put("editorWidth", width / density)
|
|
1798
|
+
.put("editorHeight", height / density)
|
|
1799
|
+
.toString()
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
override fun onAttachedToWindow() {
|
|
1803
|
+
super.onAttachedToWindow()
|
|
1804
|
+
handleAttachedToWindow()
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
internal fun handleEditorDestroyed(editorId: Long) {
|
|
1808
|
+
if (richTextView.editorId != editorId && richTextView.editorEditText.editorId != editorId) {
|
|
1809
|
+
return
|
|
1810
|
+
}
|
|
1811
|
+
cancelPendingEditorUpdateRetry()
|
|
1812
|
+
clearPendingViewCommandUpdateRetry()
|
|
1813
|
+
cancelPendingThemeRetry()
|
|
1814
|
+
cancelPendingBlurRetry()
|
|
1815
|
+
cancelPendingDetachPreflightRetry()
|
|
1816
|
+
cancelPendingOutsideTapBlur()
|
|
1817
|
+
cancelPendingKeyboardDismiss()
|
|
1818
|
+
cancelPendingToolbarRefocus()
|
|
1819
|
+
cancelPendingPreflightWake()
|
|
1820
|
+
clearPendingNativeActionRetry()
|
|
1821
|
+
clearRecentToolbarTouch()
|
|
1822
|
+
uninstallOutsideTapBlurHandler()
|
|
1823
|
+
detachKeyboardToolbarIfNeeded()
|
|
1824
|
+
richTextView.setViewportBottomInsetPx(0)
|
|
1825
|
+
val editText = richTextView.editorEditText
|
|
1826
|
+
if (editText.hasFocus()) {
|
|
1827
|
+
editText.clearFocus()
|
|
1828
|
+
}
|
|
1829
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1830
|
+
imm?.hideSoftInputFromWindow(editText.windowToken, 0)
|
|
1831
|
+
clearMentionQueryState(resetLastEvent = true)
|
|
1832
|
+
pendingEditorUpdateJson = null
|
|
1833
|
+
pendingEditorUpdateEditorId = null
|
|
1834
|
+
pendingEditorUpdateRevision = 0
|
|
1835
|
+
appliedEditorUpdateRevision = 0
|
|
1836
|
+
pendingEditorResetUpdateJson = null
|
|
1837
|
+
pendingEditorResetUpdateEditorId = null
|
|
1838
|
+
pendingEditorResetUpdateRevision = 0
|
|
1839
|
+
appliedEditorResetUpdateRevision = 0
|
|
1840
|
+
lastEditorResetUpdateJsonProp = null
|
|
1841
|
+
lastEditorResetUpdateEditorIdProp = null
|
|
1842
|
+
lastDocumentVersion = null
|
|
1843
|
+
lastReadyEditorId = null
|
|
1844
|
+
toolbarState = NativeToolbarState.empty
|
|
1845
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
1846
|
+
keyboardToolbarView.visibility = View.GONE
|
|
1847
|
+
richTextView.editorId = 0L
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
private fun handleDestroyedCurrentEditorIfNeeded(): Boolean {
|
|
1851
|
+
val editorId = richTextView.editorId.takeIf { it != 0L }
|
|
1852
|
+
?: richTextView.editorEditText.editorId.takeIf { it != 0L }
|
|
1853
|
+
?: return false
|
|
1854
|
+
if (!NativeEditorViewRegistry.isDestroyed(editorId)) return false
|
|
1855
|
+
handleEditorDestroyed(editorId)
|
|
1856
|
+
return true
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
private fun handleAttachedToWindow() {
|
|
1860
|
+
isAttachedToNativeWindow = true
|
|
1861
|
+
cancelPendingDetachPreflightRetry()
|
|
1862
|
+
richTextView.clearDeferredEditorUnbind()
|
|
1863
|
+
val editorId = richTextView.editorId
|
|
1864
|
+
if (editorId == 0L) return
|
|
1865
|
+
if (NativeEditorViewRegistry.isDestroyed(editorId)) {
|
|
1866
|
+
handleEditorDestroyed(editorId)
|
|
1867
|
+
return
|
|
1868
|
+
}
|
|
1869
|
+
if (!NativeEditorViewRegistry.register(editorId, this)) {
|
|
1870
|
+
handleEditorDestroyed(editorId)
|
|
1871
|
+
return
|
|
1872
|
+
}
|
|
1873
|
+
richTextView.rebindEditorIfNeeded(
|
|
1874
|
+
notifyListener = !hasPendingEditorResetUpdateForEditor(editorId) &&
|
|
1875
|
+
!hasPendingEditorUpdateForEditor(editorId)
|
|
1876
|
+
)
|
|
1877
|
+
if (hasPendingTheme) {
|
|
1878
|
+
pendingThemeRetryEditorId = editorId
|
|
1879
|
+
}
|
|
1880
|
+
applyPendingEditorResetUpdateIfNeeded()
|
|
1881
|
+
applyPendingEditorUpdateIfNeeded()
|
|
1882
|
+
applyPendingThemeIfNeeded()
|
|
1883
|
+
refreshReadyStateIfSettled()
|
|
1884
|
+
applyAutoFocusIfNeeded()
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
private fun emitEditorReady(editorUpdateRevision: Int? = null): Boolean {
|
|
1888
|
+
val editorId = richTextView.editorId
|
|
1889
|
+
if (editorId == 0L) return false
|
|
1890
|
+
if (!isAttachedToNativeWindow) return false
|
|
1891
|
+
if (richTextView.editorEditText.editorId != editorId) return false
|
|
1892
|
+
if (hasPendingEditorResetUpdateForCurrentEditor()) return false
|
|
1893
|
+
if (hasPendingEditorUpdateForCurrentEditor()) return false
|
|
1894
|
+
lastReadyEditorId = editorId
|
|
1895
|
+
val payload = mutableMapOf<String, Any>("editorId" to editorId)
|
|
1896
|
+
editorUpdateRevision?.let { payload["editorUpdateRevision"] = it }
|
|
1897
|
+
onEditorReadyForTesting?.invoke(payload) ?: onEditorReady(payload)
|
|
1898
|
+
return true
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
private fun emitEditorReadyIfNeeded() {
|
|
1902
|
+
val editorId = richTextView.editorId
|
|
1903
|
+
if (lastReadyEditorId == editorId) return
|
|
1904
|
+
emitEditorReady()
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
override fun onDetachedFromWindow() {
|
|
1908
|
+
prepareForDetachFromWindow()
|
|
1909
|
+
super.onDetachedFromWindow()
|
|
1910
|
+
handleDetachedFromWindow()
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
private fun prepareForDetachFromWindow() {
|
|
1914
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1915
|
+
val editorId = richTextView.editorId
|
|
1916
|
+
if (editorId == 0L || richTextView.editorEditText.editorId == 0L) return
|
|
1917
|
+
if (richTextView.editorEditText.prepareForExternalEditorUpdate()) {
|
|
1918
|
+
cancelPendingDetachPreflightRetry()
|
|
1919
|
+
richTextView.clearDeferredEditorUnbind()
|
|
1920
|
+
return
|
|
1921
|
+
}
|
|
1922
|
+
richTextView.deferEditorUnbindOnNextDetach()
|
|
1923
|
+
schedulePendingDetachPreflightRetry(editorId)
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
private fun schedulePendingDetachPreflightRetry(editorId: Long) {
|
|
1927
|
+
if (pendingDetachPreflightRetryScheduled) return
|
|
1928
|
+
if (pendingDetachPreflightRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS) {
|
|
1929
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1930
|
+
richTextView.editorEditText.restoreAuthorizedTextIfNeeded()
|
|
1931
|
+
cancelPendingDetachPreflightRetry()
|
|
1932
|
+
richTextView.unbindEditorForDetachedViewIfNeeded()
|
|
1933
|
+
return
|
|
1934
|
+
}
|
|
1935
|
+
pendingDetachPreflightRetryAttempts += 1
|
|
1936
|
+
pendingDetachPreflightRetryEditorId = editorId
|
|
1937
|
+
pendingDetachPreflightRetryScheduled = true
|
|
1938
|
+
pendingDetachPreflightRetryGeneration += 1
|
|
1939
|
+
val retryGeneration = pendingDetachPreflightRetryGeneration
|
|
1940
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingDetachPreflightRetryAttempts
|
|
1941
|
+
mainHandler.postDelayed({
|
|
1942
|
+
if (retryGeneration != pendingDetachPreflightRetryGeneration) return@postDelayed
|
|
1943
|
+
pendingDetachPreflightRetryScheduled = false
|
|
1944
|
+
if (isAttachedToNativeWindow || pendingDetachPreflightRetryEditorId != richTextView.editorId) {
|
|
1945
|
+
cancelPendingDetachPreflightRetry()
|
|
1946
|
+
return@postDelayed
|
|
1947
|
+
}
|
|
1948
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return@postDelayed
|
|
1949
|
+
if (richTextView.editorEditText.prepareForExternalEditorUpdate()) {
|
|
1950
|
+
cancelPendingDetachPreflightRetry()
|
|
1951
|
+
richTextView.unbindEditorForDetachedViewIfNeeded()
|
|
1952
|
+
return@postDelayed
|
|
1953
|
+
}
|
|
1954
|
+
schedulePendingDetachPreflightRetry(editorId)
|
|
1955
|
+
}, delayMs)
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
private fun cancelPendingDetachPreflightRetry() {
|
|
1959
|
+
pendingDetachPreflightRetryScheduled = false
|
|
1960
|
+
pendingDetachPreflightRetryEditorId = null
|
|
1961
|
+
pendingDetachPreflightRetryAttempts = 0
|
|
1962
|
+
pendingDetachPreflightRetryGeneration += 1
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
private fun handleDetachedFromWindow() {
|
|
1966
|
+
isAttachedToNativeWindow = false
|
|
1967
|
+
NativeEditorViewRegistry.unregister(
|
|
1968
|
+
richTextView.editorId,
|
|
1969
|
+
this,
|
|
1970
|
+
blockCommandsUntilRegistered = true
|
|
1971
|
+
)
|
|
1972
|
+
cancelPendingOutsideTapBlur()
|
|
1973
|
+
cancelPendingKeyboardDismiss()
|
|
1974
|
+
cancelPendingToolbarRefocus()
|
|
1975
|
+
cancelPendingBlurRetry()
|
|
1976
|
+
cancelPendingEditorUpdateRetry()
|
|
1977
|
+
clearPendingViewCommandUpdateRetry()
|
|
1978
|
+
cancelPendingThemeRetry()
|
|
1979
|
+
clearPendingNativeActionRetry()
|
|
1980
|
+
cancelPendingPreflightWake()
|
|
1981
|
+
lastReadyEditorId = null
|
|
1982
|
+
uninstallOutsideTapBlurHandler()
|
|
1983
|
+
currentImeBottom = 0
|
|
1984
|
+
keyboardToolbarView.visibility = View.GONE
|
|
1985
|
+
detachKeyboardToolbarIfNeeded()
|
|
1986
|
+
richTextView.setViewportBottomInsetPx(0)
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
1990
|
+
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) {
|
|
1991
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
1992
|
+
return
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
val childWidthSpec = getChildMeasureSpec(
|
|
1996
|
+
widthMeasureSpec,
|
|
1997
|
+
paddingLeft + paddingRight,
|
|
1998
|
+
richTextView.layoutParams.width
|
|
1999
|
+
)
|
|
2000
|
+
val childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
2001
|
+
richTextView.measure(childWidthSpec, childHeightSpec)
|
|
2002
|
+
|
|
2003
|
+
val measuredWidth = resolveSize(
|
|
2004
|
+
richTextView.measuredWidth + paddingLeft + paddingRight,
|
|
2005
|
+
widthMeasureSpec
|
|
2006
|
+
)
|
|
2007
|
+
val desiredHeight = richTextView.measuredHeight + paddingTop + paddingBottom
|
|
2008
|
+
val measuredHeight = when (MeasureSpec.getMode(heightMeasureSpec)) {
|
|
2009
|
+
MeasureSpec.AT_MOST -> desiredHeight.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec))
|
|
2010
|
+
else -> desiredHeight
|
|
2011
|
+
}
|
|
2012
|
+
setMeasuredDimension(measuredWidth, measuredHeight)
|
|
2013
|
+
emitContentHeightIfNeeded(force = false)
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
private fun emitContentHeightIfNeeded(force: Boolean) {
|
|
2017
|
+
if (heightBehavior != EditorHeightBehavior.AUTO_GROW) return
|
|
2018
|
+
val editText = richTextView.editorEditText
|
|
2019
|
+
val resolvedEditHeight = editText.resolveAutoGrowHeight()
|
|
2020
|
+
val resolvedContainerHeight =
|
|
2021
|
+
resolvedEditHeight +
|
|
2022
|
+
richTextView.paddingTop +
|
|
2023
|
+
richTextView.paddingBottom +
|
|
2024
|
+
paddingTop +
|
|
2025
|
+
paddingBottom
|
|
2026
|
+
val contentHeight = (
|
|
2027
|
+
when {
|
|
2028
|
+
editText.isLaidOut && (editText.layout?.height ?: 0) > 0 -> {
|
|
2029
|
+
maxOf(
|
|
2030
|
+
(editText.layout?.height ?: 0) +
|
|
2031
|
+
editText.compoundPaddingTop +
|
|
2032
|
+
editText.compoundPaddingBottom +
|
|
2033
|
+
richTextView.paddingTop +
|
|
2034
|
+
richTextView.paddingBottom +
|
|
2035
|
+
paddingTop +
|
|
2036
|
+
paddingBottom,
|
|
2037
|
+
resolvedContainerHeight
|
|
2038
|
+
)
|
|
2039
|
+
}
|
|
2040
|
+
richTextView.measuredHeight > 0 -> {
|
|
2041
|
+
maxOf(
|
|
2042
|
+
richTextView.measuredHeight + paddingTop + paddingBottom,
|
|
2043
|
+
resolvedContainerHeight
|
|
2044
|
+
)
|
|
2045
|
+
}
|
|
2046
|
+
editText.measuredHeight > 0 -> {
|
|
2047
|
+
maxOf(
|
|
2048
|
+
editText.measuredHeight +
|
|
2049
|
+
richTextView.paddingTop +
|
|
2050
|
+
richTextView.paddingBottom +
|
|
2051
|
+
paddingTop +
|
|
2052
|
+
paddingBottom,
|
|
2053
|
+
resolvedContainerHeight
|
|
2054
|
+
)
|
|
2055
|
+
}
|
|
2056
|
+
else -> {
|
|
2057
|
+
resolvedContainerHeight
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
).coerceAtLeast(0)
|
|
2061
|
+
if (contentHeight <= 0) return
|
|
2062
|
+
val editorId = richTextView.editorId
|
|
2063
|
+
if (
|
|
2064
|
+
!force &&
|
|
2065
|
+
contentHeight == lastEmittedContentHeight &&
|
|
2066
|
+
editorId == lastEmittedContentHeightEditorId
|
|
2067
|
+
) {
|
|
2068
|
+
return
|
|
2069
|
+
}
|
|
2070
|
+
lastEmittedContentHeight = contentHeight
|
|
2071
|
+
lastEmittedContentHeightEditorId = editorId
|
|
2072
|
+
val event = mapOf(
|
|
2073
|
+
"contentHeight" to contentHeight,
|
|
2074
|
+
"editorId" to editorId
|
|
2075
|
+
)
|
|
2076
|
+
onContentHeightChangeForTesting?.invoke(event) ?: onContentHeightChange(event)
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
/** Applies an editor update from JS without echoing it back through events. */
|
|
2080
|
+
fun applyEditorUpdate(updateJson: String): Boolean =
|
|
2081
|
+
applyEditorUpdate(updateJson, scheduleViewCommandRetry = true)
|
|
2082
|
+
|
|
2083
|
+
/** Applies a reset-style update from JS, discarding pending native composition. */
|
|
2084
|
+
fun applyEditorResetUpdate(updateJson: String): Boolean {
|
|
2085
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2086
|
+
val postedEditorId = richTextView.editorId
|
|
2087
|
+
val apply = Runnable {
|
|
2088
|
+
if (postedEditorId != richTextView.editorId) return@Runnable
|
|
2089
|
+
applyEditorResetUpdate(updateJson)
|
|
2090
|
+
}
|
|
2091
|
+
if (!post(apply)) {
|
|
2092
|
+
richTextView.post(apply)
|
|
2093
|
+
}
|
|
2094
|
+
return false
|
|
2095
|
+
}
|
|
2096
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
2097
|
+
return false
|
|
2098
|
+
}
|
|
2099
|
+
if (!isEditorReadyForNativeUpdate()) {
|
|
2100
|
+
return false
|
|
2101
|
+
}
|
|
2102
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
2103
|
+
clearPendingViewCommandUpdateRetry()
|
|
2104
|
+
isApplyingJSUpdate = true
|
|
2105
|
+
val applied = try {
|
|
2106
|
+
richTextView.editorEditText.applyUpdateJSON(
|
|
2107
|
+
updateJson,
|
|
2108
|
+
refreshInputConnectionForExternalUpdate = true
|
|
2109
|
+
)
|
|
2110
|
+
clearPendingEditorUpdateDispatchQueue("jsResetUpdate")
|
|
2111
|
+
true
|
|
2112
|
+
} catch (error: Throwable) {
|
|
2113
|
+
Log.w(LOG_TAG, "Failed to apply JS editor reset update", error)
|
|
2114
|
+
false
|
|
2115
|
+
} finally {
|
|
2116
|
+
isApplyingJSUpdate = false
|
|
2117
|
+
}
|
|
2118
|
+
if (applied) {
|
|
2119
|
+
refreshReadyStateIfSettled()
|
|
2120
|
+
}
|
|
2121
|
+
return applied
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
private fun isEditorReadyForNativeUpdate(): Boolean {
|
|
2125
|
+
val editorId = richTextView.editorId
|
|
2126
|
+
return editorId == 0L || (isAttachedToNativeWindow && richTextView.editorEditText.editorId == editorId)
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
private fun applyEditorUpdate(
|
|
2130
|
+
updateJson: String,
|
|
2131
|
+
scheduleViewCommandRetry: Boolean,
|
|
2132
|
+
expectedEditorId: Long? = null
|
|
2133
|
+
): Boolean {
|
|
2134
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2135
|
+
val postedEditorId = expectedEditorId ?: richTextView.editorId
|
|
2136
|
+
val apply = Runnable {
|
|
2137
|
+
if (postedEditorId != richTextView.editorId) return@Runnable
|
|
2138
|
+
applyEditorUpdate(updateJson, scheduleViewCommandRetry, postedEditorId)
|
|
2139
|
+
}
|
|
2140
|
+
if (!post(apply)) {
|
|
2141
|
+
richTextView.post(apply)
|
|
2142
|
+
}
|
|
2143
|
+
return false
|
|
2144
|
+
}
|
|
2145
|
+
if (expectedEditorId != null && expectedEditorId != richTextView.editorId) {
|
|
2146
|
+
return false
|
|
2147
|
+
}
|
|
2148
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
2149
|
+
return false
|
|
2150
|
+
}
|
|
2151
|
+
if (!isEditorReadyForNativeUpdate()) {
|
|
2152
|
+
if (scheduleViewCommandRetry) {
|
|
2153
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
2154
|
+
}
|
|
2155
|
+
return false
|
|
2156
|
+
}
|
|
2157
|
+
if (
|
|
2158
|
+
blockEditorUpdatePreflightForTesting ||
|
|
2159
|
+
!richTextView.editorEditText.prepareForExternalEditorUpdate()
|
|
2160
|
+
) {
|
|
2161
|
+
if (scheduleViewCommandRetry) {
|
|
2162
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
2163
|
+
}
|
|
2164
|
+
return false
|
|
2165
|
+
}
|
|
2166
|
+
isApplyingJSUpdate = true
|
|
2167
|
+
return try {
|
|
2168
|
+
richTextView.editorEditText.applyUpdateJSON(
|
|
2169
|
+
updateJson,
|
|
2170
|
+
refreshInputConnectionForExternalUpdate = true
|
|
2171
|
+
)
|
|
2172
|
+
clearPendingEditorUpdateDispatchQueue("jsUpdate")
|
|
2173
|
+
true
|
|
2174
|
+
} catch (error: Throwable) {
|
|
2175
|
+
Log.w(LOG_TAG, "Failed to apply JS editor update", error)
|
|
2176
|
+
if (scheduleViewCommandRetry) {
|
|
2177
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
2178
|
+
}
|
|
2179
|
+
false
|
|
2180
|
+
} finally {
|
|
2181
|
+
isApplyingJSUpdate = false
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
fun prepareForEditorCommandJSON(): String {
|
|
2186
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
2187
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
2188
|
+
ready = false,
|
|
2189
|
+
blockedReason = "unknown"
|
|
2190
|
+
)
|
|
2191
|
+
}
|
|
2192
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
2193
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
2194
|
+
ready = false,
|
|
2195
|
+
blockedReason = "destroyed"
|
|
2196
|
+
)
|
|
2197
|
+
}
|
|
2198
|
+
if (richTextView.editorId != 0L && !isAttachedToNativeWindow) {
|
|
2199
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
2200
|
+
ready = false,
|
|
2201
|
+
blockedReason = "detached"
|
|
2202
|
+
)
|
|
2203
|
+
}
|
|
2204
|
+
if (richTextView.editorId != 0L && richTextView.editorEditText.editorId != richTextView.editorId) {
|
|
2205
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
2206
|
+
ready = false,
|
|
2207
|
+
blockedReason = "detached"
|
|
2208
|
+
)
|
|
2209
|
+
}
|
|
2210
|
+
if (shouldBlockEditorCommandForPendingUpdate()) {
|
|
2211
|
+
return pendingEditorUpdateCommandPreparationJSON()
|
|
2212
|
+
}
|
|
2213
|
+
isApplyingJSUpdate = true
|
|
2214
|
+
return try {
|
|
2215
|
+
onBeforePrepareForEditorCommandForTesting?.invoke()
|
|
2216
|
+
val preparation = richTextView.editorEditText.prepareForExternalEditorCommand()
|
|
2217
|
+
NativeEditorViewRegistry.commandPreparationJSON(
|
|
2218
|
+
ready = preparation.ready,
|
|
2219
|
+
updateJSON = preparation.updateJSON,
|
|
2220
|
+
blockedReason = if (preparation.ready) null else "composition"
|
|
2221
|
+
)
|
|
2222
|
+
} finally {
|
|
2223
|
+
isApplyingJSUpdate = false
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
override fun onSelectionChanged(anchor: Int, head: Int) {
|
|
2228
|
+
val stateJson = refreshToolbarStateFromEditorSelection()
|
|
2229
|
+
refreshMentionQuery()
|
|
2230
|
+
clearPendingNativeActionRetryIfScopeChanged()
|
|
2231
|
+
schedulePendingPreflightWake()
|
|
2232
|
+
richTextView.refreshRemoteSelections()
|
|
2233
|
+
val event = mutableMapOf<String, Any>(
|
|
2234
|
+
"anchor" to anchor,
|
|
2235
|
+
"head" to head,
|
|
2236
|
+
"editorId" to richTextView.editorId
|
|
2237
|
+
)
|
|
2238
|
+
lastDocumentVersion?.let {
|
|
2239
|
+
event["documentVersion"] = it
|
|
2240
|
+
}
|
|
2241
|
+
if (stateJson != null) {
|
|
2242
|
+
event["stateJson"] = stateJson
|
|
2243
|
+
}
|
|
2244
|
+
onSelectionChangeForTesting?.invoke(event) ?: onSelectionChange(event)
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
override fun onEditorUpdate(updateJSON: String) {
|
|
2248
|
+
if (isApplyingJSUpdate) {
|
|
2249
|
+
dispatchEditorUpdate(
|
|
2250
|
+
PendingEditorUpdateEvent(
|
|
2251
|
+
editorId = richTextView.editorId,
|
|
2252
|
+
updateJSON = updateJSON
|
|
2253
|
+
),
|
|
2254
|
+
emitToJS = false
|
|
2255
|
+
)
|
|
2256
|
+
return
|
|
2257
|
+
}
|
|
2258
|
+
pendingEditorUpdateEvents.addLast(
|
|
2259
|
+
PendingEditorUpdateEvent(
|
|
2260
|
+
editorId = richTextView.editorId,
|
|
2261
|
+
updateJSON = updateJSON
|
|
2262
|
+
)
|
|
2263
|
+
)
|
|
2264
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
2265
|
+
"nativeViewEditorUpdateQueued",
|
|
2266
|
+
"queue=${pendingEditorUpdateEvents.size} jsonLength=${updateJSON.length}"
|
|
2267
|
+
)
|
|
2268
|
+
schedulePendingEditorUpdateDispatch()
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
internal fun pendingEditorUpdateEventCountForTesting(): Int =
|
|
2272
|
+
pendingEditorUpdateEvents.size
|
|
2273
|
+
|
|
2274
|
+
private fun schedulePendingEditorUpdateDispatch() {
|
|
2275
|
+
pendingEditorUpdateDispatchScheduled = true
|
|
2276
|
+
val generation = ++pendingEditorUpdateDispatchGeneration
|
|
2277
|
+
mainHandler.postDelayed({
|
|
2278
|
+
if (generation != pendingEditorUpdateDispatchGeneration) return@postDelayed
|
|
2279
|
+
pendingEditorUpdateDispatchScheduled = false
|
|
2280
|
+
drainPendingEditorUpdateEvents()
|
|
2281
|
+
}, EDITOR_UPDATE_EVENT_DEBOUNCE_MS)
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
private fun drainPendingEditorUpdateEvents() {
|
|
2285
|
+
if (pendingEditorUpdateEvents.isEmpty()) return
|
|
2286
|
+
val startedAt = System.nanoTime()
|
|
2287
|
+
var drainedCount = 0
|
|
2288
|
+
while (pendingEditorUpdateEvents.isNotEmpty()) {
|
|
2289
|
+
val event = pendingEditorUpdateEvents.removeFirst()
|
|
2290
|
+
if (event.editorId != richTextView.editorId) {
|
|
2291
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
2292
|
+
"nativeViewEditorUpdateSkipped",
|
|
2293
|
+
"reason=staleEditor queuedEditor=${event.editorId} currentEditor=${richTextView.editorId}"
|
|
2294
|
+
)
|
|
2295
|
+
continue
|
|
2296
|
+
}
|
|
2297
|
+
dispatchEditorUpdate(event, emitToJS = true)
|
|
2298
|
+
drainedCount += 1
|
|
2299
|
+
}
|
|
2300
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
2301
|
+
"nativeViewEditorUpdateDrained",
|
|
2302
|
+
"count=$drainedCount totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
2303
|
+
)
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
private fun clearPendingEditorUpdateDispatchQueue(reason: String) {
|
|
2307
|
+
if (pendingEditorUpdateEvents.isEmpty() && !pendingEditorUpdateDispatchScheduled) return
|
|
2308
|
+
val clearedCount = pendingEditorUpdateEvents.size
|
|
2309
|
+
pendingEditorUpdateEvents.clear()
|
|
2310
|
+
pendingEditorUpdateDispatchScheduled = false
|
|
2311
|
+
pendingEditorUpdateDispatchGeneration += 1
|
|
2312
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
2313
|
+
"nativeViewEditorUpdateQueueCleared",
|
|
2314
|
+
"reason=$reason count=$clearedCount"
|
|
2315
|
+
)
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
private fun dispatchEditorUpdate(event: PendingEditorUpdateEvent, emitToJS: Boolean) {
|
|
2319
|
+
val updateJSON = event.updateJSON
|
|
2320
|
+
val startedAt = System.nanoTime()
|
|
2321
|
+
noteDocumentVersionFromUpdateJSON(updateJSON)
|
|
2322
|
+
val noteNanos = System.nanoTime() - startedAt
|
|
2323
|
+
val toolbarStartedAt = System.nanoTime()
|
|
2324
|
+
NativeToolbarState.fromUpdateJson(updateJSON)?.let { state ->
|
|
2325
|
+
toolbarState = state
|
|
2326
|
+
keyboardToolbarView.applyState(state)
|
|
2327
|
+
}
|
|
2328
|
+
val toolbarNanos = System.nanoTime() - toolbarStartedAt
|
|
2329
|
+
val mentionStartedAt = System.nanoTime()
|
|
2330
|
+
refreshMentionQuery()
|
|
2331
|
+
val mentionNanos = System.nanoTime() - mentionStartedAt
|
|
2332
|
+
val retryStartedAt = System.nanoTime()
|
|
2333
|
+
clearPendingNativeActionRetryIfScopeChanged()
|
|
2334
|
+
schedulePendingPreflightWake()
|
|
2335
|
+
richTextView.refreshRemoteSelections()
|
|
2336
|
+
val retryNanos = System.nanoTime() - retryStartedAt
|
|
2337
|
+
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
2338
|
+
post {
|
|
2339
|
+
requestLayout()
|
|
2340
|
+
emitContentHeightIfNeeded(force = false)
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
val emitStartedAt = System.nanoTime()
|
|
2344
|
+
if (emitToJS) {
|
|
2345
|
+
val payload = mapOf<String, Any>(
|
|
2346
|
+
"updateJson" to updateJSON,
|
|
2347
|
+
"editorId" to event.editorId
|
|
2348
|
+
)
|
|
2349
|
+
onEditorUpdateForTesting?.invoke(payload) ?: onEditorUpdate(payload)
|
|
2350
|
+
}
|
|
2351
|
+
val totalNanos = System.nanoTime() - startedAt
|
|
2352
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
2353
|
+
"nativeViewEditorUpdateDispatch",
|
|
2354
|
+
"emitToJS=$emitToJS jsonLength=${updateJSON.length} noteUs=${nanosToMicros(noteNanos)} toolbarUs=${nanosToMicros(toolbarNanos)} mentionUs=${nanosToMicros(mentionNanos)} retryUs=${nanosToMicros(retryNanos)} emitUs=${nanosToMicros(System.nanoTime() - emitStartedAt)} totalUs=${nanosToMicros(totalNanos)}"
|
|
2355
|
+
)
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
private fun installOutsideTapBlurHandlerIfNeeded() {
|
|
2359
|
+
val window = resolveActivity(context)?.window ?: return
|
|
2360
|
+
if (outsideTapWindow !== window) {
|
|
2361
|
+
uninstallOutsideTapBlurHandler()
|
|
2362
|
+
}
|
|
2363
|
+
if (NativeEditorOutsideTapDispatcher.register(window, this)) {
|
|
2364
|
+
outsideTapWindow = window
|
|
2365
|
+
} else if (outsideTapWindow === window) {
|
|
2366
|
+
outsideTapWindow = null
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
private fun scheduleOutsideTapBlurHandlerInstallRetry() {
|
|
2371
|
+
cancelPendingOutsideTapBlurHandlerInstallRetry()
|
|
2372
|
+
val retry = Runnable {
|
|
2373
|
+
pendingOutsideTapHandlerInstallRetry = null
|
|
2374
|
+
if (richTextView.editorEditText.hasFocus()) {
|
|
2375
|
+
installOutsideTapBlurHandlerIfNeeded()
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
pendingOutsideTapHandlerInstallRetry = retry
|
|
2379
|
+
richTextView.editorEditText.postDelayed(retry, OUTSIDE_TAP_HANDLER_INSTALL_RETRY_DELAY_MS)
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
private fun cancelPendingOutsideTapBlurHandlerInstallRetry() {
|
|
2383
|
+
pendingOutsideTapHandlerInstallRetry?.let {
|
|
2384
|
+
richTextView.editorEditText.removeCallbacks(it)
|
|
2385
|
+
pendingOutsideTapHandlerInstallRetry = null
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
private fun uninstallOutsideTapBlurHandler() {
|
|
2390
|
+
cancelPendingOutsideTapBlurHandlerInstallRetry()
|
|
2391
|
+
val window = outsideTapWindow ?: return
|
|
2392
|
+
NativeEditorOutsideTapDispatcher.unregister(window, this)
|
|
2393
|
+
outsideTapWindow = null
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
internal fun prepareOutsideTapDecisionForWindowEvent(event: MotionEvent): NativeEditorOutsideTapDecision {
|
|
2397
|
+
if (!isAttachedToNativeWindow) {
|
|
2398
|
+
traceOutsideTap("decision ignored detached")
|
|
2399
|
+
return NativeEditorOutsideTapDecision.IGNORE
|
|
2400
|
+
}
|
|
2401
|
+
if (event.action != MotionEvent.ACTION_DOWN) {
|
|
2402
|
+
traceOutsideTap("decision ignored action=${event.action}")
|
|
2403
|
+
return NativeEditorOutsideTapDecision.IGNORE
|
|
2404
|
+
}
|
|
2405
|
+
if (!isEditorFocusedForOutsideTapDecision()) {
|
|
2406
|
+
traceOutsideTap("decision ignored not focused")
|
|
2407
|
+
return NativeEditorOutsideTapDecision.IGNORE
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
val decision = if (isTouchOutsideEditor(event)) {
|
|
2411
|
+
NativeEditorOutsideTapDecision.OUTSIDE_EDITOR
|
|
2412
|
+
} else {
|
|
2413
|
+
NativeEditorOutsideTapDecision.PRESERVE_FOCUS
|
|
2414
|
+
}
|
|
2415
|
+
traceOutsideTap("decision raw=${event.rawX.toInt()},${event.rawY.toInt()} value=$decision")
|
|
2416
|
+
return decision
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
internal fun handleOutsideTapDecisionFromWindowDispatcher(decision: NativeEditorOutsideTapDecision) {
|
|
2420
|
+
traceOutsideTap("handle decision=$decision")
|
|
2421
|
+
when (decision) {
|
|
2422
|
+
NativeEditorOutsideTapDecision.IGNORE -> {
|
|
2423
|
+
if (!richTextView.editorEditText.hasFocus()) {
|
|
2424
|
+
cancelPendingOutsideTapBlur()
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
NativeEditorOutsideTapDecision.PRESERVE_FOCUS -> cancelPendingOutsideTapBlur()
|
|
2428
|
+
NativeEditorOutsideTapDecision.OUTSIDE_EDITOR -> {
|
|
2429
|
+
clearRecentToolbarTouch()
|
|
2430
|
+
cancelPendingToolbarRefocus()
|
|
2431
|
+
scheduleOutsideTapBlur()
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
internal fun scheduleOutsideTapBlurFromWindowDispatcher() {
|
|
2437
|
+
scheduleOutsideTapBlur()
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
internal fun cancelOutsideTapBlurFromWindowDispatcher() {
|
|
2441
|
+
cancelPendingOutsideTapBlur()
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
private fun isEditorFocusedForOutsideTapDecision(): Boolean =
|
|
2445
|
+
editorFocusedForOutsideTapOverrideForTesting ?: richTextView.editorEditText.hasFocus()
|
|
2446
|
+
|
|
2447
|
+
private fun isTouchOutsideEditor(event: MotionEvent): Boolean {
|
|
2448
|
+
if (isTouchInsideKeyboardToolbar(event)) {
|
|
2449
|
+
markRecentToolbarTouch()
|
|
2450
|
+
return false
|
|
2451
|
+
}
|
|
2452
|
+
if (isTouchInsideStandaloneToolbar(event)) {
|
|
2453
|
+
markRecentToolbarTouch()
|
|
2454
|
+
return false
|
|
2455
|
+
}
|
|
2456
|
+
val rect = Rect()
|
|
2457
|
+
richTextView.editorEditText.getGlobalVisibleRect(rect)
|
|
2458
|
+
val isOutside = !rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
2459
|
+
if (isOutside) {
|
|
2460
|
+
clearRecentToolbarTouch()
|
|
2461
|
+
}
|
|
2462
|
+
return isOutside
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
private fun markRecentToolbarTouch() {
|
|
2466
|
+
lastToolbarTouchUptimeMs = SystemClock.uptimeMillis()
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
private fun clearRecentToolbarTouch() {
|
|
2470
|
+
lastToolbarTouchUptimeMs = null
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
private fun shouldPreserveFocusAfterToolbarTouch(): Boolean {
|
|
2474
|
+
val lastToolbarTouch = lastToolbarTouchUptimeMs ?: return false
|
|
2475
|
+
val elapsedMs = SystemClock.uptimeMillis() - lastToolbarTouch
|
|
2476
|
+
return elapsedMs in 0L..TOOLBAR_FOCUS_PRESERVE_MS
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
private fun consumeToolbarFocusPreservationForBlur(): Boolean {
|
|
2480
|
+
if (!shouldPreserveFocusAfterToolbarTouch()) {
|
|
2481
|
+
return false
|
|
2482
|
+
}
|
|
2483
|
+
clearRecentToolbarTouch()
|
|
2484
|
+
return true
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
internal fun markRecentToolbarTouchForTesting() {
|
|
2488
|
+
markRecentToolbarTouch()
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
internal fun shouldPreserveFocusAfterToolbarTouchForTesting(): Boolean =
|
|
2492
|
+
shouldPreserveFocusAfterToolbarTouch()
|
|
2493
|
+
|
|
2494
|
+
internal fun setEditorFocusedForOutsideTapDecisionForTesting(isFocused: Boolean?) {
|
|
2495
|
+
editorFocusedForOutsideTapOverrideForTesting = isFocused
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
internal fun setAttachedToNativeWindowForTesting(isAttached: Boolean) {
|
|
2499
|
+
isAttachedToNativeWindow = isAttached
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
internal fun handleAttachedToWindowForTesting() {
|
|
2503
|
+
handleAttachedToWindow()
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
internal fun traceOutsideTap(message: String) {
|
|
2507
|
+
onOutsideTapTraceForTesting?.invoke(message)
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
internal fun handleDetachedFromWindowForTesting() {
|
|
2511
|
+
prepareForDetachFromWindow()
|
|
2512
|
+
handleDetachedFromWindow()
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
internal fun performBlurForTesting(deferKeyboardDismiss: Boolean = false) {
|
|
2516
|
+
performBlur(deferKeyboardDismiss = deferKeyboardDismiss, allowRetry = true)
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
internal fun pendingBlurRetryAttemptsForTesting(): Int = pendingBlurRetryAttempts
|
|
2520
|
+
|
|
2521
|
+
internal fun pendingDetachPreflightRetryAttemptsForTesting(): Int =
|
|
2522
|
+
pendingDetachPreflightRetryAttempts
|
|
2523
|
+
|
|
2524
|
+
internal fun hasPendingOutsideTapBlurForTesting(): Boolean = pendingOutsideTapBlur != null
|
|
2525
|
+
|
|
2526
|
+
internal fun isOutsideTapBlurHandlerInstalledForTesting(): Boolean = outsideTapWindow != null
|
|
2527
|
+
|
|
2528
|
+
internal fun hasPendingKeyboardDismissForTesting(): Boolean = pendingKeyboardDismiss != null
|
|
2529
|
+
|
|
2530
|
+
internal fun hasPendingPreflightWakeForTesting(): Boolean = pendingPreflightWakeScheduled
|
|
2531
|
+
|
|
2532
|
+
internal fun hasPendingToolbarRefocusForTesting(): Boolean = pendingToolbarRefocus != null
|
|
2533
|
+
|
|
2534
|
+
internal fun isKeyboardToolbarAttachedForTesting(): Boolean = keyboardToolbarView.parent != null
|
|
2535
|
+
|
|
2536
|
+
internal fun currentImeBottomForTesting(): Int = currentImeBottom
|
|
2537
|
+
|
|
2538
|
+
internal fun setCurrentImeBottomForTesting(bottom: Int) {
|
|
2539
|
+
currentImeBottom = bottom
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
internal fun updateAttachedKeyboardToolbarForInsetsForTesting() {
|
|
2543
|
+
updateAttachedKeyboardToolbarForInsets()
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
internal fun scheduleToolbarRefocusForTesting() {
|
|
2547
|
+
scheduleToolbarRefocus()
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
internal fun focusFromToolbarPreserveForTesting() {
|
|
2551
|
+
focusInternal(cancelPendingOutsideTapBlur = false)
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
internal fun applyAutoFocusForTesting() {
|
|
2555
|
+
applyAutoFocusIfNeeded()
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
internal fun installOutsideTapBlurHandlerForTesting() {
|
|
2559
|
+
installOutsideTapBlurHandlerIfNeeded()
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2562
|
+
internal fun uninstallOutsideTapBlurHandlerForTesting() {
|
|
2563
|
+
uninstallOutsideTapBlurHandler()
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
internal fun schedulePendingPreflightWakeForTesting() {
|
|
2567
|
+
schedulePendingPreflightWake()
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
internal fun hasPendingNativeActionForTesting(): Boolean = pendingNativeAction != null
|
|
2571
|
+
|
|
2572
|
+
internal fun pendingNativeActionRetryAttemptsForTesting(): Int = pendingNativeActionRetryAttempts
|
|
2573
|
+
|
|
2574
|
+
internal fun lastDocumentVersionForTesting(): Int? = lastDocumentVersion
|
|
2575
|
+
|
|
2576
|
+
internal fun setLastDocumentVersionForTesting(documentVersion: Int?) {
|
|
2577
|
+
lastDocumentVersion = documentVersion
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
internal fun refreshToolbarStateFromEditorSelectionForTesting(): String? =
|
|
2581
|
+
refreshToolbarStateFromEditorSelection()
|
|
2582
|
+
|
|
2583
|
+
internal fun handleToolbarItemPressForTesting(item: NativeToolbarItem) {
|
|
2584
|
+
handleToolbarItemPress(item)
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
internal fun insertMentionSuggestionForTesting(suggestion: NativeMentionSuggestion) {
|
|
2588
|
+
insertMentionSuggestion(suggestion)
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
internal fun wakePendingPreflightWorkForTesting() {
|
|
2592
|
+
wakePendingPreflightWork()
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
internal fun emitEditorReadyForTesting(editorUpdateRevision: Int? = null): Boolean =
|
|
2596
|
+
emitEditorReady(editorUpdateRevision)
|
|
2597
|
+
|
|
2598
|
+
internal fun pendingEditorUpdateJsonForTesting(): String? = pendingEditorUpdateJson
|
|
2599
|
+
|
|
2600
|
+
internal fun pendingEditorUpdateRevisionForTesting(): Int = pendingEditorUpdateRevision
|
|
2601
|
+
|
|
2602
|
+
internal fun pendingEditorResetUpdateJsonForTesting(): String? = pendingEditorResetUpdateJson
|
|
2603
|
+
|
|
2604
|
+
internal fun pendingEditorResetUpdateRevisionForTesting(): Int =
|
|
2605
|
+
pendingEditorResetUpdateRevision
|
|
2606
|
+
|
|
2607
|
+
internal fun setAppliedEditorUpdateRevisionForTesting(editorUpdateRevision: Int) {
|
|
2608
|
+
appliedEditorUpdateRevision = editorUpdateRevision
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
internal fun pendingEditorUpdateEditorIdForTesting(): Long? = pendingEditorUpdateEditorId
|
|
2612
|
+
|
|
2613
|
+
internal fun pendingEditorResetUpdateEditorIdForTesting(): Long? =
|
|
2614
|
+
pendingEditorResetUpdateEditorId
|
|
2615
|
+
|
|
2616
|
+
internal fun pendingViewCommandUpdateJsonForTesting(): String? = pendingViewCommandUpdateJson
|
|
2617
|
+
|
|
2618
|
+
internal fun pendingViewCommandUpdateRetryAttemptsForTesting(): Int =
|
|
2619
|
+
pendingViewCommandUpdateRetryAttempts
|
|
2620
|
+
|
|
2621
|
+
internal fun scheduleViewCommandUpdateRetryForTesting(updateJson: String) {
|
|
2622
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
internal fun pendingThemeJsonForTesting(): String? = pendingThemeJson.takeIf { hasPendingTheme }
|
|
2626
|
+
|
|
2627
|
+
internal fun lastThemeJsonForTesting(): String? = lastThemeJson
|
|
2628
|
+
|
|
2629
|
+
internal fun pendingThemeRetryAttemptsForTesting(): Int = pendingThemeRetryAttempts
|
|
2630
|
+
|
|
2631
|
+
internal fun applyPendingThemeForTesting() {
|
|
2632
|
+
applyPendingThemeIfNeeded()
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
private fun isTouchInsideStandaloneToolbar(event: MotionEvent): Boolean {
|
|
2636
|
+
val visibleWindowFrame = Rect()
|
|
2637
|
+
getWindowVisibleDisplayFrame(visibleWindowFrame)
|
|
2638
|
+
return isPointInsideStandaloneToolbar(event.rawX, event.rawY, visibleWindowFrame)
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
internal fun isPointInsideStandaloneToolbarForTesting(
|
|
2642
|
+
rawX: Float,
|
|
2643
|
+
rawY: Float,
|
|
2644
|
+
visibleWindowFrame: Rect
|
|
2645
|
+
): Boolean = isPointInsideStandaloneToolbar(rawX, rawY, visibleWindowFrame)
|
|
2646
|
+
|
|
2647
|
+
private fun isPointInsideStandaloneToolbar(
|
|
2648
|
+
rawX: Float,
|
|
2649
|
+
rawY: Float,
|
|
2650
|
+
visibleWindowFrame: Rect
|
|
2651
|
+
): Boolean {
|
|
2652
|
+
if (toolbarFramesInWindow.isEmpty()) {
|
|
2653
|
+
return false
|
|
2654
|
+
}
|
|
2655
|
+
// toolbarFrame is in DP from React Native's measureInWindow, while
|
|
2656
|
+
// rawX/rawY are screen pixels. Normalize the event into the visible
|
|
2657
|
+
// window before comparing so shifted fallback rectangles cannot
|
|
2658
|
+
// preserve focus for unrelated outside taps.
|
|
2659
|
+
val density = resources.displayMetrics.density
|
|
2660
|
+
val hitSlopPx = TOOLBAR_HIT_SLOP_DP * density
|
|
2661
|
+
val eventX = rawX - visibleWindowFrame.left
|
|
2662
|
+
val eventY = rawY - visibleWindowFrame.top
|
|
2663
|
+
for (toolbarFrame in toolbarFramesInWindow) {
|
|
2664
|
+
val windowFrameInPx = RectF(
|
|
2665
|
+
toolbarFrame.left * density,
|
|
2666
|
+
toolbarFrame.top * density,
|
|
2667
|
+
toolbarFrame.right * density,
|
|
2668
|
+
toolbarFrame.bottom * density
|
|
2669
|
+
).apply {
|
|
2670
|
+
inset(-hitSlopPx, -hitSlopPx)
|
|
2671
|
+
}
|
|
2672
|
+
if (windowFrameInPx.contains(eventX, eventY)) {
|
|
2673
|
+
return true
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
return false
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
private fun isTouchInsideKeyboardToolbar(event: MotionEvent): Boolean {
|
|
2680
|
+
if (keyboardToolbarView.parent == null || keyboardToolbarView.visibility != View.VISIBLE) {
|
|
2681
|
+
return false
|
|
2682
|
+
}
|
|
2683
|
+
val rect = Rect()
|
|
2684
|
+
keyboardToolbarView.getGlobalVisibleRect(rect)
|
|
2685
|
+
return rect.contains(event.rawX.toInt(), event.rawY.toInt())
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
private companion object {
|
|
2689
|
+
private const val TOOLBAR_HIT_SLOP_DP = 8f
|
|
2690
|
+
private const val TOOLBAR_FOCUS_PRESERVE_MS = 750L
|
|
2691
|
+
private const val OUTSIDE_TAP_BLUR_DELAY_MS = 100L
|
|
2692
|
+
private const val OUTSIDE_TAP_HANDLER_INSTALL_RETRY_DELAY_MS = 64L
|
|
2693
|
+
private const val NATIVE_ACTION_RETRY_DELAY_MS = 16L
|
|
2694
|
+
private const val EDITOR_UPDATE_EVENT_DEBOUNCE_MS = 64L
|
|
2695
|
+
private const val PENDING_UPDATE_RECOVERY_RETRY_DELAY_MS = 250L
|
|
2696
|
+
private const val MAX_NATIVE_ACTION_RETRY_ATTEMPTS = 3
|
|
2697
|
+
private const val MAX_PENDING_UPDATE_RETRY_ATTEMPTS = 5
|
|
2698
|
+
private const val LOG_TAG = "NativeEditor"
|
|
2699
|
+
|
|
2700
|
+
private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
private fun resolveActivity(context: Context): Activity? {
|
|
2704
|
+
appContext.currentActivity?.let { return it }
|
|
2705
|
+
var current: Context? = context
|
|
2706
|
+
while (current is ContextWrapper) {
|
|
2707
|
+
if (current is Activity) return current
|
|
2708
|
+
current = current.baseContext
|
|
2709
|
+
}
|
|
2710
|
+
return null
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
private fun refreshMentionQuery() {
|
|
2714
|
+
val mentions = addons.mentions
|
|
2715
|
+
if (mentions == null || !richTextView.editorEditText.hasFocus()) {
|
|
2716
|
+
clearMentionQueryState()
|
|
2717
|
+
emitMentionQueryChange("", "@", 0, 0, false)
|
|
2718
|
+
return
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
val queryState = currentMentionQueryState(mentions.trigger)
|
|
2722
|
+
if (queryState == null) {
|
|
2723
|
+
clearMentionQueryState()
|
|
2724
|
+
emitMentionQueryChange("", mentions.trigger, 0, 0, false)
|
|
2725
|
+
return
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
mentionQueryState = queryState
|
|
2729
|
+
val suggestions = filteredMentionSuggestions(queryState, mentions)
|
|
2730
|
+
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: mentions.theme)
|
|
2731
|
+
syncKeyboardToolbarMentionSuggestions(suggestions)
|
|
2732
|
+
emitMentionQueryChange(
|
|
2733
|
+
queryState.query,
|
|
2734
|
+
queryState.trigger,
|
|
2735
|
+
queryState.anchor,
|
|
2736
|
+
queryState.head,
|
|
2737
|
+
true
|
|
2738
|
+
)
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
private fun clearMentionQueryState(resetLastEvent: Boolean = false) {
|
|
2742
|
+
mentionQueryState = null
|
|
2743
|
+
if (resetLastEvent) {
|
|
2744
|
+
lastMentionEventJson = null
|
|
2745
|
+
lastMentionEventEditorId = null
|
|
2746
|
+
}
|
|
2747
|
+
syncKeyboardToolbarMentionSuggestions(emptyList())
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
private fun currentMentionQueryState(trigger: String): MentionQueryState? {
|
|
2751
|
+
val editor = richTextView.editorEditText
|
|
2752
|
+
if (editor.selectionStart != editor.selectionEnd) return null
|
|
2753
|
+
val text = editor.text?.toString() ?: return null
|
|
2754
|
+
val cursorUtf16 = editor.selectionStart
|
|
2755
|
+
val cursorScalar = PositionBridge.utf16ToScalar(cursorUtf16, text)
|
|
2756
|
+
return resolveMentionQueryState(
|
|
2757
|
+
text = text,
|
|
2758
|
+
cursorScalar = cursorScalar,
|
|
2759
|
+
trigger = trigger,
|
|
2760
|
+
isCaretInsideMention = isCaretInsideMention(cursorUtf16)
|
|
2761
|
+
)
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2764
|
+
private fun isCaretInsideMention(cursorUtf16: Int): Boolean {
|
|
2765
|
+
val editable = richTextView.editorEditText.text ?: return false
|
|
2766
|
+
val checkOffsets = listOf(cursorUtf16, (cursorUtf16 - 1).coerceAtLeast(0))
|
|
2767
|
+
return checkOffsets.any { offset ->
|
|
2768
|
+
editable.getSpans(offset, offset, android.text.Annotation::class.java).any { span ->
|
|
2769
|
+
span.key == "nativeVoidNodeType" && span.value == "mention"
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
private fun filteredMentionSuggestions(
|
|
2775
|
+
queryState: MentionQueryState,
|
|
2776
|
+
config: NativeMentionsAddonConfig
|
|
2777
|
+
): List<NativeMentionSuggestion> {
|
|
2778
|
+
val normalizedQuery = queryState.query.trim().lowercase()
|
|
2779
|
+
if (normalizedQuery.isEmpty()) return config.suggestions
|
|
2780
|
+
return config.suggestions.filter { suggestion ->
|
|
2781
|
+
suggestion.title.lowercase().contains(normalizedQuery) ||
|
|
2782
|
+
suggestion.label.lowercase().contains(normalizedQuery) ||
|
|
2783
|
+
(suggestion.subtitle?.lowercase()?.contains(normalizedQuery) == true)
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
private fun syncKeyboardToolbarMentionSuggestions(suggestions: List<NativeMentionSuggestion>) {
|
|
2788
|
+
keyboardToolbarView.setMentionSuggestions(suggestions)
|
|
2789
|
+
keyboardToolbarView.requestLayout()
|
|
2790
|
+
post {
|
|
2791
|
+
updateKeyboardToolbarLayout()
|
|
2792
|
+
updateEditorViewportInset()
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
private fun emitMentionQueryChange(
|
|
2797
|
+
query: String,
|
|
2798
|
+
trigger: String,
|
|
2799
|
+
anchor: Int,
|
|
2800
|
+
head: Int,
|
|
2801
|
+
isActive: Boolean
|
|
2802
|
+
) {
|
|
2803
|
+
val eventJson = JSONObject()
|
|
2804
|
+
.put("type", "mentionsQueryChange")
|
|
2805
|
+
.put("query", query)
|
|
2806
|
+
.put("trigger", trigger)
|
|
2807
|
+
.put("range", JSONObject().put("anchor", anchor).put("head", head))
|
|
2808
|
+
.put("isActive", isActive)
|
|
2809
|
+
.apply {
|
|
2810
|
+
lastDocumentVersion?.let { put("documentVersion", it) }
|
|
2811
|
+
}
|
|
2812
|
+
.toString()
|
|
2813
|
+
val editorId = richTextView.editorId
|
|
2814
|
+
if (eventJson == lastMentionEventJson && editorId == lastMentionEventEditorId) return
|
|
2815
|
+
lastMentionEventJson = eventJson
|
|
2816
|
+
lastMentionEventEditorId = editorId
|
|
2817
|
+
emitAddonEvent(mapOf("eventJson" to eventJson, "editorId" to editorId))
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
private fun resolvedMentionAttrs(
|
|
2821
|
+
trigger: String,
|
|
2822
|
+
suggestion: NativeMentionSuggestion
|
|
2823
|
+
): JSONObject {
|
|
2824
|
+
val attrs = JSONObject(suggestion.attrs.toString())
|
|
2825
|
+
if (!attrs.has("label")) {
|
|
2826
|
+
attrs.put("label", suggestion.label)
|
|
2827
|
+
}
|
|
2828
|
+
if (!attrs.has("mentionSuggestionChar")) {
|
|
2829
|
+
attrs.put("mentionSuggestionChar", trigger)
|
|
2830
|
+
}
|
|
2831
|
+
return attrs
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
private fun emitMentionSelect(trigger: String, suggestion: NativeMentionSuggestion, attrs: JSONObject) {
|
|
2835
|
+
val eventJson = JSONObject()
|
|
2836
|
+
.put("type", "mentionsSelect")
|
|
2837
|
+
.put("trigger", trigger)
|
|
2838
|
+
.put("suggestionKey", suggestion.key)
|
|
2839
|
+
.put("attrs", attrs)
|
|
2840
|
+
.apply {
|
|
2841
|
+
lastDocumentVersion?.let { put("documentVersion", it) }
|
|
2842
|
+
}
|
|
2843
|
+
.toString()
|
|
2844
|
+
emitAddonEvent(mapOf("eventJson" to eventJson, "editorId" to richTextView.editorId))
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
private fun emitMentionSelectRequest(
|
|
2848
|
+
trigger: String,
|
|
2849
|
+
suggestion: NativeMentionSuggestion,
|
|
2850
|
+
attrs: JSONObject,
|
|
2851
|
+
range: MentionQueryState,
|
|
2852
|
+
preflightUpdateJSON: String?
|
|
2853
|
+
) {
|
|
2854
|
+
val eventJson = JSONObject()
|
|
2855
|
+
.put("type", "mentionsSelectRequest")
|
|
2856
|
+
.put("trigger", trigger)
|
|
2857
|
+
.put("suggestionKey", suggestion.key)
|
|
2858
|
+
.put("attrs", attrs)
|
|
2859
|
+
.put("range", JSONObject().put("anchor", range.anchor).put("head", range.head))
|
|
2860
|
+
.apply {
|
|
2861
|
+
if (preflightUpdateJSON != null) {
|
|
2862
|
+
put("updateJson", preflightUpdateJSON)
|
|
2863
|
+
}
|
|
2864
|
+
(documentVersionFromUpdateJSON(preflightUpdateJSON) ?: lastDocumentVersion)
|
|
2865
|
+
?.let { put("documentVersion", it) }
|
|
2866
|
+
}
|
|
2867
|
+
.toString()
|
|
2868
|
+
emitAddonEvent(mapOf("eventJson" to eventJson, "editorId" to richTextView.editorId))
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
private fun insertMentionSuggestion(
|
|
2872
|
+
suggestion: NativeMentionSuggestion,
|
|
2873
|
+
allowPreflightRetry: Boolean = true
|
|
2874
|
+
) {
|
|
2875
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
2876
|
+
if (!richTextView.editorEditText.isEditable) {
|
|
2877
|
+
clearPendingNativeActionRetry()
|
|
2878
|
+
return
|
|
2879
|
+
}
|
|
2880
|
+
val mentions = addons.mentions ?: return
|
|
2881
|
+
if (shouldBlockEditorCommandForPendingUpdate()) {
|
|
2882
|
+
if (allowPreflightRetry) {
|
|
2883
|
+
schedulePendingNativeActionRetry(
|
|
2884
|
+
PendingNativeAction.MentionSuggestionSelect(suggestion)
|
|
2885
|
+
)
|
|
2886
|
+
}
|
|
2887
|
+
return
|
|
2888
|
+
}
|
|
2889
|
+
val preparation = richTextView.editorEditText.prepareForExternalEditorCommand()
|
|
2890
|
+
if (!preparation.ready) {
|
|
2891
|
+
if (allowPreflightRetry) {
|
|
2892
|
+
schedulePendingNativeActionRetry(
|
|
2893
|
+
PendingNativeAction.MentionSuggestionSelect(suggestion)
|
|
2894
|
+
)
|
|
2895
|
+
}
|
|
2896
|
+
return
|
|
2897
|
+
}
|
|
2898
|
+
val preflightUpdateJSON = preparation.updateJSON
|
|
2899
|
+
noteDocumentVersionFromUpdateJSON(preflightUpdateJSON)
|
|
2900
|
+
clearPendingNativeActionRetry()
|
|
2901
|
+
val queryState = currentMentionQueryState(mentions.trigger) ?: run {
|
|
2902
|
+
clearMentionQueryState()
|
|
2903
|
+
return
|
|
2904
|
+
}
|
|
2905
|
+
val freshSuggestions = filteredMentionSuggestions(queryState, mentions)
|
|
2906
|
+
if (freshSuggestions.none { it.key == suggestion.key }) {
|
|
2907
|
+
refreshMentionQuery()
|
|
2908
|
+
return
|
|
2909
|
+
}
|
|
2910
|
+
mentionQueryState = queryState
|
|
2911
|
+
val attrs = resolvedMentionAttrs(mentions.trigger, suggestion)
|
|
2912
|
+
if (mentions.resolveSelectionAttrs || mentions.resolveTheme) {
|
|
2913
|
+
emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState, preflightUpdateJSON)
|
|
2914
|
+
lastMentionEventJson = null
|
|
2915
|
+
clearMentionQueryState()
|
|
2916
|
+
return
|
|
2917
|
+
}
|
|
2918
|
+
val docJson = JSONObject()
|
|
2919
|
+
.put("type", "doc")
|
|
2920
|
+
.put(
|
|
2921
|
+
"content",
|
|
2922
|
+
JSONArray().put(
|
|
2923
|
+
JSONObject()
|
|
2924
|
+
.put("type", "mention")
|
|
2925
|
+
.put("attrs", attrs)
|
|
2926
|
+
)
|
|
2927
|
+
)
|
|
2928
|
+
|
|
2929
|
+
val updateJson = editorInsertContentJsonAtSelectionScalar(
|
|
2930
|
+
richTextView.editorId.toULong(),
|
|
2931
|
+
queryState.anchor.toUInt(),
|
|
2932
|
+
queryState.head.toUInt(),
|
|
2933
|
+
docJson.toString()
|
|
2934
|
+
)
|
|
2935
|
+
richTextView.editorEditText.applyUpdateJSON(updateJson)
|
|
2936
|
+
emitMentionSelect(mentions.trigger, suggestion, attrs)
|
|
2937
|
+
lastMentionEventJson = null
|
|
2938
|
+
clearMentionQueryState()
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
private fun refreshToolbarStateFromEditorSelection(): String? {
|
|
2942
|
+
if (richTextView.editorId == 0L) return null
|
|
2943
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return null
|
|
2944
|
+
onRefreshToolbarStateFromEditorSelectionForTesting?.let { callback ->
|
|
2945
|
+
val stateJson = callback()
|
|
2946
|
+
noteDocumentVersionFromUpdateJSON(stateJson)
|
|
2947
|
+
return stateJson
|
|
2948
|
+
}
|
|
2949
|
+
val stateJson = editorGetSelectionState(richTextView.editorId.toULong())
|
|
2950
|
+
noteDocumentVersionFromUpdateJSON(stateJson)
|
|
2951
|
+
val state = NativeToolbarState.fromUpdateJson(stateJson) ?: return null
|
|
2952
|
+
toolbarState = state
|
|
2953
|
+
keyboardToolbarView.applyState(state)
|
|
2954
|
+
return stateJson
|
|
2955
|
+
}
|
|
2956
|
+
|
|
2957
|
+
private fun ensureKeyboardToolbarAttached() {
|
|
2958
|
+
val host = resolveActivity(context)?.findViewById<ViewGroup>(android.R.id.content) ?: return
|
|
2959
|
+
pendingKeyboardToolbarDetachGeneration += 1
|
|
2960
|
+
if (keyboardToolbarView.parent === host) {
|
|
2961
|
+
updateKeyboardToolbarLayout()
|
|
2962
|
+
return
|
|
2963
|
+
}
|
|
2964
|
+
(keyboardToolbarView.parent as? ViewGroup)?.removeView(keyboardToolbarView)
|
|
2965
|
+
host.addView(
|
|
2966
|
+
keyboardToolbarView,
|
|
2967
|
+
FrameLayout.LayoutParams(
|
|
2968
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
2969
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
2970
|
+
Gravity.BOTTOM or Gravity.START
|
|
2971
|
+
)
|
|
2972
|
+
)
|
|
2973
|
+
updateKeyboardToolbarLayout()
|
|
2974
|
+
ViewCompat.requestApplyInsets(keyboardToolbarView)
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
private fun detachKeyboardToolbarIfNeeded() {
|
|
2978
|
+
pendingKeyboardToolbarDetachGeneration += 1
|
|
2979
|
+
val generation = pendingKeyboardToolbarDetachGeneration
|
|
2980
|
+
val parent = keyboardToolbarView.parent as? ViewGroup ?: return
|
|
2981
|
+
keyboardToolbarView.visibility = View.GONE
|
|
2982
|
+
parent.post {
|
|
2983
|
+
if (generation != pendingKeyboardToolbarDetachGeneration) return@post
|
|
2984
|
+
if (keyboardToolbarView.parent === parent) {
|
|
2985
|
+
parent.removeView(keyboardToolbarView)
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
private fun updateKeyboardToolbarLayout() {
|
|
2991
|
+
val params = keyboardToolbarView.layoutParams as? FrameLayout.LayoutParams ?: return
|
|
2992
|
+
val toolbarTheme = richTextView.editorEditText.theme?.toolbar
|
|
2993
|
+
val density = resources.displayMetrics.density
|
|
2994
|
+
params.gravity = Gravity.BOTTOM or Gravity.START
|
|
2995
|
+
val horizontalInsetPx = ((toolbarTheme?.resolvedHorizontalInset() ?: 0f) * density).toInt()
|
|
2996
|
+
val keyboardOffsetPx = ((toolbarTheme?.resolvedKeyboardOffset() ?: 0f) * density).toInt()
|
|
2997
|
+
params.leftMargin = horizontalInsetPx
|
|
2998
|
+
params.rightMargin = horizontalInsetPx
|
|
2999
|
+
params.bottomMargin = currentImeBottom + keyboardOffsetPx
|
|
3000
|
+
keyboardToolbarView.layoutParams = params
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
private fun updateAttachedKeyboardToolbarForInsets() {
|
|
3004
|
+
if (currentImeBottom <= 0) {
|
|
3005
|
+
clearPendingNativeActionRetry()
|
|
3006
|
+
}
|
|
3007
|
+
keyboardToolbarView.visibility = if (currentImeBottom > 0) View.VISIBLE else View.INVISIBLE
|
|
3008
|
+
updateEditorViewportInset()
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
private fun updateKeyboardToolbarVisibility() {
|
|
3012
|
+
val shouldAttach =
|
|
3013
|
+
showsToolbar &&
|
|
3014
|
+
canFocusCurrentEditor() &&
|
|
3015
|
+
toolbarPlacement == ToolbarPlacement.KEYBOARD &&
|
|
3016
|
+
richTextView.editorEditText.isEditable &&
|
|
3017
|
+
richTextView.editorEditText.hasFocus()
|
|
3018
|
+
|
|
3019
|
+
if (!shouldAttach) {
|
|
3020
|
+
keyboardToolbarView.visibility = View.GONE
|
|
3021
|
+
detachKeyboardToolbarIfNeeded()
|
|
3022
|
+
updateEditorViewportInset()
|
|
3023
|
+
return
|
|
3024
|
+
}
|
|
3025
|
+
|
|
3026
|
+
ensureKeyboardToolbarAttached()
|
|
3027
|
+
keyboardToolbarView.visibility = if (currentImeBottom > 0) View.VISIBLE else View.INVISIBLE
|
|
3028
|
+
updateEditorViewportInset()
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
private fun updateEditorViewportInset(forceMeasureToolbar: Boolean = false) {
|
|
3032
|
+
val shouldReserveToolbarSpace =
|
|
3033
|
+
heightBehavior == EditorHeightBehavior.FIXED &&
|
|
3034
|
+
showsToolbar &&
|
|
3035
|
+
toolbarPlacement == ToolbarPlacement.KEYBOARD &&
|
|
3036
|
+
richTextView.editorEditText.isEditable &&
|
|
3037
|
+
richTextView.editorEditText.hasFocus() &&
|
|
3038
|
+
currentImeBottom > 0
|
|
3039
|
+
|
|
3040
|
+
if (!shouldReserveToolbarSpace) {
|
|
3041
|
+
richTextView.setViewportBottomInsetPx(0)
|
|
3042
|
+
return
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
val hostWidth = (resolveActivity(context)?.findViewById<ViewGroup>(android.R.id.content)?.width ?: width)
|
|
3046
|
+
.coerceAtLeast(0)
|
|
3047
|
+
val toolbarTheme = richTextView.editorEditText.theme?.toolbar
|
|
3048
|
+
val density = resources.displayMetrics.density
|
|
3049
|
+
val horizontalInsetPx = ((toolbarTheme?.resolvedHorizontalInset() ?: 0f) * density).toInt()
|
|
3050
|
+
if (forceMeasureToolbar || keyboardToolbarView.measuredHeight == 0) {
|
|
3051
|
+
val availableWidth = (hostWidth - horizontalInsetPx * 2).coerceAtLeast(0)
|
|
3052
|
+
val widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST)
|
|
3053
|
+
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
3054
|
+
keyboardToolbarView.measure(widthSpec, heightSpec)
|
|
3055
|
+
}
|
|
3056
|
+
val toolbarHeight = keyboardToolbarView.measuredHeight.coerceAtLeast(keyboardToolbarView.height)
|
|
3057
|
+
richTextView.setViewportBottomInsetPx(toolbarHeight.coerceAtLeast(0))
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
private fun handleListToggle(listType: String) {
|
|
3061
|
+
val isActive = toolbarState.nodes[listType] == true
|
|
3062
|
+
richTextView.editorEditText.performToolbarToggleList(listType, isActive)
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
private fun handleToolbarItemPress(
|
|
3066
|
+
item: NativeToolbarItem,
|
|
3067
|
+
allowPreflightRetry: Boolean = true
|
|
3068
|
+
) {
|
|
3069
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
3070
|
+
if (!richTextView.editorEditText.isEditable) {
|
|
3071
|
+
clearPendingNativeActionRetry()
|
|
3072
|
+
return
|
|
3073
|
+
}
|
|
3074
|
+
var preflightUpdateJSON: String? = null
|
|
3075
|
+
val needsEditorPreflight = when (item.type) {
|
|
3076
|
+
ToolbarItemKind.mark,
|
|
3077
|
+
ToolbarItemKind.heading,
|
|
3078
|
+
ToolbarItemKind.blockquote,
|
|
3079
|
+
ToolbarItemKind.list,
|
|
3080
|
+
ToolbarItemKind.command,
|
|
3081
|
+
ToolbarItemKind.node,
|
|
3082
|
+
ToolbarItemKind.action -> true
|
|
3083
|
+
ToolbarItemKind.group,
|
|
3084
|
+
ToolbarItemKind.separator -> false
|
|
3085
|
+
}
|
|
3086
|
+
if (needsEditorPreflight) {
|
|
3087
|
+
if (shouldBlockEditorCommandForPendingUpdate()) {
|
|
3088
|
+
if (allowPreflightRetry) {
|
|
3089
|
+
schedulePendingNativeActionRetry(PendingNativeAction.ToolbarItemPress(item))
|
|
3090
|
+
}
|
|
3091
|
+
return
|
|
3092
|
+
}
|
|
3093
|
+
val preparation = richTextView.editorEditText.prepareForExternalEditorCommand()
|
|
3094
|
+
if (!preparation.ready) {
|
|
3095
|
+
if (allowPreflightRetry) {
|
|
3096
|
+
schedulePendingNativeActionRetry(PendingNativeAction.ToolbarItemPress(item))
|
|
3097
|
+
}
|
|
3098
|
+
return
|
|
3099
|
+
}
|
|
3100
|
+
preflightUpdateJSON = preparation.updateJSON
|
|
3101
|
+
noteDocumentVersionFromUpdateJSON(preflightUpdateJSON)
|
|
3102
|
+
clearPendingNativeActionRetry()
|
|
3103
|
+
}
|
|
3104
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
3105
|
+
when (item.type) {
|
|
3106
|
+
ToolbarItemKind.mark -> item.mark?.let { richTextView.editorEditText.performToolbarToggleMark(it) }
|
|
3107
|
+
ToolbarItemKind.heading -> item.headingLevel?.let { richTextView.editorEditText.performToolbarToggleHeading(it) }
|
|
3108
|
+
ToolbarItemKind.blockquote -> richTextView.editorEditText.performToolbarToggleBlockquote()
|
|
3109
|
+
ToolbarItemKind.list -> item.listType?.name?.let { handleListToggle(it) }
|
|
3110
|
+
ToolbarItemKind.command -> when (item.command) {
|
|
3111
|
+
ToolbarCommand.indentList -> richTextView.editorEditText.performToolbarIndentListItem()
|
|
3112
|
+
ToolbarCommand.outdentList -> richTextView.editorEditText.performToolbarOutdentListItem()
|
|
3113
|
+
ToolbarCommand.undo -> richTextView.editorEditText.performToolbarUndo()
|
|
3114
|
+
ToolbarCommand.redo -> richTextView.editorEditText.performToolbarRedo()
|
|
3115
|
+
null -> Unit
|
|
3116
|
+
}
|
|
3117
|
+
ToolbarItemKind.node -> item.nodeType?.let { richTextView.editorEditText.performToolbarInsertNode(it) }
|
|
3118
|
+
ToolbarItemKind.action -> item.key?.let {
|
|
3119
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
3120
|
+
val payload = mutableMapOf<String, Any>(
|
|
3121
|
+
"key" to it,
|
|
3122
|
+
"editorId" to richTextView.editorId
|
|
3123
|
+
)
|
|
3124
|
+
addPreflightUpdateToEvent(payload, preflightUpdateJSON)
|
|
3125
|
+
if (!payload.containsKey("documentVersion")) {
|
|
3126
|
+
lastDocumentVersion?.let { version ->
|
|
3127
|
+
payload["documentVersion"] = version
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
onToolbarActionForTesting?.invoke(payload) ?: onToolbarAction(payload)
|
|
3131
|
+
}
|
|
3132
|
+
ToolbarItemKind.group -> Unit
|
|
3133
|
+
ToolbarItemKind.separator -> Unit
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
|
|
3137
|
+
}
|