@apollohg/react-native-prose-editor 0.5.16 → 0.5.18
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/android/src/main/java/com/apollohg/editor/EditorEditText.kt +2440 -275
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +783 -64
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1767 -81
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +209 -87
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +27 -0
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +58 -9
- package/dist/NativeEditorBridge.d.ts +34 -1
- package/dist/NativeEditorBridge.js +243 -83
- package/dist/NativeRichTextEditor.js +998 -137
- package/dist/addons.d.ts +7 -0
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- 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/NativeEditorExpoView.swift +830 -17
- package/ios/NativeEditorModule.swift +304 -108
- package/ios/PositionBridge.swift +24 -1
- package/ios/RichTextEditorView.swift +912 -89
- package/package.json +2 -1
- 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
|
@@ -5,7 +5,10 @@ import android.content.Context
|
|
|
5
5
|
import android.content.ContextWrapper
|
|
6
6
|
import android.graphics.Rect
|
|
7
7
|
import android.graphics.RectF
|
|
8
|
+
import android.os.Handler
|
|
9
|
+
import android.os.Looper
|
|
8
10
|
import android.os.SystemClock
|
|
11
|
+
import android.util.Log
|
|
9
12
|
import android.view.Gravity
|
|
10
13
|
import android.view.MotionEvent
|
|
11
14
|
import android.view.View
|
|
@@ -20,8 +23,334 @@ import expo.modules.kotlin.viewevent.EventDispatcher
|
|
|
20
23
|
import expo.modules.kotlin.views.ExpoView
|
|
21
24
|
import org.json.JSONArray
|
|
22
25
|
import org.json.JSONObject
|
|
26
|
+
import java.lang.ref.WeakReference
|
|
27
|
+
import java.util.concurrent.CountDownLatch
|
|
28
|
+
import java.util.concurrent.TimeUnit
|
|
29
|
+
import java.util.WeakHashMap
|
|
30
|
+
import java.util.concurrent.atomic.AtomicInteger
|
|
31
|
+
import java.util.concurrent.atomic.AtomicReference
|
|
23
32
|
import uniffi.editor_core.*
|
|
24
33
|
|
|
34
|
+
private const val DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS = 250L
|
|
35
|
+
|
|
36
|
+
private class WeakNativeEditorExpoView private constructor(
|
|
37
|
+
val view: WeakReference<NativeEditorExpoView?>
|
|
38
|
+
) {
|
|
39
|
+
constructor(view: NativeEditorExpoView) : this(WeakReference(view))
|
|
40
|
+
|
|
41
|
+
companion object {
|
|
42
|
+
fun cleared(): WeakNativeEditorExpoView =
|
|
43
|
+
WeakNativeEditorExpoView(WeakReference<NativeEditorExpoView?>(null))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
internal object NativeEditorViewRegistry {
|
|
48
|
+
private data class CommandPreparationSnapshot(
|
|
49
|
+
val view: NativeEditorExpoView?,
|
|
50
|
+
val isDetached: Boolean,
|
|
51
|
+
val isDestroyed: Boolean
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
private val viewsByEditorId = mutableMapOf<Long, WeakNativeEditorExpoView>()
|
|
55
|
+
private val detachedEditorOwnersByEditorId = mutableMapOf<Long, WeakNativeEditorExpoView>()
|
|
56
|
+
private val destroyedEditorIds = mutableSetOf<Long>()
|
|
57
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
58
|
+
|
|
59
|
+
@Synchronized
|
|
60
|
+
fun markEditorCreated(editorId: Long) {
|
|
61
|
+
if (editorId == 0L) return
|
|
62
|
+
destroyedEditorIds.remove(editorId)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@Synchronized
|
|
66
|
+
fun register(editorId: Long, view: NativeEditorExpoView): Boolean {
|
|
67
|
+
if (editorId == 0L) return false
|
|
68
|
+
if (destroyedEditorIds.contains(editorId)) return false
|
|
69
|
+
viewsByEditorId[editorId] = WeakNativeEditorExpoView(view)
|
|
70
|
+
detachedEditorOwnersByEditorId.remove(editorId)
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Synchronized
|
|
75
|
+
fun unregister(
|
|
76
|
+
editorId: Long,
|
|
77
|
+
view: NativeEditorExpoView,
|
|
78
|
+
blockCommandsUntilRegistered: Boolean = false
|
|
79
|
+
) {
|
|
80
|
+
if (editorId == 0L) return
|
|
81
|
+
val registeredView = viewsByEditorId[editorId]?.view?.get()
|
|
82
|
+
if (registeredView === view) {
|
|
83
|
+
viewsByEditorId.remove(editorId)
|
|
84
|
+
}
|
|
85
|
+
if (blockCommandsUntilRegistered) {
|
|
86
|
+
detachedEditorOwnersByEditorId[editorId] = WeakNativeEditorExpoView(view)
|
|
87
|
+
} else {
|
|
88
|
+
val detachedOwner = detachedEditorOwnersByEditorId[editorId]?.view?.get()
|
|
89
|
+
if (registeredView === view || detachedOwner === view) {
|
|
90
|
+
detachedEditorOwnersByEditorId.remove(editorId)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Synchronized
|
|
96
|
+
fun isDestroyed(editorId: Long): Boolean = destroyedEditorIds.contains(editorId)
|
|
97
|
+
|
|
98
|
+
@Synchronized
|
|
99
|
+
internal fun forceDetachedOwnerClearedForTesting(editorId: Long) {
|
|
100
|
+
detachedEditorOwnersByEditorId[editorId] = WeakNativeEditorExpoView.cleared()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fun invalidateDestroyedEditor(editorId: Long) {
|
|
104
|
+
if (editorId == 0L) return
|
|
105
|
+
val affectedViews = synchronized(this) {
|
|
106
|
+
destroyedEditorIds.add(editorId)
|
|
107
|
+
val views = listOfNotNull(
|
|
108
|
+
viewsByEditorId.remove(editorId)?.view?.get(),
|
|
109
|
+
detachedEditorOwnersByEditorId.remove(editorId)?.view?.get()
|
|
110
|
+
).distinct()
|
|
111
|
+
views
|
|
112
|
+
}
|
|
113
|
+
if (affectedViews.isEmpty()) return
|
|
114
|
+
val invalidate = Runnable {
|
|
115
|
+
affectedViews.forEach { view ->
|
|
116
|
+
view.handleEditorDestroyed(editorId)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
120
|
+
invalidate.run()
|
|
121
|
+
} else {
|
|
122
|
+
val latch = CountDownLatch(1)
|
|
123
|
+
val posted = mainHandler.post {
|
|
124
|
+
try {
|
|
125
|
+
invalidate.run()
|
|
126
|
+
} finally {
|
|
127
|
+
latch.countDown()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!posted) return
|
|
131
|
+
try {
|
|
132
|
+
latch.await(DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
|
|
133
|
+
} catch (_: InterruptedException) {
|
|
134
|
+
Thread.currentThread().interrupt()
|
|
135
|
+
} catch (_: Throwable) {
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
fun prepareForCommandJSON(editorId: Long): String {
|
|
142
|
+
val prepare = {
|
|
143
|
+
val snapshot = synchronized(this) {
|
|
144
|
+
val isDestroyed = destroyedEditorIds.contains(editorId)
|
|
145
|
+
if (isDestroyed) {
|
|
146
|
+
return@synchronized CommandPreparationSnapshot(
|
|
147
|
+
view = null,
|
|
148
|
+
isDetached = false,
|
|
149
|
+
isDestroyed = true
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
val candidate = viewsByEditorId[editorId]?.view?.get()
|
|
153
|
+
if (candidate == null) {
|
|
154
|
+
viewsByEditorId.remove(editorId)
|
|
155
|
+
}
|
|
156
|
+
val detachedOwner = detachedEditorOwnersByEditorId[editorId]?.view?.get()
|
|
157
|
+
val isDetached = if (detachedOwner == null) {
|
|
158
|
+
detachedEditorOwnersByEditorId.remove(editorId)
|
|
159
|
+
false
|
|
160
|
+
} else {
|
|
161
|
+
true
|
|
162
|
+
}
|
|
163
|
+
CommandPreparationSnapshot(
|
|
164
|
+
view = candidate,
|
|
165
|
+
isDetached = isDetached,
|
|
166
|
+
isDestroyed = false
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
snapshot.view?.prepareForEditorCommandJSON()
|
|
170
|
+
?: commandPreparationJSON(
|
|
171
|
+
ready = !snapshot.isDetached && !snapshot.isDestroyed,
|
|
172
|
+
blockedReason = if (snapshot.isDestroyed) {
|
|
173
|
+
"destroyed"
|
|
174
|
+
} else if (snapshot.isDetached) {
|
|
175
|
+
"detached"
|
|
176
|
+
} else {
|
|
177
|
+
null
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
183
|
+
return prepare()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
val result = AtomicReference(commandPreparationJSON(ready = false, blockedReason = "unknown"))
|
|
187
|
+
val state = AtomicInteger(PREFLIGHT_STATE_QUEUED)
|
|
188
|
+
val latch = CountDownLatch(1)
|
|
189
|
+
if (!mainHandler.post {
|
|
190
|
+
try {
|
|
191
|
+
if (state.compareAndSet(PREFLIGHT_STATE_QUEUED, PREFLIGHT_STATE_RUNNING)) {
|
|
192
|
+
result.set(prepare())
|
|
193
|
+
state.set(PREFLIGHT_STATE_DONE)
|
|
194
|
+
}
|
|
195
|
+
} finally {
|
|
196
|
+
latch.countDown()
|
|
197
|
+
}
|
|
198
|
+
}) {
|
|
199
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
if (!latch.await(DESTROY_INVALIDATION_AWAIT_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
|
|
203
|
+
if (state.compareAndSet(PREFLIGHT_STATE_QUEUED, PREFLIGHT_STATE_CANCELLED)) {
|
|
204
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
205
|
+
}
|
|
206
|
+
if (state.get() == PREFLIGHT_STATE_RUNNING) {
|
|
207
|
+
latch.await()
|
|
208
|
+
return result.get()
|
|
209
|
+
}
|
|
210
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
211
|
+
}
|
|
212
|
+
} catch (_: InterruptedException) {
|
|
213
|
+
var interrupted = true
|
|
214
|
+
if (state.compareAndSet(PREFLIGHT_STATE_QUEUED, PREFLIGHT_STATE_CANCELLED)) {
|
|
215
|
+
Thread.currentThread().interrupt()
|
|
216
|
+
return commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
217
|
+
}
|
|
218
|
+
while (state.get() == PREFLIGHT_STATE_RUNNING) {
|
|
219
|
+
try {
|
|
220
|
+
latch.await()
|
|
221
|
+
break
|
|
222
|
+
} catch (_: InterruptedException) {
|
|
223
|
+
interrupted = true
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (interrupted) {
|
|
227
|
+
Thread.currentThread().interrupt()
|
|
228
|
+
}
|
|
229
|
+
return if (state.get() == PREFLIGHT_STATE_DONE) {
|
|
230
|
+
result.get()
|
|
231
|
+
} else {
|
|
232
|
+
commandPreparationJSON(ready = false, blockedReason = "unknown")
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return result.get()
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fun commandPreparationJSON(
|
|
239
|
+
ready: Boolean,
|
|
240
|
+
updateJSON: String? = null,
|
|
241
|
+
blockedReason: String? = null
|
|
242
|
+
): String {
|
|
243
|
+
return JSONObject().apply {
|
|
244
|
+
put("ready", ready)
|
|
245
|
+
if (updateJSON != null) {
|
|
246
|
+
put("updateJSON", updateJSON)
|
|
247
|
+
}
|
|
248
|
+
if (!ready && blockedReason != null) {
|
|
249
|
+
put("blockedReason", blockedReason)
|
|
250
|
+
}
|
|
251
|
+
}.toString()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private const val PREFLIGHT_STATE_QUEUED = 0
|
|
255
|
+
private const val PREFLIGHT_STATE_RUNNING = 1
|
|
256
|
+
private const val PREFLIGHT_STATE_CANCELLED = 2
|
|
257
|
+
private const val PREFLIGHT_STATE_DONE = 3
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private object NativeEditorOutsideTapDispatcher {
|
|
261
|
+
private val dispatchers = WeakHashMap<Window, OutsideTapWindowCallback>()
|
|
262
|
+
|
|
263
|
+
fun register(window: Window, view: NativeEditorExpoView) {
|
|
264
|
+
val currentCallback = window.callback ?: return
|
|
265
|
+
val previousDispatcher = dispatchers[window]
|
|
266
|
+
val dispatcher = if (currentCallback is OutsideTapWindowCallback) {
|
|
267
|
+
previousDispatcher
|
|
268
|
+
?.takeIf { it !== currentCallback }
|
|
269
|
+
?.transferViewsTo(currentCallback)
|
|
270
|
+
currentCallback
|
|
271
|
+
} else {
|
|
272
|
+
OutsideTapWindowCallback(window, currentCallback).also { nextDispatcher ->
|
|
273
|
+
previousDispatcher?.transferViewsTo(nextDispatcher)
|
|
274
|
+
window.callback = nextDispatcher
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
dispatchers[window] = dispatcher
|
|
278
|
+
dispatcher.add(view)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fun unregister(window: Window, view: NativeEditorExpoView) {
|
|
282
|
+
val dispatcher = dispatchers[window] ?: return
|
|
283
|
+
if (!dispatcher.remove(view)) return
|
|
284
|
+
dispatchers.remove(window)
|
|
285
|
+
if (window.callback === dispatcher) {
|
|
286
|
+
window.callback = dispatcher.baseCallback
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private class OutsideTapWindowCallback(
|
|
291
|
+
private val window: Window,
|
|
292
|
+
val baseCallback: Window.Callback
|
|
293
|
+
) : Window.Callback by baseCallback {
|
|
294
|
+
private val views = mutableListOf<WeakReference<NativeEditorExpoView>>()
|
|
295
|
+
private var disabled = false
|
|
296
|
+
|
|
297
|
+
fun add(view: NativeEditorExpoView) {
|
|
298
|
+
prune()
|
|
299
|
+
if (views.any { it.get() === view }) return
|
|
300
|
+
views.add(WeakReference(view))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fun liveViews(): List<NativeEditorExpoView> {
|
|
304
|
+
prune()
|
|
305
|
+
return views.mapNotNull { it.get() }
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fun transferViewsTo(target: OutsideTapWindowCallback) {
|
|
309
|
+
liveViews().forEach { target.add(it) }
|
|
310
|
+
views.clear()
|
|
311
|
+
disabled = true
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fun remove(view: NativeEditorExpoView): Boolean {
|
|
315
|
+
views.removeAll { it.get()?.let { candidate -> candidate === view } != false }
|
|
316
|
+
return views.isEmpty()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
320
|
+
if (disabled) {
|
|
321
|
+
return baseCallback.dispatchTouchEvent(event)
|
|
322
|
+
}
|
|
323
|
+
val activeViews = liveViews()
|
|
324
|
+
if (event.action != MotionEvent.ACTION_DOWN || activeViews.isEmpty()) {
|
|
325
|
+
return baseCallback.dispatchTouchEvent(event)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
val decisions = activeViews.map { view ->
|
|
329
|
+
view to view.shouldScheduleOutsideTapBlurForWindowEvent(event)
|
|
330
|
+
}
|
|
331
|
+
val result = baseCallback.dispatchTouchEvent(event)
|
|
332
|
+
decisions.forEach { (view, shouldBlur) ->
|
|
333
|
+
if (shouldBlur) {
|
|
334
|
+
view.scheduleOutsideTapBlurFromWindowDispatcher()
|
|
335
|
+
} else {
|
|
336
|
+
view.cancelOutsideTapBlurFromWindowDispatcher()
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return result
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private fun prune() {
|
|
343
|
+
views.removeAll { it.get() == null }
|
|
344
|
+
if (views.isEmpty() && window.callback === this) {
|
|
345
|
+
window.callback = baseCallback
|
|
346
|
+
if (dispatchers[window] === this) {
|
|
347
|
+
dispatchers.remove(window)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
25
354
|
/**
|
|
26
355
|
* Expo Modules wrapper view that hosts a [RichTextEditorView] and bridges
|
|
27
356
|
* editor events to React Native via [EventDispatcher].
|
|
@@ -43,13 +372,38 @@ class NativeEditorExpoView(
|
|
|
43
372
|
}
|
|
44
373
|
}
|
|
45
374
|
|
|
375
|
+
private sealed class PendingNativeAction {
|
|
376
|
+
data class ToolbarItemPress(val item: NativeToolbarItem) : PendingNativeAction()
|
|
377
|
+
data class MentionSuggestionSelect(val suggestion: NativeMentionSuggestion) : PendingNativeAction()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private data class PendingNativeActionScope(
|
|
381
|
+
val editorId: Long,
|
|
382
|
+
val documentVersion: Int?,
|
|
383
|
+
val allowedDocumentVersion: Int?,
|
|
384
|
+
val hadFocus: Boolean,
|
|
385
|
+
val hadVisibleToolbar: Boolean,
|
|
386
|
+
val selectionAnchor: Int?,
|
|
387
|
+
val selectionHead: Int?,
|
|
388
|
+
val mentionAnchor: Int? = null,
|
|
389
|
+
val mentionHead: Int? = null,
|
|
390
|
+
val mentionQuery: String? = null
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
private data class PendingEditorUpdateEvent(
|
|
394
|
+
val editorId: Long,
|
|
395
|
+
val updateJSON: String
|
|
396
|
+
)
|
|
397
|
+
|
|
46
398
|
val richTextView: RichTextEditorView = RichTextEditorView(context)
|
|
47
399
|
private val keyboardToolbarView = EditorKeyboardToolbarView(context)
|
|
400
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
48
401
|
|
|
49
402
|
private val onEditorUpdate by EventDispatcher<Map<String, Any>>()
|
|
50
403
|
private val onSelectionChange by EventDispatcher<Map<String, Any>>()
|
|
51
404
|
private val onFocusChange by EventDispatcher<Map<String, Any>>()
|
|
52
405
|
private val onContentHeightChange by EventDispatcher<Map<String, Any>>()
|
|
406
|
+
private val onEditorReady by EventDispatcher<Map<String, Any>>()
|
|
53
407
|
@Suppress("unused")
|
|
54
408
|
private val onToolbarAction by EventDispatcher<Map<String, Any>>()
|
|
55
409
|
@Suppress("unused")
|
|
@@ -57,34 +411,88 @@ class NativeEditorExpoView(
|
|
|
57
411
|
|
|
58
412
|
/** Guard flag: when true, editor updates originated from JS and should not echo back. */
|
|
59
413
|
var isApplyingJSUpdate = false
|
|
414
|
+
internal var blockEditorUpdatePreflightForTesting = false
|
|
415
|
+
internal var blockThemePreflightForTesting = false
|
|
416
|
+
internal var onToolbarActionForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
417
|
+
internal var onAddonEventForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
418
|
+
internal var onFocusChangeForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
419
|
+
internal var onEditorReadyForTesting: ((Map<String, Any>) -> Unit)? = null
|
|
420
|
+
internal var onRefreshToolbarStateFromEditorSelectionForTesting: (() -> String?)? = null
|
|
421
|
+
internal var onBeforePrepareForEditorCommandForTesting: (() -> Unit)? = null
|
|
422
|
+
private var isAttachedToNativeWindow = false
|
|
60
423
|
private var didApplyAutoFocus = false
|
|
61
424
|
private var heightBehavior = EditorHeightBehavior.FIXED
|
|
62
425
|
private var lastEmittedContentHeight = 0
|
|
63
|
-
private var
|
|
64
|
-
private var previousWindowCallback: Window.Callback? = null
|
|
426
|
+
private var outsideTapWindow: Window? = null
|
|
65
427
|
private var toolbarFramesInWindow: List<RectF> = emptyList()
|
|
66
428
|
private var lastToolbarTouchUptimeMs: Long? = null
|
|
67
429
|
private var pendingOutsideTapBlur: Runnable? = null
|
|
68
430
|
private var pendingKeyboardDismiss: Runnable? = null
|
|
431
|
+
private var pendingToolbarRefocus: Runnable? = null
|
|
432
|
+
private var pendingToolbarRefocusEditorId: Long? = null
|
|
433
|
+
private var pendingToolbarRefocusGeneration = 0
|
|
434
|
+
private var autoFocusRequested = false
|
|
69
435
|
private var addons = NativeEditorAddons(null)
|
|
70
436
|
private var mentionQueryState: MentionQueryState? = null
|
|
71
437
|
private var lastMentionEventJson: String? = null
|
|
438
|
+
private var lastMentionEventEditorId: Long? = null
|
|
72
439
|
private var lastThemeJson: String? = null
|
|
440
|
+
private var pendingThemeJson: String? = null
|
|
441
|
+
private var hasPendingTheme = false
|
|
442
|
+
private var pendingThemeRetryScheduled = false
|
|
443
|
+
private var pendingThemeRetryEditorId: Long? = null
|
|
444
|
+
private var pendingThemeRetryGeneration = 0
|
|
445
|
+
private var pendingThemeRetryAttempts = 0
|
|
73
446
|
private var lastAddonsJson: String? = null
|
|
74
447
|
private var lastRemoteSelectionsJson: String? = null
|
|
75
448
|
private var lastToolbarItemsJson: String? = null
|
|
76
449
|
private var lastToolbarFrameJson: String? = null
|
|
450
|
+
private var lastDocumentVersion: Int? = null
|
|
77
451
|
private var toolbarState = NativeToolbarState.empty
|
|
78
452
|
private var showsToolbar = true
|
|
79
453
|
private var toolbarPlacement = ToolbarPlacement.KEYBOARD
|
|
80
454
|
private var currentImeBottom = 0
|
|
81
455
|
private var pendingEditorUpdateJson: String? = null
|
|
456
|
+
private var pendingEditorUpdateEditorId: Long? = null
|
|
82
457
|
private var pendingEditorUpdateRevision = 0
|
|
83
458
|
private var appliedEditorUpdateRevision = 0
|
|
459
|
+
private var pendingEditorUpdateRetryScheduled = false
|
|
460
|
+
private var pendingEditorUpdateRetryEditorId: Long? = null
|
|
461
|
+
private var pendingEditorUpdateRetryGeneration = 0
|
|
462
|
+
private var pendingEditorUpdateRetryAttempts = 0
|
|
463
|
+
private var pendingEditorUpdateForcedRecoveryAttempted = false
|
|
464
|
+
private var pendingViewCommandUpdateJson: String? = null
|
|
465
|
+
private var pendingViewCommandUpdateEditorId: Long? = null
|
|
466
|
+
private var pendingViewCommandUpdateRetryScheduled = false
|
|
467
|
+
private var pendingViewCommandUpdateRetryGeneration = 0
|
|
468
|
+
private var pendingViewCommandUpdateRetryAttempts = 0
|
|
469
|
+
private var pendingPreflightWakeScheduled = false
|
|
470
|
+
private var pendingPreflightWakeGeneration = 0
|
|
471
|
+
private var pendingBlurRetry: Runnable? = null
|
|
472
|
+
private var pendingBlurRetryEditorId: Long? = null
|
|
473
|
+
private var pendingBlurRetryGeneration = 0
|
|
474
|
+
private var pendingBlurRetryAttempts = 0
|
|
475
|
+
private var pendingDetachPreflightRetryScheduled = false
|
|
476
|
+
private var pendingDetachPreflightRetryEditorId: Long? = null
|
|
477
|
+
private var pendingDetachPreflightRetryGeneration = 0
|
|
478
|
+
private var pendingDetachPreflightRetryAttempts = 0
|
|
479
|
+
private var pendingNativeAction: PendingNativeAction? = null
|
|
480
|
+
private var pendingNativeActionScope: PendingNativeActionScope? = null
|
|
481
|
+
private var pendingNativeActionRetryScheduled = false
|
|
482
|
+
private var pendingNativeActionRetryEditorId: Long? = null
|
|
483
|
+
private var pendingNativeActionRetryGeneration = 0
|
|
484
|
+
private var pendingNativeActionRetryAttempts = 0
|
|
485
|
+
private var lastReadyEditorId: Long? = null
|
|
486
|
+
private val pendingEditorUpdateEvents = java.util.ArrayDeque<PendingEditorUpdateEvent>()
|
|
487
|
+
private var pendingEditorUpdateDispatchGeneration = 0
|
|
488
|
+
private var pendingEditorUpdateDispatchScheduled = false
|
|
84
489
|
|
|
85
490
|
init {
|
|
86
491
|
addView(richTextView, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
|
|
87
492
|
richTextView.editorEditText.editorListener = this
|
|
493
|
+
richTextView.onBeforeDetachedFromWindow = {
|
|
494
|
+
prepareForDetachFromWindow()
|
|
495
|
+
}
|
|
88
496
|
keyboardToolbarView.onPressItem = { item ->
|
|
89
497
|
handleToolbarItemPress(item)
|
|
90
498
|
}
|
|
@@ -102,36 +510,130 @@ class NativeEditorExpoView(
|
|
|
102
510
|
// Observe EditText focus changes.
|
|
103
511
|
richTextView.editorEditText.setOnFocusChangeListener { _, hasFocus ->
|
|
104
512
|
if (hasFocus) {
|
|
513
|
+
cancelPendingToolbarRefocus()
|
|
105
514
|
installOutsideTapBlurHandlerIfNeeded()
|
|
106
515
|
refreshMentionQuery()
|
|
107
516
|
} else {
|
|
108
517
|
if (shouldPreserveFocusAfterToolbarTouch()) {
|
|
109
|
-
|
|
110
|
-
focus()
|
|
111
|
-
}
|
|
518
|
+
scheduleToolbarRefocus()
|
|
112
519
|
return@setOnFocusChangeListener
|
|
113
520
|
}
|
|
114
521
|
uninstallOutsideTapBlurHandler()
|
|
115
522
|
clearMentionQueryState()
|
|
523
|
+
clearPendingNativeActionRetry()
|
|
116
524
|
}
|
|
117
525
|
updateKeyboardToolbarVisibility()
|
|
118
|
-
val event = mapOf<String, Any>(
|
|
119
|
-
|
|
526
|
+
val event = mapOf<String, Any>(
|
|
527
|
+
"isFocused" to hasFocus,
|
|
528
|
+
"editorId" to richTextView.editorId
|
|
529
|
+
)
|
|
530
|
+
onFocusChangeForTesting?.invoke(event) ?: onFocusChange(event)
|
|
120
531
|
}
|
|
121
532
|
}
|
|
122
533
|
|
|
123
534
|
fun setEditorId(id: Long) {
|
|
124
|
-
|
|
535
|
+
if (id != 0L && NativeEditorViewRegistry.isDestroyed(id)) {
|
|
536
|
+
setEditorId(0L)
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
val previousEditorId = richTextView.editorId
|
|
540
|
+
if (previousEditorId == id && richTextView.editorEditText.editorId == id) {
|
|
541
|
+
if (id != 0L && isAttachedToNativeWindow) {
|
|
542
|
+
if (!NativeEditorViewRegistry.register(id, this)) {
|
|
543
|
+
handleEditorDestroyed(id)
|
|
544
|
+
return
|
|
545
|
+
}
|
|
546
|
+
applyPendingEditorUpdateIfNeeded()
|
|
547
|
+
applyPendingThemeIfNeeded()
|
|
548
|
+
refreshReadyStateIfSettled()
|
|
549
|
+
applyAutoFocusIfNeeded()
|
|
550
|
+
} else if (id != 0L) {
|
|
551
|
+
NativeEditorViewRegistry.unregister(
|
|
552
|
+
id,
|
|
553
|
+
this,
|
|
554
|
+
blockCommandsUntilRegistered = true
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (previousEditorId != id) {
|
|
560
|
+
NativeEditorViewRegistry.unregister(previousEditorId, this)
|
|
561
|
+
lastDocumentVersion = null
|
|
562
|
+
cancelPendingToolbarRefocus()
|
|
563
|
+
cancelPendingEditorUpdateRetry()
|
|
564
|
+
if (pendingEditorUpdateEditorId != null && pendingEditorUpdateEditorId != id) {
|
|
565
|
+
clearPendingEditorUpdateState()
|
|
566
|
+
}
|
|
567
|
+
appliedEditorUpdateRevision = 0
|
|
568
|
+
clearPendingViewCommandUpdateRetry()
|
|
569
|
+
cancelPendingThemeRetry()
|
|
570
|
+
if (hasPendingTheme) {
|
|
571
|
+
pendingThemeRetryEditorId = id
|
|
572
|
+
}
|
|
573
|
+
cancelPendingBlurRetry()
|
|
574
|
+
clearPendingNativeActionRetry()
|
|
575
|
+
clearMentionQueryState(resetLastEvent = true)
|
|
576
|
+
lastReadyEditorId = null
|
|
577
|
+
}
|
|
578
|
+
if (!isAttachedToNativeWindow) {
|
|
579
|
+
richTextView.setEditorIdWhileDetached(id)
|
|
580
|
+
if (id != 0L) {
|
|
581
|
+
NativeEditorViewRegistry.unregister(
|
|
582
|
+
id,
|
|
583
|
+
this,
|
|
584
|
+
blockCommandsUntilRegistered = true
|
|
585
|
+
)
|
|
586
|
+
} else {
|
|
587
|
+
toolbarState = NativeToolbarState.empty
|
|
588
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
589
|
+
}
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (hasPendingEditorUpdateForEditor(id)) {
|
|
594
|
+
richTextView.setEditorIdWhileDetached(id)
|
|
595
|
+
richTextView.rebindEditorIfNeeded(notifyListener = false)
|
|
596
|
+
} else {
|
|
597
|
+
richTextView.editorId = id
|
|
598
|
+
}
|
|
599
|
+
if (id != 0L) {
|
|
600
|
+
if (!NativeEditorViewRegistry.register(id, this)) {
|
|
601
|
+
handleEditorDestroyed(id)
|
|
602
|
+
return
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
toolbarState = NativeToolbarState.empty
|
|
606
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
607
|
+
}
|
|
608
|
+
applyPendingEditorUpdateIfNeeded()
|
|
609
|
+
applyPendingThemeIfNeeded()
|
|
610
|
+
refreshReadyStateIfSettled()
|
|
611
|
+
applyAutoFocusIfNeeded()
|
|
125
612
|
}
|
|
126
613
|
|
|
127
614
|
fun setThemeJson(themeJson: String?) {
|
|
615
|
+
if (lastThemeJson == themeJson && !hasPendingTheme) return
|
|
616
|
+
pendingThemeJson = themeJson
|
|
617
|
+
hasPendingTheme = true
|
|
618
|
+
pendingThemeRetryEditorId = richTextView.editorId
|
|
619
|
+
pendingThemeRetryAttempts = 0
|
|
620
|
+
applyPendingThemeIfNeeded()
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private fun applyThemeJson(themeJson: String?) {
|
|
128
624
|
if (lastThemeJson == themeJson) return
|
|
129
625
|
lastThemeJson = themeJson
|
|
130
626
|
val theme = EditorTheme.fromJson(themeJson)
|
|
131
627
|
richTextView.applyTheme(theme)
|
|
132
628
|
keyboardToolbarView.applyTheme(theme?.toolbar)
|
|
133
629
|
keyboardToolbarView.applyMentionTheme(theme?.mentions ?: addons.mentions?.theme)
|
|
630
|
+
keyboardToolbarView.requestLayout()
|
|
134
631
|
updateKeyboardToolbarLayout()
|
|
632
|
+
updateEditorViewportInset(forceMeasureToolbar = true)
|
|
633
|
+
post {
|
|
634
|
+
updateKeyboardToolbarLayout()
|
|
635
|
+
updateEditorViewportInset(forceMeasureToolbar = true)
|
|
636
|
+
}
|
|
135
637
|
}
|
|
136
638
|
|
|
137
639
|
fun setHeightBehavior(rawHeightBehavior: String) {
|
|
@@ -159,6 +661,7 @@ class NativeEditorExpoView(
|
|
|
159
661
|
|
|
160
662
|
fun setAddonsJson(addonsJson: String?) {
|
|
161
663
|
if (lastAddonsJson == addonsJson) return
|
|
664
|
+
clearPendingNativeActionRetry()
|
|
162
665
|
lastAddonsJson = addonsJson
|
|
163
666
|
addons = NativeEditorAddons.fromJson(addonsJson)
|
|
164
667
|
keyboardToolbarView.applyMentionTheme(richTextView.editorEditText.theme?.mentions ?: addons.mentions?.theme)
|
|
@@ -174,9 +677,12 @@ class NativeEditorExpoView(
|
|
|
174
677
|
}
|
|
175
678
|
|
|
176
679
|
fun setAutoFocus(autoFocus: Boolean) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
680
|
+
autoFocusRequested = autoFocus
|
|
681
|
+
applyAutoFocusIfNeeded()
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private fun applyAutoFocusIfNeeded() {
|
|
685
|
+
if (!autoFocusRequested || didApplyAutoFocus || !canFocusCurrentEditor()) return
|
|
180
686
|
didApplyAutoFocus = true
|
|
181
687
|
focus()
|
|
182
688
|
}
|
|
@@ -193,13 +699,34 @@ class NativeEditorExpoView(
|
|
|
193
699
|
richTextView.editorEditText.setKeyboardType(keyboardType)
|
|
194
700
|
}
|
|
195
701
|
|
|
702
|
+
fun setEditable(editable: Boolean) {
|
|
703
|
+
if (richTextView.editorEditText.isEditable == editable) return
|
|
704
|
+
if (!editable) {
|
|
705
|
+
cancelPendingToolbarRefocus()
|
|
706
|
+
clearPendingNativeActionRetry()
|
|
707
|
+
}
|
|
708
|
+
richTextView.editorEditText.isEditable = editable
|
|
709
|
+
updateKeyboardToolbarVisibility()
|
|
710
|
+
}
|
|
711
|
+
|
|
196
712
|
fun setShowToolbar(showToolbar: Boolean) {
|
|
713
|
+
if (showsToolbar == showToolbar) return
|
|
714
|
+
if (!showToolbar) {
|
|
715
|
+
cancelPendingToolbarRefocus()
|
|
716
|
+
clearPendingNativeActionRetry()
|
|
717
|
+
}
|
|
197
718
|
showsToolbar = showToolbar
|
|
198
719
|
updateKeyboardToolbarVisibility()
|
|
199
720
|
}
|
|
200
721
|
|
|
201
722
|
fun setToolbarPlacement(rawToolbarPlacement: String?) {
|
|
202
|
-
|
|
723
|
+
val nextPlacement = ToolbarPlacement.fromRaw(rawToolbarPlacement)
|
|
724
|
+
if (toolbarPlacement == nextPlacement) return
|
|
725
|
+
if (nextPlacement != ToolbarPlacement.KEYBOARD) {
|
|
726
|
+
cancelPendingToolbarRefocus()
|
|
727
|
+
clearPendingNativeActionRetry()
|
|
728
|
+
}
|
|
729
|
+
toolbarPlacement = nextPlacement
|
|
203
730
|
updateKeyboardToolbarVisibility()
|
|
204
731
|
}
|
|
205
732
|
|
|
@@ -209,6 +736,7 @@ class NativeEditorExpoView(
|
|
|
209
736
|
|
|
210
737
|
fun setToolbarItemsJson(toolbarItemsJson: String?) {
|
|
211
738
|
if (lastToolbarItemsJson == toolbarItemsJson) return
|
|
739
|
+
clearPendingNativeActionRetry()
|
|
212
740
|
lastToolbarItemsJson = toolbarItemsJson
|
|
213
741
|
keyboardToolbarView.setItems(NativeToolbarItem.fromJson(toolbarItemsJson))
|
|
214
742
|
}
|
|
@@ -267,23 +795,495 @@ class NativeEditorExpoView(
|
|
|
267
795
|
pendingEditorUpdateJson = editorUpdateJson
|
|
268
796
|
}
|
|
269
797
|
|
|
798
|
+
fun setPendingEditorUpdateEditorId(editorUpdateEditorId: Long?) {
|
|
799
|
+
pendingEditorUpdateEditorId = editorUpdateEditorId
|
|
800
|
+
}
|
|
801
|
+
|
|
270
802
|
fun setPendingEditorUpdateRevision(editorUpdateRevision: Int) {
|
|
803
|
+
if (pendingEditorUpdateRevision != editorUpdateRevision) {
|
|
804
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
805
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
806
|
+
}
|
|
271
807
|
pendingEditorUpdateRevision = editorUpdateRevision
|
|
272
808
|
}
|
|
273
809
|
|
|
810
|
+
private fun hasPendingEditorUpdateForEditor(editorId: Long): Boolean =
|
|
811
|
+
pendingEditorUpdateJson != null &&
|
|
812
|
+
pendingEditorUpdateRevision != 0 &&
|
|
813
|
+
pendingEditorUpdateRevision != appliedEditorUpdateRevision &&
|
|
814
|
+
pendingEditorUpdateEditorId == editorId
|
|
815
|
+
|
|
816
|
+
private fun hasPendingEditorUpdateForCurrentEditor(): Boolean =
|
|
817
|
+
hasPendingEditorUpdateForEditor(richTextView.editorId)
|
|
818
|
+
|
|
819
|
+
private fun pendingEditorUpdateCommandPreparationJSON(): String =
|
|
820
|
+
NativeEditorViewRegistry.commandPreparationJSON(
|
|
821
|
+
ready = false,
|
|
822
|
+
blockedReason = "pendingUpdate"
|
|
823
|
+
)
|
|
824
|
+
|
|
825
|
+
private fun shouldBlockEditorCommandForPendingUpdate(): Boolean =
|
|
826
|
+
hasPendingEditorUpdateForCurrentEditor()
|
|
827
|
+
|
|
828
|
+
private fun refreshReadyStateIfSettled() {
|
|
829
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
830
|
+
if (hasPendingEditorUpdateForCurrentEditor()) return
|
|
831
|
+
if (!isAttachedToNativeWindow) return
|
|
832
|
+
if (richTextView.editorEditText.editorId != richTextView.editorId) return
|
|
833
|
+
refreshToolbarStateFromEditorSelection()
|
|
834
|
+
refreshMentionQuery()
|
|
835
|
+
emitEditorReadyIfNeeded()
|
|
836
|
+
}
|
|
837
|
+
|
|
274
838
|
fun applyPendingEditorUpdateIfNeeded() {
|
|
275
|
-
|
|
839
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
276
840
|
if (pendingEditorUpdateRevision == 0) return
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
841
|
+
val revision = pendingEditorUpdateRevision
|
|
842
|
+
val editorId = richTextView.editorId
|
|
843
|
+
val expectedEditorId = pendingEditorUpdateEditorId
|
|
844
|
+
if (expectedEditorId == null) return
|
|
845
|
+
if (expectedEditorId != editorId) return
|
|
846
|
+
if (pendingEditorUpdateJson == null) {
|
|
847
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
848
|
+
refreshReadyStateIfSettled()
|
|
849
|
+
return
|
|
850
|
+
}
|
|
851
|
+
val updateJson = pendingEditorUpdateJson ?: return
|
|
852
|
+
if (pendingEditorUpdateRevision == appliedEditorUpdateRevision) {
|
|
853
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
854
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
855
|
+
refreshReadyStateIfSettled()
|
|
856
|
+
return
|
|
857
|
+
}
|
|
858
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return
|
|
859
|
+
val apply = Runnable {
|
|
860
|
+
if (editorId != richTextView.editorId) return@Runnable
|
|
861
|
+
if (expectedEditorId != richTextView.editorId) return@Runnable
|
|
862
|
+
if (editorId != 0L && !isAttachedToNativeWindow) return@Runnable
|
|
863
|
+
if (revision != pendingEditorUpdateRevision) return@Runnable
|
|
864
|
+
if (revision == appliedEditorUpdateRevision) {
|
|
865
|
+
clearPendingEditorUpdateState(resetAppliedRevision = false)
|
|
866
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
867
|
+
refreshReadyStateIfSettled()
|
|
868
|
+
return@Runnable
|
|
869
|
+
}
|
|
870
|
+
if (applyEditorUpdate(updateJson, scheduleViewCommandRetry = false)) {
|
|
871
|
+
appliedEditorUpdateRevision = revision
|
|
872
|
+
pendingEditorUpdateJson = null
|
|
873
|
+
pendingEditorUpdateEditorId = null
|
|
874
|
+
pendingEditorUpdateRevision = 0
|
|
875
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
876
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
877
|
+
cancelPendingEditorUpdateRetry()
|
|
878
|
+
emitEditorReady(editorUpdateRevision = revision)
|
|
879
|
+
refreshReadyStateIfSettled()
|
|
880
|
+
} else {
|
|
881
|
+
schedulePendingEditorUpdateRetry()
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
885
|
+
apply.run()
|
|
886
|
+
} else if (!post(apply)) {
|
|
887
|
+
richTextView.post(apply)
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
private fun clearPendingEditorUpdateState(resetAppliedRevision: Boolean = true) {
|
|
892
|
+
pendingEditorUpdateJson = null
|
|
893
|
+
pendingEditorUpdateEditorId = null
|
|
894
|
+
pendingEditorUpdateRevision = 0
|
|
895
|
+
if (resetAppliedRevision) {
|
|
896
|
+
appliedEditorUpdateRevision = 0
|
|
897
|
+
}
|
|
898
|
+
cancelPendingEditorUpdateRetry()
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private fun cancelPendingEditorUpdateRetry() {
|
|
902
|
+
pendingEditorUpdateRetryScheduled = false
|
|
903
|
+
pendingEditorUpdateRetryEditorId = null
|
|
904
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
905
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
906
|
+
pendingEditorUpdateRetryGeneration += 1
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private fun schedulePendingEditorUpdateRetry() {
|
|
910
|
+
if (pendingEditorUpdateRetryScheduled) return
|
|
911
|
+
val pastFastRetryBudget =
|
|
912
|
+
pendingEditorUpdateRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS
|
|
913
|
+
if (
|
|
914
|
+
pastFastRetryBudget &&
|
|
915
|
+
!pendingEditorUpdateForcedRecoveryAttempted &&
|
|
916
|
+
richTextView.editorId != 0L &&
|
|
917
|
+
richTextView.editorEditText.editorId == richTextView.editorId
|
|
918
|
+
) {
|
|
919
|
+
pendingEditorUpdateForcedRecoveryAttempted = true
|
|
920
|
+
richTextView.editorEditText.discardTransientNativeInputForExternalRecovery()
|
|
921
|
+
}
|
|
922
|
+
if (!pastFastRetryBudget) {
|
|
923
|
+
pendingEditorUpdateRetryAttempts += 1
|
|
924
|
+
}
|
|
925
|
+
pendingEditorUpdateRetryEditorId = richTextView.editorId
|
|
926
|
+
pendingEditorUpdateRetryScheduled = true
|
|
927
|
+
pendingEditorUpdateRetryGeneration += 1
|
|
928
|
+
val retryGeneration = pendingEditorUpdateRetryGeneration
|
|
929
|
+
val delayMs = if (pastFastRetryBudget) {
|
|
930
|
+
PENDING_UPDATE_RECOVERY_RETRY_DELAY_MS
|
|
931
|
+
} else {
|
|
932
|
+
NATIVE_ACTION_RETRY_DELAY_MS * pendingEditorUpdateRetryAttempts
|
|
933
|
+
}
|
|
934
|
+
val retry = Runnable {
|
|
935
|
+
if (retryGeneration != pendingEditorUpdateRetryGeneration) return@Runnable
|
|
936
|
+
if (pendingEditorUpdateRetryEditorId != richTextView.editorId) {
|
|
937
|
+
clearPendingEditorUpdateState()
|
|
938
|
+
return@Runnable
|
|
939
|
+
}
|
|
940
|
+
pendingEditorUpdateRetryScheduled = false
|
|
941
|
+
pendingEditorUpdateRetryEditorId = null
|
|
942
|
+
applyPendingEditorUpdateIfNeeded()
|
|
943
|
+
}
|
|
944
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
private fun clearPendingThemeRetry() {
|
|
948
|
+
pendingThemeJson = null
|
|
949
|
+
hasPendingTheme = false
|
|
950
|
+
cancelPendingThemeRetry()
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private fun cancelPendingThemeRetry() {
|
|
954
|
+
pendingThemeRetryScheduled = false
|
|
955
|
+
pendingThemeRetryEditorId = null
|
|
956
|
+
pendingThemeRetryAttempts = 0
|
|
957
|
+
pendingThemeRetryGeneration += 1
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private fun applyPendingThemeIfNeeded() {
|
|
961
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
962
|
+
if (!hasPendingTheme) return
|
|
963
|
+
val themeJson = pendingThemeJson
|
|
964
|
+
val editorId = richTextView.editorId
|
|
965
|
+
if (pendingThemeRetryEditorId != editorId) {
|
|
966
|
+
pendingThemeRetryEditorId = editorId
|
|
967
|
+
}
|
|
968
|
+
if (
|
|
969
|
+
blockThemePreflightForTesting ||
|
|
970
|
+
!richTextView.editorEditText.prepareForExternalEditorUpdate()
|
|
971
|
+
) {
|
|
972
|
+
schedulePendingThemeRetry()
|
|
973
|
+
return
|
|
974
|
+
}
|
|
975
|
+
pendingThemeJson = null
|
|
976
|
+
hasPendingTheme = false
|
|
977
|
+
cancelPendingThemeRetry()
|
|
978
|
+
applyThemeJson(themeJson)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
private fun schedulePendingThemeRetry() {
|
|
982
|
+
if (pendingThemeRetryScheduled) return
|
|
983
|
+
if (pendingThemeRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS) return
|
|
984
|
+
pendingThemeRetryAttempts += 1
|
|
985
|
+
pendingThemeRetryEditorId = richTextView.editorId
|
|
986
|
+
pendingThemeRetryScheduled = true
|
|
987
|
+
pendingThemeRetryGeneration += 1
|
|
988
|
+
val retryGeneration = pendingThemeRetryGeneration
|
|
989
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingThemeRetryAttempts
|
|
990
|
+
val retry = Runnable {
|
|
991
|
+
if (retryGeneration != pendingThemeRetryGeneration) return@Runnable
|
|
992
|
+
if (pendingThemeRetryEditorId != richTextView.editorId) {
|
|
993
|
+
clearPendingThemeRetry()
|
|
994
|
+
return@Runnable
|
|
995
|
+
}
|
|
996
|
+
pendingThemeRetryScheduled = false
|
|
997
|
+
applyPendingThemeIfNeeded()
|
|
998
|
+
}
|
|
999
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private fun clearPendingViewCommandUpdateRetry() {
|
|
1003
|
+
pendingViewCommandUpdateJson = null
|
|
1004
|
+
pendingViewCommandUpdateEditorId = null
|
|
1005
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1006
|
+
pendingViewCommandUpdateRetryAttempts = 0
|
|
1007
|
+
pendingViewCommandUpdateRetryGeneration += 1
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
private fun scheduleViewCommandUpdateRetry(updateJson: String) {
|
|
1011
|
+
if (pendingViewCommandUpdateJson != updateJson) {
|
|
1012
|
+
pendingViewCommandUpdateRetryAttempts = 0
|
|
1013
|
+
}
|
|
1014
|
+
pendingViewCommandUpdateJson = updateJson
|
|
1015
|
+
pendingViewCommandUpdateEditorId = richTextView.editorId
|
|
1016
|
+
if (pendingViewCommandUpdateRetryScheduled) return
|
|
1017
|
+
if (pendingViewCommandUpdateRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS) return
|
|
1018
|
+
pendingViewCommandUpdateRetryAttempts += 1
|
|
1019
|
+
pendingViewCommandUpdateRetryScheduled = true
|
|
1020
|
+
pendingViewCommandUpdateRetryGeneration += 1
|
|
1021
|
+
val retryGeneration = pendingViewCommandUpdateRetryGeneration
|
|
1022
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingViewCommandUpdateRetryAttempts
|
|
1023
|
+
val retry = Runnable {
|
|
1024
|
+
if (retryGeneration != pendingViewCommandUpdateRetryGeneration) return@Runnable
|
|
1025
|
+
val retryJson = pendingViewCommandUpdateJson ?: run {
|
|
1026
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1027
|
+
return@Runnable
|
|
1028
|
+
}
|
|
1029
|
+
if (pendingViewCommandUpdateEditorId != richTextView.editorId || richTextView.editorId == 0L) {
|
|
1030
|
+
clearPendingViewCommandUpdateRetry()
|
|
1031
|
+
return@Runnable
|
|
1032
|
+
}
|
|
1033
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
1034
|
+
clearPendingViewCommandUpdateRetry()
|
|
1035
|
+
return@Runnable
|
|
1036
|
+
}
|
|
1037
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1038
|
+
if (applyEditorUpdate(retryJson, scheduleViewCommandRetry = true)) {
|
|
1039
|
+
clearPendingViewCommandUpdateRetry()
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private fun schedulePendingPreflightWake() {
|
|
1046
|
+
if (pendingPreflightWakeScheduled) return
|
|
1047
|
+
pendingPreflightWakeScheduled = true
|
|
1048
|
+
pendingPreflightWakeGeneration += 1
|
|
1049
|
+
val wakeGeneration = pendingPreflightWakeGeneration
|
|
1050
|
+
mainHandler.post {
|
|
1051
|
+
if (wakeGeneration != pendingPreflightWakeGeneration) return@post
|
|
1052
|
+
pendingPreflightWakeScheduled = false
|
|
1053
|
+
wakePendingPreflightWork()
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private fun cancelPendingPreflightWake() {
|
|
1058
|
+
pendingPreflightWakeScheduled = false
|
|
1059
|
+
pendingPreflightWakeGeneration += 1
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private fun wakePendingPreflightWork() {
|
|
1063
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
1064
|
+
schedulePendingPreflightWake()
|
|
1065
|
+
return
|
|
1066
|
+
}
|
|
1067
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1068
|
+
if (pendingEditorUpdateJson != null) {
|
|
1069
|
+
pendingEditorUpdateRetryAttempts = 0
|
|
1070
|
+
pendingEditorUpdateForcedRecoveryAttempted = false
|
|
1071
|
+
applyPendingEditorUpdateIfNeeded()
|
|
1072
|
+
}
|
|
1073
|
+
if (hasPendingTheme) {
|
|
1074
|
+
pendingThemeRetryAttempts = 0
|
|
1075
|
+
applyPendingThemeIfNeeded()
|
|
1076
|
+
}
|
|
1077
|
+
pendingViewCommandUpdateJson?.let { updateJson ->
|
|
1078
|
+
pendingViewCommandUpdateRetryAttempts = 0
|
|
1079
|
+
pendingViewCommandUpdateRetryScheduled = false
|
|
1080
|
+
pendingViewCommandUpdateRetryGeneration += 1
|
|
1081
|
+
if (applyEditorUpdate(updateJson, scheduleViewCommandRetry = true)) {
|
|
1082
|
+
clearPendingViewCommandUpdateRetry()
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
retryPendingNativeActionFromWake()
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
private fun clearPendingNativeActionRetry() {
|
|
1089
|
+
pendingNativeAction = null
|
|
1090
|
+
pendingNativeActionScope = null
|
|
1091
|
+
pendingNativeActionRetryEditorId = null
|
|
1092
|
+
pendingNativeActionRetryScheduled = false
|
|
1093
|
+
pendingNativeActionRetryAttempts = 0
|
|
1094
|
+
pendingNativeActionRetryGeneration += 1
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private fun currentNativeActionScope(action: PendingNativeAction): PendingNativeActionScope {
|
|
1098
|
+
val selection = richTextView.editorEditText.currentScalarSelection()
|
|
1099
|
+
val mentionScope = when (action) {
|
|
1100
|
+
is PendingNativeAction.MentionSuggestionSelect ->
|
|
1101
|
+
mentionQueryState ?: addons.mentions?.let { currentMentionQueryState(it.trigger) }
|
|
1102
|
+
is PendingNativeAction.ToolbarItemPress -> null
|
|
1103
|
+
}
|
|
1104
|
+
return PendingNativeActionScope(
|
|
1105
|
+
editorId = richTextView.editorId,
|
|
1106
|
+
documentVersion = lastDocumentVersion,
|
|
1107
|
+
allowedDocumentVersion = documentVersionFromUpdateJSON(pendingEditorUpdateJson),
|
|
1108
|
+
hadFocus = isEditorEffectivelyFocusedForNativeAction(),
|
|
1109
|
+
hadVisibleToolbar = isNativeActionToolbarVisible(action),
|
|
1110
|
+
selectionAnchor = selection?.first,
|
|
1111
|
+
selectionHead = selection?.second,
|
|
1112
|
+
mentionAnchor = mentionScope?.anchor,
|
|
1113
|
+
mentionHead = mentionScope?.head,
|
|
1114
|
+
mentionQuery = mentionScope?.query
|
|
1115
|
+
)
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private fun isPendingNativeActionScopeCurrent(
|
|
1119
|
+
action: PendingNativeAction,
|
|
1120
|
+
scope: PendingNativeActionScope
|
|
1121
|
+
): Boolean {
|
|
1122
|
+
if (scope.editorId != richTextView.editorId) return false
|
|
1123
|
+
if (scope.hadFocus != isEditorEffectivelyFocusedForNativeAction()) return false
|
|
1124
|
+
if (scope.hadVisibleToolbar != isNativeActionToolbarVisible(action)) return false
|
|
1125
|
+
if (
|
|
1126
|
+
scope.documentVersion != lastDocumentVersion &&
|
|
1127
|
+
(scope.allowedDocumentVersion == null || scope.allowedDocumentVersion != lastDocumentVersion)
|
|
1128
|
+
) {
|
|
1129
|
+
return false
|
|
1130
|
+
}
|
|
1131
|
+
val selection = richTextView.editorEditText.currentScalarSelection()
|
|
1132
|
+
if (scope.selectionAnchor != selection?.first || scope.selectionHead != selection?.second) {
|
|
1133
|
+
return false
|
|
1134
|
+
}
|
|
1135
|
+
if (action is PendingNativeAction.MentionSuggestionSelect) {
|
|
1136
|
+
val mentions = addons.mentions ?: return false
|
|
1137
|
+
val currentQuery = currentMentionQueryState(mentions.trigger) ?: return false
|
|
1138
|
+
if (
|
|
1139
|
+
scope.mentionAnchor != currentQuery.anchor ||
|
|
1140
|
+
scope.mentionHead != currentQuery.head ||
|
|
1141
|
+
scope.mentionQuery != currentQuery.query
|
|
1142
|
+
) {
|
|
1143
|
+
return false
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return true
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
private fun isNativeActionToolbarVisible(action: PendingNativeAction): Boolean {
|
|
1150
|
+
if (!showsToolbar || toolbarPlacement != ToolbarPlacement.KEYBOARD) return false
|
|
1151
|
+
if (keyboardToolbarView.parent == null || keyboardToolbarView.visibility != View.VISIBLE) return false
|
|
1152
|
+
if (action is PendingNativeAction.MentionSuggestionSelect) {
|
|
1153
|
+
return keyboardToolbarView.isShowingMentionSuggestions
|
|
1154
|
+
}
|
|
1155
|
+
return true
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
private fun isEditorEffectivelyFocusedForNativeAction(): Boolean =
|
|
1159
|
+
richTextView.editorEditText.hasFocus() ||
|
|
1160
|
+
(pendingToolbarRefocus != null && pendingToolbarRefocusEditorId == richTextView.editorId)
|
|
1161
|
+
|
|
1162
|
+
private fun clearPendingNativeActionRetryIfScopeChanged() {
|
|
1163
|
+
val action = pendingNativeAction ?: return
|
|
1164
|
+
val scope = pendingNativeActionScope ?: return
|
|
1165
|
+
if (!isPendingNativeActionScopeCurrent(action, scope)) {
|
|
1166
|
+
clearPendingNativeActionRetry()
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private fun schedulePendingNativeActionRetry(action: PendingNativeAction) {
|
|
1171
|
+
val isSameAction = pendingNativeAction == action
|
|
1172
|
+
if (isSameAction) {
|
|
1173
|
+
pendingNativeActionRetryAttempts += 1
|
|
1174
|
+
} else {
|
|
1175
|
+
pendingNativeActionRetryAttempts = 1
|
|
1176
|
+
pendingNativeActionScope = currentNativeActionScope(action)
|
|
1177
|
+
}
|
|
1178
|
+
if (pendingNativeActionRetryAttempts > MAX_NATIVE_ACTION_RETRY_ATTEMPTS) {
|
|
1179
|
+
pendingNativeAction = action
|
|
1180
|
+
pendingNativeActionRetryEditorId = richTextView.editorId
|
|
1181
|
+
pendingNativeActionRetryScheduled = false
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
pendingNativeAction = action
|
|
1185
|
+
pendingNativeActionRetryEditorId = richTextView.editorId
|
|
1186
|
+
if (pendingNativeActionRetryScheduled) return
|
|
1187
|
+
pendingNativeActionRetryScheduled = true
|
|
1188
|
+
pendingNativeActionRetryGeneration += 1
|
|
1189
|
+
val retryGeneration = pendingNativeActionRetryGeneration
|
|
1190
|
+
val retry = Runnable {
|
|
1191
|
+
if (retryGeneration != pendingNativeActionRetryGeneration) return@Runnable
|
|
1192
|
+
val retryAction = pendingNativeAction ?: run {
|
|
1193
|
+
pendingNativeActionRetryScheduled = false
|
|
1194
|
+
return@Runnable
|
|
1195
|
+
}
|
|
1196
|
+
val retryScope = pendingNativeActionScope ?: run {
|
|
1197
|
+
clearPendingNativeActionRetry()
|
|
1198
|
+
return@Runnable
|
|
1199
|
+
}
|
|
1200
|
+
if (pendingNativeActionRetryEditorId != richTextView.editorId || richTextView.editorId == 0L) {
|
|
1201
|
+
clearPendingNativeActionRetry()
|
|
1202
|
+
return@Runnable
|
|
1203
|
+
}
|
|
1204
|
+
if (!isPendingNativeActionScopeCurrent(retryAction, retryScope)) {
|
|
1205
|
+
clearPendingNativeActionRetry()
|
|
1206
|
+
return@Runnable
|
|
1207
|
+
}
|
|
1208
|
+
pendingNativeActionRetryScheduled = false
|
|
1209
|
+
val allowNextRetry = pendingNativeActionRetryAttempts < MAX_NATIVE_ACTION_RETRY_ATTEMPTS
|
|
1210
|
+
when (retryAction) {
|
|
1211
|
+
is PendingNativeAction.ToolbarItemPress ->
|
|
1212
|
+
handleToolbarItemPress(retryAction.item, allowPreflightRetry = allowNextRetry)
|
|
1213
|
+
is PendingNativeAction.MentionSuggestionSelect ->
|
|
1214
|
+
insertMentionSuggestion(retryAction.suggestion, allowPreflightRetry = allowNextRetry)
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
mainHandler.postDelayed(retry, NATIVE_ACTION_RETRY_DELAY_MS)
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private fun retryPendingNativeActionFromWake() {
|
|
1221
|
+
val action = pendingNativeAction ?: return
|
|
1222
|
+
val scope = pendingNativeActionScope ?: run {
|
|
1223
|
+
clearPendingNativeActionRetry()
|
|
1224
|
+
return
|
|
1225
|
+
}
|
|
1226
|
+
if (!isPendingNativeActionScopeCurrent(action, scope)) {
|
|
1227
|
+
clearPendingNativeActionRetry()
|
|
1228
|
+
return
|
|
1229
|
+
}
|
|
1230
|
+
pendingNativeActionRetryAttempts = 0
|
|
1231
|
+
pendingNativeActionRetryScheduled = false
|
|
1232
|
+
when (action) {
|
|
1233
|
+
is PendingNativeAction.ToolbarItemPress ->
|
|
1234
|
+
handleToolbarItemPress(action.item, allowPreflightRetry = true)
|
|
1235
|
+
is PendingNativeAction.MentionSuggestionSelect ->
|
|
1236
|
+
insertMentionSuggestion(action.suggestion, allowPreflightRetry = true)
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
private fun documentVersionFromUpdateJSON(updateJSON: String?): Int? =
|
|
1241
|
+
try {
|
|
1242
|
+
if (updateJSON == null) null
|
|
1243
|
+
else {
|
|
1244
|
+
val version = JSONObject(updateJSON).optInt("documentVersion", Int.MIN_VALUE)
|
|
1245
|
+
version.takeIf { it != Int.MIN_VALUE }
|
|
1246
|
+
}
|
|
1247
|
+
} catch (_: Throwable) {
|
|
1248
|
+
null
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
private fun noteDocumentVersionFromUpdateJSON(updateJSON: String?) {
|
|
1252
|
+
documentVersionFromUpdateJSON(updateJSON)?.let { version ->
|
|
1253
|
+
lastDocumentVersion = version
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private fun addPreflightUpdateToEvent(
|
|
1258
|
+
event: MutableMap<String, Any>,
|
|
1259
|
+
updateJSON: String?
|
|
1260
|
+
) {
|
|
1261
|
+
if (updateJSON == null) return
|
|
1262
|
+
event["updateJson"] = updateJSON
|
|
1263
|
+
documentVersionFromUpdateJSON(updateJSON)?.let { version ->
|
|
1264
|
+
event["documentVersion"] = version
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private fun emitAddonEvent(payload: Map<String, Any>) {
|
|
1269
|
+
onAddonEventForTesting?.invoke(payload) ?: onAddonEvent(payload)
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
private fun canFocusCurrentEditor(): Boolean {
|
|
1273
|
+
val editorId = richTextView.editorId
|
|
1274
|
+
return editorId != 0L &&
|
|
1275
|
+
isAttachedToNativeWindow &&
|
|
1276
|
+
!NativeEditorViewRegistry.isDestroyed(editorId)
|
|
280
1277
|
}
|
|
281
1278
|
|
|
282
1279
|
fun focus() {
|
|
1280
|
+
if (!canFocusCurrentEditor()) return
|
|
283
1281
|
cancelPendingOutsideTapBlur()
|
|
284
1282
|
cancelPendingKeyboardDismiss()
|
|
1283
|
+
cancelPendingBlurRetry()
|
|
285
1284
|
richTextView.editorEditText.requestFocus()
|
|
286
1285
|
richTextView.editorEditText.post {
|
|
1286
|
+
if (!canFocusCurrentEditor()) return@post
|
|
287
1287
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
288
1288
|
imm?.showSoftInput(richTextView.editorEditText, InputMethodManager.SHOW_IMPLICIT)
|
|
289
1289
|
}
|
|
@@ -293,24 +1293,96 @@ class NativeEditorExpoView(
|
|
|
293
1293
|
cancelPendingOutsideTapBlur()
|
|
294
1294
|
cancelPendingKeyboardDismiss()
|
|
295
1295
|
clearRecentToolbarTouch()
|
|
1296
|
+
performBlur(deferKeyboardDismiss = false, allowRetry = true)
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
private fun performBlur(deferKeyboardDismiss: Boolean, allowRetry: Boolean) {
|
|
1300
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1301
|
+
if (!richTextView.editorEditText.prepareForExternalEditorUpdate()) {
|
|
1302
|
+
if (allowRetry && pendingBlurRetryAttempts < MAX_PENDING_UPDATE_RETRY_ATTEMPTS) {
|
|
1303
|
+
schedulePendingBlurRetry(deferKeyboardDismiss)
|
|
1304
|
+
return
|
|
1305
|
+
}
|
|
1306
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1307
|
+
richTextView.editorEditText.restoreAuthorizedTextIfNeeded()
|
|
1308
|
+
}
|
|
1309
|
+
completeBlur(deferKeyboardDismiss)
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
private fun completeBlur(deferKeyboardDismiss: Boolean) {
|
|
1313
|
+
cancelPendingBlurRetry()
|
|
296
1314
|
richTextView.editorEditText.clearFocus()
|
|
1315
|
+
if (deferKeyboardDismiss) {
|
|
1316
|
+
val dismiss = Runnable {
|
|
1317
|
+
pendingKeyboardDismiss = null
|
|
1318
|
+
if (!richTextView.editorEditText.hasFocus()) {
|
|
1319
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1320
|
+
imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
pendingKeyboardDismiss = dismiss
|
|
1324
|
+
richTextView.editorEditText.post(dismiss)
|
|
1325
|
+
return
|
|
1326
|
+
}
|
|
297
1327
|
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
298
1328
|
imm?.hideSoftInputFromWindow(richTextView.editorEditText.windowToken, 0)
|
|
299
1329
|
}
|
|
300
1330
|
|
|
1331
|
+
private fun schedulePendingBlurRetry(deferKeyboardDismiss: Boolean) {
|
|
1332
|
+
pendingBlurRetry?.let {
|
|
1333
|
+
mainHandler.removeCallbacks(it)
|
|
1334
|
+
pendingBlurRetry = null
|
|
1335
|
+
}
|
|
1336
|
+
pendingBlurRetryAttempts += 1
|
|
1337
|
+
pendingBlurRetryEditorId = richTextView.editorId
|
|
1338
|
+
pendingBlurRetryGeneration += 1
|
|
1339
|
+
val retryGeneration = pendingBlurRetryGeneration
|
|
1340
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingBlurRetryAttempts
|
|
1341
|
+
val retry = Runnable {
|
|
1342
|
+
pendingBlurRetry = null
|
|
1343
|
+
if (retryGeneration != pendingBlurRetryGeneration) return@Runnable
|
|
1344
|
+
if (pendingBlurRetryEditorId != richTextView.editorId) {
|
|
1345
|
+
pendingBlurRetryEditorId = null
|
|
1346
|
+
return@Runnable
|
|
1347
|
+
}
|
|
1348
|
+
pendingBlurRetryEditorId = null
|
|
1349
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return@Runnable
|
|
1350
|
+
performBlur(deferKeyboardDismiss, allowRetry = true)
|
|
1351
|
+
}
|
|
1352
|
+
pendingBlurRetry = retry
|
|
1353
|
+
mainHandler.postDelayed(retry, delayMs)
|
|
1354
|
+
}
|
|
1355
|
+
|
|
301
1356
|
private fun blurWithDeferredKeyboardDismiss() {
|
|
302
1357
|
cancelPendingKeyboardDismiss()
|
|
303
1358
|
clearRecentToolbarTouch()
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1359
|
+
performBlur(deferKeyboardDismiss = true, allowRetry = true)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
private fun scheduleToolbarRefocus() {
|
|
1363
|
+
cancelPendingToolbarRefocus()
|
|
1364
|
+
val editorId = richTextView.editorId
|
|
1365
|
+
pendingToolbarRefocusEditorId = editorId
|
|
1366
|
+
pendingToolbarRefocusGeneration += 1
|
|
1367
|
+
val refocusGeneration = pendingToolbarRefocusGeneration
|
|
1368
|
+
val refocus = Runnable {
|
|
1369
|
+
pendingToolbarRefocus = null
|
|
1370
|
+
if (refocusGeneration != pendingToolbarRefocusGeneration) return@Runnable
|
|
1371
|
+
if (pendingToolbarRefocusEditorId != richTextView.editorId) return@Runnable
|
|
1372
|
+
pendingToolbarRefocusEditorId = null
|
|
1373
|
+
focus()
|
|
1374
|
+
}
|
|
1375
|
+
pendingToolbarRefocus = refocus
|
|
1376
|
+
richTextView.editorEditText.post(refocus)
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
private fun cancelPendingToolbarRefocus() {
|
|
1380
|
+
pendingToolbarRefocus?.let {
|
|
1381
|
+
richTextView.editorEditText.removeCallbacks(it)
|
|
1382
|
+
pendingToolbarRefocus = null
|
|
311
1383
|
}
|
|
312
|
-
|
|
313
|
-
|
|
1384
|
+
pendingToolbarRefocusEditorId = null
|
|
1385
|
+
pendingToolbarRefocusGeneration += 1
|
|
314
1386
|
}
|
|
315
1387
|
|
|
316
1388
|
private fun scheduleOutsideTapBlur() {
|
|
@@ -339,6 +1411,16 @@ class NativeEditorExpoView(
|
|
|
339
1411
|
}
|
|
340
1412
|
}
|
|
341
1413
|
|
|
1414
|
+
private fun cancelPendingBlurRetry() {
|
|
1415
|
+
pendingBlurRetry?.let {
|
|
1416
|
+
mainHandler.removeCallbacks(it)
|
|
1417
|
+
pendingBlurRetry = null
|
|
1418
|
+
}
|
|
1419
|
+
pendingBlurRetryEditorId = null
|
|
1420
|
+
pendingBlurRetryAttempts = 0
|
|
1421
|
+
pendingBlurRetryGeneration += 1
|
|
1422
|
+
}
|
|
1423
|
+
|
|
342
1424
|
fun getCaretRectJson(): String? {
|
|
343
1425
|
if (width <= 0 || height <= 0) return null
|
|
344
1426
|
val rect = richTextView.caretRect() ?: return null
|
|
@@ -353,12 +1435,182 @@ class NativeEditorExpoView(
|
|
|
353
1435
|
.toString()
|
|
354
1436
|
}
|
|
355
1437
|
|
|
1438
|
+
override fun onAttachedToWindow() {
|
|
1439
|
+
super.onAttachedToWindow()
|
|
1440
|
+
handleAttachedToWindow()
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
internal fun handleEditorDestroyed(editorId: Long) {
|
|
1444
|
+
if (richTextView.editorId != editorId && richTextView.editorEditText.editorId != editorId) {
|
|
1445
|
+
return
|
|
1446
|
+
}
|
|
1447
|
+
cancelPendingEditorUpdateRetry()
|
|
1448
|
+
clearPendingViewCommandUpdateRetry()
|
|
1449
|
+
cancelPendingThemeRetry()
|
|
1450
|
+
cancelPendingBlurRetry()
|
|
1451
|
+
cancelPendingDetachPreflightRetry()
|
|
1452
|
+
cancelPendingOutsideTapBlur()
|
|
1453
|
+
cancelPendingKeyboardDismiss()
|
|
1454
|
+
cancelPendingToolbarRefocus()
|
|
1455
|
+
cancelPendingPreflightWake()
|
|
1456
|
+
clearPendingNativeActionRetry()
|
|
1457
|
+
clearRecentToolbarTouch()
|
|
1458
|
+
uninstallOutsideTapBlurHandler()
|
|
1459
|
+
detachKeyboardToolbarIfNeeded()
|
|
1460
|
+
richTextView.setViewportBottomInsetPx(0)
|
|
1461
|
+
val editText = richTextView.editorEditText
|
|
1462
|
+
if (editText.hasFocus()) {
|
|
1463
|
+
editText.clearFocus()
|
|
1464
|
+
}
|
|
1465
|
+
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
|
1466
|
+
imm?.hideSoftInputFromWindow(editText.windowToken, 0)
|
|
1467
|
+
clearMentionQueryState(resetLastEvent = true)
|
|
1468
|
+
pendingEditorUpdateJson = null
|
|
1469
|
+
pendingEditorUpdateEditorId = null
|
|
1470
|
+
pendingEditorUpdateRevision = 0
|
|
1471
|
+
appliedEditorUpdateRevision = 0
|
|
1472
|
+
lastDocumentVersion = null
|
|
1473
|
+
lastReadyEditorId = null
|
|
1474
|
+
toolbarState = NativeToolbarState.empty
|
|
1475
|
+
keyboardToolbarView.applyState(toolbarState)
|
|
1476
|
+
keyboardToolbarView.visibility = View.GONE
|
|
1477
|
+
richTextView.editorId = 0L
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
private fun handleDestroyedCurrentEditorIfNeeded(): Boolean {
|
|
1481
|
+
val editorId = richTextView.editorId.takeIf { it != 0L }
|
|
1482
|
+
?: richTextView.editorEditText.editorId.takeIf { it != 0L }
|
|
1483
|
+
?: return false
|
|
1484
|
+
if (!NativeEditorViewRegistry.isDestroyed(editorId)) return false
|
|
1485
|
+
handleEditorDestroyed(editorId)
|
|
1486
|
+
return true
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
private fun handleAttachedToWindow() {
|
|
1490
|
+
isAttachedToNativeWindow = true
|
|
1491
|
+
cancelPendingDetachPreflightRetry()
|
|
1492
|
+
richTextView.clearDeferredEditorUnbind()
|
|
1493
|
+
val editorId = richTextView.editorId
|
|
1494
|
+
if (editorId == 0L) return
|
|
1495
|
+
if (NativeEditorViewRegistry.isDestroyed(editorId)) {
|
|
1496
|
+
handleEditorDestroyed(editorId)
|
|
1497
|
+
return
|
|
1498
|
+
}
|
|
1499
|
+
if (!NativeEditorViewRegistry.register(editorId, this)) {
|
|
1500
|
+
handleEditorDestroyed(editorId)
|
|
1501
|
+
return
|
|
1502
|
+
}
|
|
1503
|
+
richTextView.rebindEditorIfNeeded(
|
|
1504
|
+
notifyListener = !hasPendingEditorUpdateForEditor(editorId)
|
|
1505
|
+
)
|
|
1506
|
+
if (hasPendingTheme) {
|
|
1507
|
+
pendingThemeRetryEditorId = editorId
|
|
1508
|
+
}
|
|
1509
|
+
applyPendingEditorUpdateIfNeeded()
|
|
1510
|
+
applyPendingThemeIfNeeded()
|
|
1511
|
+
refreshReadyStateIfSettled()
|
|
1512
|
+
applyAutoFocusIfNeeded()
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
private fun emitEditorReady(editorUpdateRevision: Int? = null): Boolean {
|
|
1516
|
+
val editorId = richTextView.editorId
|
|
1517
|
+
if (editorId == 0L) return false
|
|
1518
|
+
if (!isAttachedToNativeWindow) return false
|
|
1519
|
+
if (richTextView.editorEditText.editorId != editorId) return false
|
|
1520
|
+
if (hasPendingEditorUpdateForCurrentEditor()) return false
|
|
1521
|
+
lastReadyEditorId = editorId
|
|
1522
|
+
val payload = mutableMapOf<String, Any>("editorId" to editorId)
|
|
1523
|
+
editorUpdateRevision?.let { payload["editorUpdateRevision"] = it }
|
|
1524
|
+
onEditorReadyForTesting?.invoke(payload) ?: onEditorReady(payload)
|
|
1525
|
+
return true
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
private fun emitEditorReadyIfNeeded() {
|
|
1529
|
+
val editorId = richTextView.editorId
|
|
1530
|
+
if (lastReadyEditorId == editorId) return
|
|
1531
|
+
emitEditorReady()
|
|
1532
|
+
}
|
|
1533
|
+
|
|
356
1534
|
override fun onDetachedFromWindow() {
|
|
1535
|
+
prepareForDetachFromWindow()
|
|
357
1536
|
super.onDetachedFromWindow()
|
|
1537
|
+
handleDetachedFromWindow()
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
private fun prepareForDetachFromWindow() {
|
|
1541
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1542
|
+
val editorId = richTextView.editorId
|
|
1543
|
+
if (editorId == 0L || richTextView.editorEditText.editorId == 0L) return
|
|
1544
|
+
if (richTextView.editorEditText.prepareForExternalEditorUpdate()) {
|
|
1545
|
+
cancelPendingDetachPreflightRetry()
|
|
1546
|
+
richTextView.clearDeferredEditorUnbind()
|
|
1547
|
+
return
|
|
1548
|
+
}
|
|
1549
|
+
richTextView.deferEditorUnbindOnNextDetach()
|
|
1550
|
+
schedulePendingDetachPreflightRetry(editorId)
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
private fun schedulePendingDetachPreflightRetry(editorId: Long) {
|
|
1554
|
+
if (pendingDetachPreflightRetryScheduled) return
|
|
1555
|
+
if (pendingDetachPreflightRetryAttempts >= MAX_PENDING_UPDATE_RETRY_ATTEMPTS) {
|
|
1556
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
1557
|
+
richTextView.editorEditText.restoreAuthorizedTextIfNeeded()
|
|
1558
|
+
cancelPendingDetachPreflightRetry()
|
|
1559
|
+
richTextView.unbindEditorForDetachedViewIfNeeded()
|
|
1560
|
+
return
|
|
1561
|
+
}
|
|
1562
|
+
pendingDetachPreflightRetryAttempts += 1
|
|
1563
|
+
pendingDetachPreflightRetryEditorId = editorId
|
|
1564
|
+
pendingDetachPreflightRetryScheduled = true
|
|
1565
|
+
pendingDetachPreflightRetryGeneration += 1
|
|
1566
|
+
val retryGeneration = pendingDetachPreflightRetryGeneration
|
|
1567
|
+
val delayMs = NATIVE_ACTION_RETRY_DELAY_MS * pendingDetachPreflightRetryAttempts
|
|
1568
|
+
mainHandler.postDelayed({
|
|
1569
|
+
if (retryGeneration != pendingDetachPreflightRetryGeneration) return@postDelayed
|
|
1570
|
+
pendingDetachPreflightRetryScheduled = false
|
|
1571
|
+
if (isAttachedToNativeWindow || pendingDetachPreflightRetryEditorId != richTextView.editorId) {
|
|
1572
|
+
cancelPendingDetachPreflightRetry()
|
|
1573
|
+
return@postDelayed
|
|
1574
|
+
}
|
|
1575
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return@postDelayed
|
|
1576
|
+
if (richTextView.editorEditText.prepareForExternalEditorUpdate()) {
|
|
1577
|
+
cancelPendingDetachPreflightRetry()
|
|
1578
|
+
richTextView.unbindEditorForDetachedViewIfNeeded()
|
|
1579
|
+
return@postDelayed
|
|
1580
|
+
}
|
|
1581
|
+
schedulePendingDetachPreflightRetry(editorId)
|
|
1582
|
+
}, delayMs)
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
private fun cancelPendingDetachPreflightRetry() {
|
|
1586
|
+
pendingDetachPreflightRetryScheduled = false
|
|
1587
|
+
pendingDetachPreflightRetryEditorId = null
|
|
1588
|
+
pendingDetachPreflightRetryAttempts = 0
|
|
1589
|
+
pendingDetachPreflightRetryGeneration += 1
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
private fun handleDetachedFromWindow() {
|
|
1593
|
+
isAttachedToNativeWindow = false
|
|
1594
|
+
NativeEditorViewRegistry.unregister(
|
|
1595
|
+
richTextView.editorId,
|
|
1596
|
+
this,
|
|
1597
|
+
blockCommandsUntilRegistered = true
|
|
1598
|
+
)
|
|
358
1599
|
cancelPendingOutsideTapBlur()
|
|
359
1600
|
cancelPendingKeyboardDismiss()
|
|
1601
|
+
cancelPendingToolbarRefocus()
|
|
1602
|
+
cancelPendingBlurRetry()
|
|
1603
|
+
cancelPendingEditorUpdateRetry()
|
|
1604
|
+
clearPendingViewCommandUpdateRetry()
|
|
1605
|
+
cancelPendingThemeRetry()
|
|
1606
|
+
clearPendingNativeActionRetry()
|
|
1607
|
+
cancelPendingPreflightWake()
|
|
1608
|
+
lastReadyEditorId = null
|
|
360
1609
|
uninstallOutsideTapBlurHandler()
|
|
1610
|
+
currentImeBottom = 0
|
|
1611
|
+
keyboardToolbarView.visibility = View.GONE
|
|
361
1612
|
detachKeyboardToolbarIfNeeded()
|
|
1613
|
+
richTextView.setViewportBottomInsetPx(0)
|
|
362
1614
|
}
|
|
363
1615
|
|
|
364
1616
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
@@ -436,30 +1688,132 @@ class NativeEditorExpoView(
|
|
|
436
1688
|
if (contentHeight <= 0) return
|
|
437
1689
|
if (!force && contentHeight == lastEmittedContentHeight) return
|
|
438
1690
|
lastEmittedContentHeight = contentHeight
|
|
439
|
-
onContentHeightChange(
|
|
1691
|
+
onContentHeightChange(
|
|
1692
|
+
mapOf(
|
|
1693
|
+
"contentHeight" to contentHeight,
|
|
1694
|
+
"editorId" to richTextView.editorId
|
|
1695
|
+
)
|
|
1696
|
+
)
|
|
440
1697
|
}
|
|
441
1698
|
|
|
442
1699
|
/** Applies an editor update from JS without echoing it back through events. */
|
|
443
|
-
fun applyEditorUpdate(updateJson: String)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
1700
|
+
fun applyEditorUpdate(updateJson: String): Boolean =
|
|
1701
|
+
applyEditorUpdate(updateJson, scheduleViewCommandRetry = true)
|
|
1702
|
+
|
|
1703
|
+
private fun isEditorReadyForNativeUpdate(): Boolean {
|
|
1704
|
+
val editorId = richTextView.editorId
|
|
1705
|
+
return editorId == 0L || (isAttachedToNativeWindow && richTextView.editorEditText.editorId == editorId)
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
private fun applyEditorUpdate(
|
|
1709
|
+
updateJson: String,
|
|
1710
|
+
scheduleViewCommandRetry: Boolean,
|
|
1711
|
+
expectedEditorId: Long? = null
|
|
1712
|
+
): Boolean {
|
|
1713
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
1714
|
+
val postedEditorId = expectedEditorId ?: richTextView.editorId
|
|
1715
|
+
val apply = Runnable {
|
|
1716
|
+
if (postedEditorId != richTextView.editorId) return@Runnable
|
|
1717
|
+
applyEditorUpdate(updateJson, scheduleViewCommandRetry, postedEditorId)
|
|
1718
|
+
}
|
|
452
1719
|
if (!post(apply)) {
|
|
453
1720
|
richTextView.post(apply)
|
|
454
1721
|
}
|
|
1722
|
+
return false
|
|
1723
|
+
}
|
|
1724
|
+
if (expectedEditorId != null && expectedEditorId != richTextView.editorId) {
|
|
1725
|
+
return false
|
|
1726
|
+
}
|
|
1727
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
1728
|
+
return false
|
|
1729
|
+
}
|
|
1730
|
+
if (!isEditorReadyForNativeUpdate()) {
|
|
1731
|
+
if (scheduleViewCommandRetry) {
|
|
1732
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
1733
|
+
}
|
|
1734
|
+
return false
|
|
1735
|
+
}
|
|
1736
|
+
if (
|
|
1737
|
+
blockEditorUpdatePreflightForTesting ||
|
|
1738
|
+
!richTextView.editorEditText.prepareForExternalEditorUpdate()
|
|
1739
|
+
) {
|
|
1740
|
+
if (scheduleViewCommandRetry) {
|
|
1741
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
1742
|
+
}
|
|
1743
|
+
return false
|
|
1744
|
+
}
|
|
1745
|
+
isApplyingJSUpdate = true
|
|
1746
|
+
return try {
|
|
1747
|
+
richTextView.editorEditText.applyUpdateJSON(updateJson)
|
|
1748
|
+
clearPendingEditorUpdateDispatchQueue("jsUpdate")
|
|
1749
|
+
true
|
|
1750
|
+
} catch (error: Throwable) {
|
|
1751
|
+
Log.w(LOG_TAG, "Failed to apply JS editor update", error)
|
|
1752
|
+
if (scheduleViewCommandRetry) {
|
|
1753
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
1754
|
+
}
|
|
1755
|
+
false
|
|
1756
|
+
} finally {
|
|
1757
|
+
isApplyingJSUpdate = false
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
fun prepareForEditorCommandJSON(): String {
|
|
1762
|
+
if (Looper.myLooper() != Looper.getMainLooper()) {
|
|
1763
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
1764
|
+
ready = false,
|
|
1765
|
+
blockedReason = "unknown"
|
|
1766
|
+
)
|
|
1767
|
+
}
|
|
1768
|
+
if (handleDestroyedCurrentEditorIfNeeded()) {
|
|
1769
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
1770
|
+
ready = false,
|
|
1771
|
+
blockedReason = "destroyed"
|
|
1772
|
+
)
|
|
1773
|
+
}
|
|
1774
|
+
if (richTextView.editorId != 0L && !isAttachedToNativeWindow) {
|
|
1775
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
1776
|
+
ready = false,
|
|
1777
|
+
blockedReason = "detached"
|
|
1778
|
+
)
|
|
1779
|
+
}
|
|
1780
|
+
if (richTextView.editorId != 0L && richTextView.editorEditText.editorId != richTextView.editorId) {
|
|
1781
|
+
return NativeEditorViewRegistry.commandPreparationJSON(
|
|
1782
|
+
ready = false,
|
|
1783
|
+
blockedReason = "detached"
|
|
1784
|
+
)
|
|
1785
|
+
}
|
|
1786
|
+
if (shouldBlockEditorCommandForPendingUpdate()) {
|
|
1787
|
+
return pendingEditorUpdateCommandPreparationJSON()
|
|
1788
|
+
}
|
|
1789
|
+
isApplyingJSUpdate = true
|
|
1790
|
+
return try {
|
|
1791
|
+
onBeforePrepareForEditorCommandForTesting?.invoke()
|
|
1792
|
+
val preparation = richTextView.editorEditText.prepareForExternalEditorCommand()
|
|
1793
|
+
NativeEditorViewRegistry.commandPreparationJSON(
|
|
1794
|
+
ready = preparation.ready,
|
|
1795
|
+
updateJSON = preparation.updateJSON,
|
|
1796
|
+
blockedReason = if (preparation.ready) null else "composition"
|
|
1797
|
+
)
|
|
1798
|
+
} finally {
|
|
1799
|
+
isApplyingJSUpdate = false
|
|
455
1800
|
}
|
|
456
1801
|
}
|
|
457
1802
|
|
|
458
1803
|
override fun onSelectionChanged(anchor: Int, head: Int) {
|
|
459
1804
|
val stateJson = refreshToolbarStateFromEditorSelection()
|
|
460
1805
|
refreshMentionQuery()
|
|
1806
|
+
clearPendingNativeActionRetryIfScopeChanged()
|
|
1807
|
+
schedulePendingPreflightWake()
|
|
461
1808
|
richTextView.refreshRemoteSelections()
|
|
462
|
-
val event = mutableMapOf<String, Any>(
|
|
1809
|
+
val event = mutableMapOf<String, Any>(
|
|
1810
|
+
"anchor" to anchor,
|
|
1811
|
+
"head" to head,
|
|
1812
|
+
"editorId" to richTextView.editorId
|
|
1813
|
+
)
|
|
1814
|
+
lastDocumentVersion?.let {
|
|
1815
|
+
event["documentVersion"] = it
|
|
1816
|
+
}
|
|
463
1817
|
if (stateJson != null) {
|
|
464
1818
|
event["stateJson"] = stateJson
|
|
465
1819
|
}
|
|
@@ -467,57 +1821,142 @@ class NativeEditorExpoView(
|
|
|
467
1821
|
}
|
|
468
1822
|
|
|
469
1823
|
override fun onEditorUpdate(updateJSON: String) {
|
|
1824
|
+
if (isApplyingJSUpdate) {
|
|
1825
|
+
dispatchEditorUpdate(
|
|
1826
|
+
PendingEditorUpdateEvent(
|
|
1827
|
+
editorId = richTextView.editorId,
|
|
1828
|
+
updateJSON = updateJSON
|
|
1829
|
+
),
|
|
1830
|
+
emitToJS = false
|
|
1831
|
+
)
|
|
1832
|
+
return
|
|
1833
|
+
}
|
|
1834
|
+
pendingEditorUpdateEvents.addLast(
|
|
1835
|
+
PendingEditorUpdateEvent(
|
|
1836
|
+
editorId = richTextView.editorId,
|
|
1837
|
+
updateJSON = updateJSON
|
|
1838
|
+
)
|
|
1839
|
+
)
|
|
1840
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
1841
|
+
"nativeViewEditorUpdateQueued",
|
|
1842
|
+
"queue=${pendingEditorUpdateEvents.size} jsonLength=${updateJSON.length}"
|
|
1843
|
+
)
|
|
1844
|
+
schedulePendingEditorUpdateDispatch()
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
internal fun pendingEditorUpdateEventCountForTesting(): Int =
|
|
1848
|
+
pendingEditorUpdateEvents.size
|
|
1849
|
+
|
|
1850
|
+
private fun schedulePendingEditorUpdateDispatch() {
|
|
1851
|
+
pendingEditorUpdateDispatchScheduled = true
|
|
1852
|
+
val generation = ++pendingEditorUpdateDispatchGeneration
|
|
1853
|
+
mainHandler.postDelayed({
|
|
1854
|
+
if (generation != pendingEditorUpdateDispatchGeneration) return@postDelayed
|
|
1855
|
+
pendingEditorUpdateDispatchScheduled = false
|
|
1856
|
+
drainPendingEditorUpdateEvents()
|
|
1857
|
+
}, EDITOR_UPDATE_EVENT_DEBOUNCE_MS)
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
private fun drainPendingEditorUpdateEvents() {
|
|
1861
|
+
if (pendingEditorUpdateEvents.isEmpty()) return
|
|
1862
|
+
val startedAt = System.nanoTime()
|
|
1863
|
+
var drainedCount = 0
|
|
1864
|
+
while (pendingEditorUpdateEvents.isNotEmpty()) {
|
|
1865
|
+
val event = pendingEditorUpdateEvents.removeFirst()
|
|
1866
|
+
if (event.editorId != richTextView.editorId) {
|
|
1867
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
1868
|
+
"nativeViewEditorUpdateSkipped",
|
|
1869
|
+
"reason=staleEditor queuedEditor=${event.editorId} currentEditor=${richTextView.editorId}"
|
|
1870
|
+
)
|
|
1871
|
+
continue
|
|
1872
|
+
}
|
|
1873
|
+
dispatchEditorUpdate(event, emitToJS = true)
|
|
1874
|
+
drainedCount += 1
|
|
1875
|
+
}
|
|
1876
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
1877
|
+
"nativeViewEditorUpdateDrained",
|
|
1878
|
+
"count=$drainedCount totalUs=${nanosToMicros(System.nanoTime() - startedAt)}"
|
|
1879
|
+
)
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
private fun clearPendingEditorUpdateDispatchQueue(reason: String) {
|
|
1883
|
+
if (pendingEditorUpdateEvents.isEmpty() && !pendingEditorUpdateDispatchScheduled) return
|
|
1884
|
+
val clearedCount = pendingEditorUpdateEvents.size
|
|
1885
|
+
pendingEditorUpdateEvents.clear()
|
|
1886
|
+
pendingEditorUpdateDispatchScheduled = false
|
|
1887
|
+
pendingEditorUpdateDispatchGeneration += 1
|
|
1888
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
1889
|
+
"nativeViewEditorUpdateQueueCleared",
|
|
1890
|
+
"reason=$reason count=$clearedCount"
|
|
1891
|
+
)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
private fun dispatchEditorUpdate(event: PendingEditorUpdateEvent, emitToJS: Boolean) {
|
|
1895
|
+
val updateJSON = event.updateJSON
|
|
1896
|
+
val startedAt = System.nanoTime()
|
|
1897
|
+
noteDocumentVersionFromUpdateJSON(updateJSON)
|
|
1898
|
+
val noteNanos = System.nanoTime() - startedAt
|
|
1899
|
+
val toolbarStartedAt = System.nanoTime()
|
|
470
1900
|
NativeToolbarState.fromUpdateJson(updateJSON)?.let { state ->
|
|
471
1901
|
toolbarState = state
|
|
472
1902
|
keyboardToolbarView.applyState(state)
|
|
473
1903
|
}
|
|
1904
|
+
val toolbarNanos = System.nanoTime() - toolbarStartedAt
|
|
1905
|
+
val mentionStartedAt = System.nanoTime()
|
|
474
1906
|
refreshMentionQuery()
|
|
1907
|
+
val mentionNanos = System.nanoTime() - mentionStartedAt
|
|
1908
|
+
val retryStartedAt = System.nanoTime()
|
|
1909
|
+
clearPendingNativeActionRetryIfScopeChanged()
|
|
1910
|
+
schedulePendingPreflightWake()
|
|
475
1911
|
richTextView.refreshRemoteSelections()
|
|
1912
|
+
val retryNanos = System.nanoTime() - retryStartedAt
|
|
476
1913
|
if (heightBehavior == EditorHeightBehavior.AUTO_GROW) {
|
|
477
1914
|
post {
|
|
478
1915
|
requestLayout()
|
|
479
1916
|
emitContentHeightIfNeeded(force = false)
|
|
480
1917
|
}
|
|
481
1918
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1919
|
+
val emitStartedAt = System.nanoTime()
|
|
1920
|
+
if (emitToJS) {
|
|
1921
|
+
val payload = mapOf<String, Any>(
|
|
1922
|
+
"updateJson" to updateJSON,
|
|
1923
|
+
"editorId" to event.editorId
|
|
1924
|
+
)
|
|
1925
|
+
onEditorUpdate(payload)
|
|
1926
|
+
}
|
|
1927
|
+
val totalNanos = System.nanoTime() - startedAt
|
|
1928
|
+
richTextView.editorEditText.recordImeTraceForTesting(
|
|
1929
|
+
"nativeViewEditorUpdateDispatch",
|
|
1930
|
+
"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)}"
|
|
1931
|
+
)
|
|
485
1932
|
}
|
|
486
1933
|
|
|
487
1934
|
private fun installOutsideTapBlurHandlerIfNeeded() {
|
|
488
1935
|
val window = resolveActivity(context)?.window ?: return
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
|
494
|
-
val shouldBlur =
|
|
495
|
-
event.action == MotionEvent.ACTION_DOWN &&
|
|
496
|
-
richTextView.editorEditText.hasFocus() &&
|
|
497
|
-
isTouchOutsideEditor(event)
|
|
498
|
-
val result = currentCallback.dispatchTouchEvent(event)
|
|
499
|
-
if (shouldBlur) {
|
|
500
|
-
scheduleOutsideTapBlur()
|
|
501
|
-
} else if (event.action == MotionEvent.ACTION_DOWN) {
|
|
502
|
-
cancelPendingOutsideTapBlur()
|
|
503
|
-
}
|
|
504
|
-
return result
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
previousWindowCallback = currentCallback
|
|
509
|
-
outsideTapWindowCallback = wrappedCallback
|
|
510
|
-
window.callback = wrappedCallback
|
|
1936
|
+
if (outsideTapWindow === window) return
|
|
1937
|
+
uninstallOutsideTapBlurHandler()
|
|
1938
|
+
NativeEditorOutsideTapDispatcher.register(window, this)
|
|
1939
|
+
outsideTapWindow = window
|
|
511
1940
|
}
|
|
512
1941
|
|
|
513
1942
|
private fun uninstallOutsideTapBlurHandler() {
|
|
514
|
-
val window =
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1943
|
+
val window = outsideTapWindow ?: return
|
|
1944
|
+
NativeEditorOutsideTapDispatcher.unregister(window, this)
|
|
1945
|
+
outsideTapWindow = null
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
internal fun shouldScheduleOutsideTapBlurForWindowEvent(event: MotionEvent): Boolean =
|
|
1949
|
+
isAttachedToNativeWindow &&
|
|
1950
|
+
event.action == MotionEvent.ACTION_DOWN &&
|
|
1951
|
+
richTextView.editorEditText.hasFocus() &&
|
|
1952
|
+
isTouchOutsideEditor(event)
|
|
1953
|
+
|
|
1954
|
+
internal fun scheduleOutsideTapBlurFromWindowDispatcher() {
|
|
1955
|
+
scheduleOutsideTapBlur()
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
internal fun cancelOutsideTapBlurFromWindowDispatcher() {
|
|
1959
|
+
cancelPendingOutsideTapBlur()
|
|
521
1960
|
}
|
|
522
1961
|
|
|
523
1962
|
private fun isTouchOutsideEditor(event: MotionEvent): Boolean {
|
|
@@ -555,6 +1994,125 @@ class NativeEditorExpoView(
|
|
|
555
1994
|
internal fun shouldPreserveFocusAfterToolbarTouchForTesting(): Boolean =
|
|
556
1995
|
shouldPreserveFocusAfterToolbarTouch()
|
|
557
1996
|
|
|
1997
|
+
internal fun setAttachedToNativeWindowForTesting(isAttached: Boolean) {
|
|
1998
|
+
isAttachedToNativeWindow = isAttached
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
internal fun handleAttachedToWindowForTesting() {
|
|
2002
|
+
handleAttachedToWindow()
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
internal fun handleDetachedFromWindowForTesting() {
|
|
2006
|
+
prepareForDetachFromWindow()
|
|
2007
|
+
handleDetachedFromWindow()
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
internal fun performBlurForTesting(deferKeyboardDismiss: Boolean = false) {
|
|
2011
|
+
performBlur(deferKeyboardDismiss = deferKeyboardDismiss, allowRetry = true)
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
internal fun pendingBlurRetryAttemptsForTesting(): Int = pendingBlurRetryAttempts
|
|
2015
|
+
|
|
2016
|
+
internal fun pendingDetachPreflightRetryAttemptsForTesting(): Int =
|
|
2017
|
+
pendingDetachPreflightRetryAttempts
|
|
2018
|
+
|
|
2019
|
+
internal fun hasPendingOutsideTapBlurForTesting(): Boolean = pendingOutsideTapBlur != null
|
|
2020
|
+
|
|
2021
|
+
internal fun hasPendingKeyboardDismissForTesting(): Boolean = pendingKeyboardDismiss != null
|
|
2022
|
+
|
|
2023
|
+
internal fun hasPendingPreflightWakeForTesting(): Boolean = pendingPreflightWakeScheduled
|
|
2024
|
+
|
|
2025
|
+
internal fun hasPendingToolbarRefocusForTesting(): Boolean = pendingToolbarRefocus != null
|
|
2026
|
+
|
|
2027
|
+
internal fun isKeyboardToolbarAttachedForTesting(): Boolean = keyboardToolbarView.parent != null
|
|
2028
|
+
|
|
2029
|
+
internal fun currentImeBottomForTesting(): Int = currentImeBottom
|
|
2030
|
+
|
|
2031
|
+
internal fun setCurrentImeBottomForTesting(bottom: Int) {
|
|
2032
|
+
currentImeBottom = bottom
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
internal fun updateAttachedKeyboardToolbarForInsetsForTesting() {
|
|
2036
|
+
updateAttachedKeyboardToolbarForInsets()
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
internal fun scheduleToolbarRefocusForTesting() {
|
|
2040
|
+
scheduleToolbarRefocus()
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
internal fun applyAutoFocusForTesting() {
|
|
2044
|
+
applyAutoFocusIfNeeded()
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
internal fun installOutsideTapBlurHandlerForTesting() {
|
|
2048
|
+
installOutsideTapBlurHandlerIfNeeded()
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
internal fun uninstallOutsideTapBlurHandlerForTesting() {
|
|
2052
|
+
uninstallOutsideTapBlurHandler()
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
internal fun schedulePendingPreflightWakeForTesting() {
|
|
2056
|
+
schedulePendingPreflightWake()
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
internal fun hasPendingNativeActionForTesting(): Boolean = pendingNativeAction != null
|
|
2060
|
+
|
|
2061
|
+
internal fun pendingNativeActionRetryAttemptsForTesting(): Int = pendingNativeActionRetryAttempts
|
|
2062
|
+
|
|
2063
|
+
internal fun lastDocumentVersionForTesting(): Int? = lastDocumentVersion
|
|
2064
|
+
|
|
2065
|
+
internal fun setLastDocumentVersionForTesting(documentVersion: Int?) {
|
|
2066
|
+
lastDocumentVersion = documentVersion
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
internal fun refreshToolbarStateFromEditorSelectionForTesting(): String? =
|
|
2070
|
+
refreshToolbarStateFromEditorSelection()
|
|
2071
|
+
|
|
2072
|
+
internal fun handleToolbarItemPressForTesting(item: NativeToolbarItem) {
|
|
2073
|
+
handleToolbarItemPress(item)
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
internal fun insertMentionSuggestionForTesting(suggestion: NativeMentionSuggestion) {
|
|
2077
|
+
insertMentionSuggestion(suggestion)
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
internal fun wakePendingPreflightWorkForTesting() {
|
|
2081
|
+
wakePendingPreflightWork()
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
internal fun emitEditorReadyForTesting(editorUpdateRevision: Int? = null): Boolean =
|
|
2085
|
+
emitEditorReady(editorUpdateRevision)
|
|
2086
|
+
|
|
2087
|
+
internal fun pendingEditorUpdateJsonForTesting(): String? = pendingEditorUpdateJson
|
|
2088
|
+
|
|
2089
|
+
internal fun pendingEditorUpdateRevisionForTesting(): Int = pendingEditorUpdateRevision
|
|
2090
|
+
|
|
2091
|
+
internal fun setAppliedEditorUpdateRevisionForTesting(editorUpdateRevision: Int) {
|
|
2092
|
+
appliedEditorUpdateRevision = editorUpdateRevision
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
internal fun pendingEditorUpdateEditorIdForTesting(): Long? = pendingEditorUpdateEditorId
|
|
2096
|
+
|
|
2097
|
+
internal fun pendingViewCommandUpdateJsonForTesting(): String? = pendingViewCommandUpdateJson
|
|
2098
|
+
|
|
2099
|
+
internal fun pendingViewCommandUpdateRetryAttemptsForTesting(): Int =
|
|
2100
|
+
pendingViewCommandUpdateRetryAttempts
|
|
2101
|
+
|
|
2102
|
+
internal fun scheduleViewCommandUpdateRetryForTesting(updateJson: String) {
|
|
2103
|
+
scheduleViewCommandUpdateRetry(updateJson)
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
internal fun pendingThemeJsonForTesting(): String? = pendingThemeJson.takeIf { hasPendingTheme }
|
|
2107
|
+
|
|
2108
|
+
internal fun lastThemeJsonForTesting(): String? = lastThemeJson
|
|
2109
|
+
|
|
2110
|
+
internal fun pendingThemeRetryAttemptsForTesting(): Int = pendingThemeRetryAttempts
|
|
2111
|
+
|
|
2112
|
+
internal fun applyPendingThemeForTesting() {
|
|
2113
|
+
applyPendingThemeIfNeeded()
|
|
2114
|
+
}
|
|
2115
|
+
|
|
558
2116
|
private fun isTouchInsideStandaloneToolbar(event: MotionEvent): Boolean {
|
|
559
2117
|
val visibleWindowFrame = Rect()
|
|
560
2118
|
getWindowVisibleDisplayFrame(visibleWindowFrame)
|
|
@@ -619,6 +2177,14 @@ class NativeEditorExpoView(
|
|
|
619
2177
|
private const val TOOLBAR_HIT_SLOP_DP = 8f
|
|
620
2178
|
private const val TOOLBAR_FOCUS_PRESERVE_MS = 750L
|
|
621
2179
|
private const val OUTSIDE_TAP_BLUR_DELAY_MS = 100L
|
|
2180
|
+
private const val NATIVE_ACTION_RETRY_DELAY_MS = 16L
|
|
2181
|
+
private const val EDITOR_UPDATE_EVENT_DEBOUNCE_MS = 64L
|
|
2182
|
+
private const val PENDING_UPDATE_RECOVERY_RETRY_DELAY_MS = 250L
|
|
2183
|
+
private const val MAX_NATIVE_ACTION_RETRY_ATTEMPTS = 3
|
|
2184
|
+
private const val MAX_PENDING_UPDATE_RETRY_ATTEMPTS = 5
|
|
2185
|
+
private const val LOG_TAG = "NativeEditor"
|
|
2186
|
+
|
|
2187
|
+
private fun nanosToMicros(nanos: Long): Long = nanos / 1_000L
|
|
622
2188
|
}
|
|
623
2189
|
|
|
624
2190
|
private fun resolveActivity(context: Context): Activity? {
|
|
@@ -658,8 +2224,12 @@ class NativeEditorExpoView(
|
|
|
658
2224
|
)
|
|
659
2225
|
}
|
|
660
2226
|
|
|
661
|
-
private fun clearMentionQueryState() {
|
|
2227
|
+
private fun clearMentionQueryState(resetLastEvent: Boolean = false) {
|
|
662
2228
|
mentionQueryState = null
|
|
2229
|
+
if (resetLastEvent) {
|
|
2230
|
+
lastMentionEventJson = null
|
|
2231
|
+
lastMentionEventEditorId = null
|
|
2232
|
+
}
|
|
663
2233
|
syncKeyboardToolbarMentionSuggestions(emptyList())
|
|
664
2234
|
}
|
|
665
2235
|
|
|
@@ -722,10 +2292,15 @@ class NativeEditorExpoView(
|
|
|
722
2292
|
.put("trigger", trigger)
|
|
723
2293
|
.put("range", JSONObject().put("anchor", anchor).put("head", head))
|
|
724
2294
|
.put("isActive", isActive)
|
|
2295
|
+
.apply {
|
|
2296
|
+
lastDocumentVersion?.let { put("documentVersion", it) }
|
|
2297
|
+
}
|
|
725
2298
|
.toString()
|
|
726
|
-
|
|
2299
|
+
val editorId = richTextView.editorId
|
|
2300
|
+
if (eventJson == lastMentionEventJson && editorId == lastMentionEventEditorId) return
|
|
727
2301
|
lastMentionEventJson = eventJson
|
|
728
|
-
|
|
2302
|
+
lastMentionEventEditorId = editorId
|
|
2303
|
+
emitAddonEvent(mapOf("eventJson" to eventJson, "editorId" to editorId))
|
|
729
2304
|
}
|
|
730
2305
|
|
|
731
2306
|
private fun resolvedMentionAttrs(
|
|
@@ -748,15 +2323,19 @@ class NativeEditorExpoView(
|
|
|
748
2323
|
.put("trigger", trigger)
|
|
749
2324
|
.put("suggestionKey", suggestion.key)
|
|
750
2325
|
.put("attrs", attrs)
|
|
2326
|
+
.apply {
|
|
2327
|
+
lastDocumentVersion?.let { put("documentVersion", it) }
|
|
2328
|
+
}
|
|
751
2329
|
.toString()
|
|
752
|
-
|
|
2330
|
+
emitAddonEvent(mapOf("eventJson" to eventJson, "editorId" to richTextView.editorId))
|
|
753
2331
|
}
|
|
754
2332
|
|
|
755
2333
|
private fun emitMentionSelectRequest(
|
|
756
2334
|
trigger: String,
|
|
757
2335
|
suggestion: NativeMentionSuggestion,
|
|
758
2336
|
attrs: JSONObject,
|
|
759
|
-
range: MentionQueryState
|
|
2337
|
+
range: MentionQueryState,
|
|
2338
|
+
preflightUpdateJSON: String?
|
|
760
2339
|
) {
|
|
761
2340
|
val eventJson = JSONObject()
|
|
762
2341
|
.put("type", "mentionsSelectRequest")
|
|
@@ -764,16 +2343,60 @@ class NativeEditorExpoView(
|
|
|
764
2343
|
.put("suggestionKey", suggestion.key)
|
|
765
2344
|
.put("attrs", attrs)
|
|
766
2345
|
.put("range", JSONObject().put("anchor", range.anchor).put("head", range.head))
|
|
2346
|
+
.apply {
|
|
2347
|
+
if (preflightUpdateJSON != null) {
|
|
2348
|
+
put("updateJson", preflightUpdateJSON)
|
|
2349
|
+
}
|
|
2350
|
+
(documentVersionFromUpdateJSON(preflightUpdateJSON) ?: lastDocumentVersion)
|
|
2351
|
+
?.let { put("documentVersion", it) }
|
|
2352
|
+
}
|
|
767
2353
|
.toString()
|
|
768
|
-
|
|
2354
|
+
emitAddonEvent(mapOf("eventJson" to eventJson, "editorId" to richTextView.editorId))
|
|
769
2355
|
}
|
|
770
2356
|
|
|
771
|
-
private fun insertMentionSuggestion(
|
|
2357
|
+
private fun insertMentionSuggestion(
|
|
2358
|
+
suggestion: NativeMentionSuggestion,
|
|
2359
|
+
allowPreflightRetry: Boolean = true
|
|
2360
|
+
) {
|
|
2361
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
2362
|
+
if (!richTextView.editorEditText.isEditable) {
|
|
2363
|
+
clearPendingNativeActionRetry()
|
|
2364
|
+
return
|
|
2365
|
+
}
|
|
772
2366
|
val mentions = addons.mentions ?: return
|
|
773
|
-
|
|
2367
|
+
if (shouldBlockEditorCommandForPendingUpdate()) {
|
|
2368
|
+
if (allowPreflightRetry) {
|
|
2369
|
+
schedulePendingNativeActionRetry(
|
|
2370
|
+
PendingNativeAction.MentionSuggestionSelect(suggestion)
|
|
2371
|
+
)
|
|
2372
|
+
}
|
|
2373
|
+
return
|
|
2374
|
+
}
|
|
2375
|
+
val preparation = richTextView.editorEditText.prepareForExternalEditorCommand()
|
|
2376
|
+
if (!preparation.ready) {
|
|
2377
|
+
if (allowPreflightRetry) {
|
|
2378
|
+
schedulePendingNativeActionRetry(
|
|
2379
|
+
PendingNativeAction.MentionSuggestionSelect(suggestion)
|
|
2380
|
+
)
|
|
2381
|
+
}
|
|
2382
|
+
return
|
|
2383
|
+
}
|
|
2384
|
+
val preflightUpdateJSON = preparation.updateJSON
|
|
2385
|
+
noteDocumentVersionFromUpdateJSON(preflightUpdateJSON)
|
|
2386
|
+
clearPendingNativeActionRetry()
|
|
2387
|
+
val queryState = currentMentionQueryState(mentions.trigger) ?: run {
|
|
2388
|
+
clearMentionQueryState()
|
|
2389
|
+
return
|
|
2390
|
+
}
|
|
2391
|
+
val freshSuggestions = filteredMentionSuggestions(queryState, mentions)
|
|
2392
|
+
if (freshSuggestions.none { it.key == suggestion.key }) {
|
|
2393
|
+
refreshMentionQuery()
|
|
2394
|
+
return
|
|
2395
|
+
}
|
|
2396
|
+
mentionQueryState = queryState
|
|
774
2397
|
val attrs = resolvedMentionAttrs(mentions.trigger, suggestion)
|
|
775
2398
|
if (mentions.resolveSelectionAttrs || mentions.resolveTheme) {
|
|
776
|
-
emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState)
|
|
2399
|
+
emitMentionSelectRequest(mentions.trigger, suggestion, attrs, queryState, preflightUpdateJSON)
|
|
777
2400
|
lastMentionEventJson = null
|
|
778
2401
|
clearMentionQueryState()
|
|
779
2402
|
return
|
|
@@ -803,7 +2426,14 @@ class NativeEditorExpoView(
|
|
|
803
2426
|
|
|
804
2427
|
private fun refreshToolbarStateFromEditorSelection(): String? {
|
|
805
2428
|
if (richTextView.editorId == 0L) return null
|
|
2429
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return null
|
|
2430
|
+
onRefreshToolbarStateFromEditorSelectionForTesting?.let { callback ->
|
|
2431
|
+
val stateJson = callback()
|
|
2432
|
+
noteDocumentVersionFromUpdateJSON(stateJson)
|
|
2433
|
+
return stateJson
|
|
2434
|
+
}
|
|
806
2435
|
val stateJson = editorGetSelectionState(richTextView.editorId.toULong())
|
|
2436
|
+
noteDocumentVersionFromUpdateJSON(stateJson)
|
|
807
2437
|
val state = NativeToolbarState.fromUpdateJson(stateJson) ?: return null
|
|
808
2438
|
toolbarState = state
|
|
809
2439
|
keyboardToolbarView.applyState(state)
|
|
@@ -847,6 +2477,9 @@ class NativeEditorExpoView(
|
|
|
847
2477
|
}
|
|
848
2478
|
|
|
849
2479
|
private fun updateAttachedKeyboardToolbarForInsets() {
|
|
2480
|
+
if (currentImeBottom <= 0) {
|
|
2481
|
+
clearPendingNativeActionRetry()
|
|
2482
|
+
}
|
|
850
2483
|
keyboardToolbarView.visibility = if (currentImeBottom > 0) View.VISIBLE else View.INVISIBLE
|
|
851
2484
|
updateEditorViewportInset()
|
|
852
2485
|
}
|
|
@@ -854,6 +2487,7 @@ class NativeEditorExpoView(
|
|
|
854
2487
|
private fun updateKeyboardToolbarVisibility() {
|
|
855
2488
|
val shouldAttach =
|
|
856
2489
|
showsToolbar &&
|
|
2490
|
+
canFocusCurrentEditor() &&
|
|
857
2491
|
toolbarPlacement == ToolbarPlacement.KEYBOARD &&
|
|
858
2492
|
richTextView.editorEditText.isEditable &&
|
|
859
2493
|
richTextView.editorEditText.hasFocus()
|
|
@@ -870,7 +2504,7 @@ class NativeEditorExpoView(
|
|
|
870
2504
|
updateEditorViewportInset()
|
|
871
2505
|
}
|
|
872
2506
|
|
|
873
|
-
private fun updateEditorViewportInset() {
|
|
2507
|
+
private fun updateEditorViewportInset(forceMeasureToolbar: Boolean = false) {
|
|
874
2508
|
val shouldReserveToolbarSpace =
|
|
875
2509
|
heightBehavior == EditorHeightBehavior.FIXED &&
|
|
876
2510
|
showsToolbar &&
|
|
@@ -889,7 +2523,7 @@ class NativeEditorExpoView(
|
|
|
889
2523
|
val toolbarTheme = richTextView.editorEditText.theme?.toolbar
|
|
890
2524
|
val density = resources.displayMetrics.density
|
|
891
2525
|
val horizontalInsetPx = ((toolbarTheme?.resolvedHorizontalInset() ?: 0f) * density).toInt()
|
|
892
|
-
if (keyboardToolbarView.measuredHeight == 0) {
|
|
2526
|
+
if (forceMeasureToolbar || keyboardToolbarView.measuredHeight == 0) {
|
|
893
2527
|
val availableWidth = (hostWidth - horizontalInsetPx * 2).coerceAtLeast(0)
|
|
894
2528
|
val widthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST)
|
|
895
2529
|
val heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
|
|
@@ -904,7 +2538,46 @@ class NativeEditorExpoView(
|
|
|
904
2538
|
richTextView.editorEditText.performToolbarToggleList(listType, isActive)
|
|
905
2539
|
}
|
|
906
2540
|
|
|
907
|
-
private fun handleToolbarItemPress(
|
|
2541
|
+
private fun handleToolbarItemPress(
|
|
2542
|
+
item: NativeToolbarItem,
|
|
2543
|
+
allowPreflightRetry: Boolean = true
|
|
2544
|
+
) {
|
|
2545
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
2546
|
+
if (!richTextView.editorEditText.isEditable) {
|
|
2547
|
+
clearPendingNativeActionRetry()
|
|
2548
|
+
return
|
|
2549
|
+
}
|
|
2550
|
+
var preflightUpdateJSON: String? = null
|
|
2551
|
+
val needsEditorPreflight = when (item.type) {
|
|
2552
|
+
ToolbarItemKind.mark,
|
|
2553
|
+
ToolbarItemKind.heading,
|
|
2554
|
+
ToolbarItemKind.blockquote,
|
|
2555
|
+
ToolbarItemKind.list,
|
|
2556
|
+
ToolbarItemKind.command,
|
|
2557
|
+
ToolbarItemKind.node,
|
|
2558
|
+
ToolbarItemKind.action -> true
|
|
2559
|
+
ToolbarItemKind.group,
|
|
2560
|
+
ToolbarItemKind.separator -> false
|
|
2561
|
+
}
|
|
2562
|
+
if (needsEditorPreflight) {
|
|
2563
|
+
if (shouldBlockEditorCommandForPendingUpdate()) {
|
|
2564
|
+
if (allowPreflightRetry) {
|
|
2565
|
+
schedulePendingNativeActionRetry(PendingNativeAction.ToolbarItemPress(item))
|
|
2566
|
+
}
|
|
2567
|
+
return
|
|
2568
|
+
}
|
|
2569
|
+
val preparation = richTextView.editorEditText.prepareForExternalEditorCommand()
|
|
2570
|
+
if (!preparation.ready) {
|
|
2571
|
+
if (allowPreflightRetry) {
|
|
2572
|
+
schedulePendingNativeActionRetry(PendingNativeAction.ToolbarItemPress(item))
|
|
2573
|
+
}
|
|
2574
|
+
return
|
|
2575
|
+
}
|
|
2576
|
+
preflightUpdateJSON = preparation.updateJSON
|
|
2577
|
+
noteDocumentVersionFromUpdateJSON(preflightUpdateJSON)
|
|
2578
|
+
clearPendingNativeActionRetry()
|
|
2579
|
+
}
|
|
2580
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
908
2581
|
when (item.type) {
|
|
909
2582
|
ToolbarItemKind.mark -> item.mark?.let { richTextView.editorEditText.performToolbarToggleMark(it) }
|
|
910
2583
|
ToolbarItemKind.heading -> item.headingLevel?.let { richTextView.editorEditText.performToolbarToggleHeading(it) }
|
|
@@ -918,7 +2591,20 @@ class NativeEditorExpoView(
|
|
|
918
2591
|
null -> Unit
|
|
919
2592
|
}
|
|
920
2593
|
ToolbarItemKind.node -> item.nodeType?.let { richTextView.editorEditText.performToolbarInsertNode(it) }
|
|
921
|
-
ToolbarItemKind.action -> item.key?.let {
|
|
2594
|
+
ToolbarItemKind.action -> item.key?.let {
|
|
2595
|
+
if (handleDestroyedCurrentEditorIfNeeded()) return
|
|
2596
|
+
val payload = mutableMapOf<String, Any>(
|
|
2597
|
+
"key" to it,
|
|
2598
|
+
"editorId" to richTextView.editorId
|
|
2599
|
+
)
|
|
2600
|
+
addPreflightUpdateToEvent(payload, preflightUpdateJSON)
|
|
2601
|
+
if (!payload.containsKey("documentVersion")) {
|
|
2602
|
+
lastDocumentVersion?.let { version ->
|
|
2603
|
+
payload["documentVersion"] = version
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
onToolbarActionForTesting?.invoke(payload) ?: onToolbarAction(payload)
|
|
2607
|
+
}
|
|
922
2608
|
ToolbarItemKind.group -> Unit
|
|
923
2609
|
ToolbarItemKind.separator -> Unit
|
|
924
2610
|
}
|