Skip to content

Commit

Permalink
refactor: extract CandidatesView's cursor anchor updating to IMS
Browse files Browse the repository at this point in the history
  • Loading branch information
WhiredPlanck committed Jan 29, 2025
1 parent d66eaa6 commit f917fa4
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.view.ViewTreeObserver.OnPreDrawListener
import android.view.inputmethod.CursorAnchorInfo
import androidx.annotation.Size
import androidx.core.graphics.component1
import androidx.core.graphics.component2
import androidx.core.graphics.component3
Expand Down Expand Up @@ -47,8 +47,6 @@ class CandidatesView(
rime: RimeSession,
theme: Theme,
) : BaseInputMessenger(service, rime, theme) {
var useVirtualKeyboard: Boolean = true

private val ctx = context.withTheme(android.R.style.Theme_DeviceDefault_Settings)

private val position by AppPrefs.defaultInstance().candidates.position
Expand Down Expand Up @@ -131,19 +129,11 @@ class CandidatesView(
}
val selfWidth = width.toFloat()
val selfHeight = height.toFloat()
val (_, inputViewHeight) =
intArrayOf(0, 0)
.also { service.inputView?.keyboardView?.getLocationInWindow(it) }

val minX = 0f
val minY = 0f
val maxX = parentWidth - selfWidth
val maxY =
if (useVirtualKeyboard) {
inputViewHeight - selfHeight
} else {
parentHeight - selfHeight
}
val maxY = (if (bottom + selfHeight > parentHeight) top else parentHeight) - selfHeight
when (position) {
PopupPosition.TOP_RIGHT -> {
x = maxX
Expand Down Expand Up @@ -181,41 +171,14 @@ class CandidatesView(
shouldUpdatePosition = false
}

private val decorLocation = floatArrayOf(0f, 0f)

fun updateCursorAnchor(
info: CursorAnchorInfo,
updateDecorLocation: (FloatArray, FloatArray) -> Unit,
anchorPosition: RectF,
@Size(2) parent: FloatArray,
) {
val bounds = info.getCharacterBounds(0)
// update anchorPosition
if (bounds == null) {
// composing is disabled in target app or trime settings
// use the position of the insertion marker instead
anchorPosition.top = info.insertionMarkerTop
anchorPosition.left = info.insertionMarkerHorizontal
anchorPosition.bottom = info.insertionMarkerBottom
anchorPosition.right = info.insertionMarkerHorizontal
} else {
// for different writing system (e.g. right to left languages),
// we have to calculate the correct RectF
val horizontal = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) bounds.right else bounds.left
anchorPosition.top = bounds.top
anchorPosition.left = horizontal
anchorPosition.bottom = bounds.bottom
anchorPosition.right = horizontal
}
updateDecorLocation(decorLocation, parentSize)
@Suppress("KotlinConstantConditions")
// Any component of anchorPosition can be NaN,
// meaning it will not equal itself!
if (anchorPosition != anchorPosition) {
anchorPosition.set(0f, parentSize[1], 0f, parentSize[1])
return
}
info.matrix.mapRect(anchorPosition)
val (dX, dY) = decorLocation
anchorPosition.offset(-dX, -dY)
this.anchorPosition.set(anchorPosition)
val (parentWidth, parentHeight) = parent
parentSize[0] = parentWidth
parentSize[1] = parentHeight
updatePosition()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ class InputDeviceManager {
private fun setupCandidatesViewCallback(isVirtual: Boolean) {
val shouldSetupView = !isVirtual || candidatesMode == PopupCandidatesMode.ALWAYS_SHOW
candidatesView?.handleMessages = shouldSetupView
candidatesView?.useVirtualKeyboard = isVirtual
if (!shouldSetupView) {
candidatesView?.visibility = View.GONE
}
Expand Down Expand Up @@ -63,7 +62,7 @@ class InputDeviceManager {
if (useVirtualKeyboard == isVirtualKeyboard) {
return
}
// monitor CursorAnchorInfo when switching to ComposingPopupWindow
// monitor CursorAnchorInfo when switching to CandidatesView
service.currentInputConnection.monitorCursorAnchor(!useVirtualKeyboard)
isVirtualKeyboard = useVirtualKeyboard
}
Expand Down
113 changes: 84 additions & 29 deletions app/src/main/java/com/osfans/trime/ime/core/TrimeInputMethodService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.app.Dialog
import android.app.PendingIntent
import android.content.Intent
import android.content.res.Configuration
import android.graphics.RectF
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.Bundle
Expand All @@ -30,7 +31,6 @@ import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
import android.widget.FrameLayout
import androidx.annotation.Keep
import androidx.annotation.Size
import androidx.core.content.ContextCompat
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.isVisible
Expand All @@ -52,6 +52,7 @@ import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.data.theme.ThemeManager
import com.osfans.trime.ime.broadcast.IntentReceiver
import com.osfans.trime.ime.candidates.popup.PopupCandidatesMode
import com.osfans.trime.ime.candidates.suggestion.InlineSuggestionHandler
import com.osfans.trime.ime.composition.CandidatesView
import com.osfans.trime.ime.keyboard.InitializationUi
Expand Down Expand Up @@ -85,7 +86,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
get() = AppPrefs.defaultInstance()
private lateinit var decorView: View
private lateinit var contentView: FrameLayout
var inputView: InputView? = null
private var inputView: InputView? = null
private var candidatesView: CandidatesView? = null
private val inputDeviceManager = InputDeviceManager()
private var initializationUi: InitializationUi? = null
Expand Down Expand Up @@ -345,28 +346,69 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
ColorManager.onSystemNightModeChange(newConfig.isNightMode())
}

private val contentSize = floatArrayOf(0f, 0f)
private val decorLocation = floatArrayOf(0f, 0f)
private val decorLocationInt = intArrayOf(0, 0)
private var decorLocationUpdated = false

private fun updateDecorLocation(
@Size(2) outDecor: FloatArray,
@Size(2) outContent: FloatArray,
) {
if (decorLocationUpdated) return
outContent[0] = contentView.width.toFloat()
outContent[1] = contentView.height.toFloat()
private fun updateDecorLocation() {
contentSize[0] = contentView.width.toFloat()
contentSize[1] =
if (inputDeviceManager.isVirtualKeyboard) {
inputViewLocation[1].toFloat()
} else {
contentView.height.toFloat()
}
decorView.getLocationOnScreen(decorLocationInt)
outDecor[0] = decorLocationInt[0].toFloat()
outDecor[1] = decorLocationInt[1].toFloat()
decorLocation[0] = decorLocationInt[0].toFloat()
decorLocation[1] = decorLocationInt[1].toFloat()
// contentSize and decorLocation can be completely wrong,
// when measuring right after the very first onStartInputView() of an IMS' lifecycle
if (outContent[0] > 0 && outContent[1] > 0) {
if (contentSize[0] > 0 && contentSize[1] > 0) {
decorLocationUpdated = true
}
}

override fun onUpdateCursorAnchorInfo(cursorAnchorInfo: CursorAnchorInfo) {
candidatesView?.updateCursorAnchor(cursorAnchorInfo, ::updateDecorLocation)
private val anchorPosition = RectF()

private fun workaroundNullCursorAnchorInfo() {
anchorPosition.set(0f, contentSize[1], 0f, contentSize[1])
candidatesView?.updateCursorAnchor(anchorPosition, contentSize)
}

override fun onUpdateCursorAnchorInfo(info: CursorAnchorInfo) {
val bounds = info.getCharacterBounds(0)
// update anchorPosition
if (bounds == null) {
// composing is disabled in target app or trime settings
// use the position of the insertion marker instead
anchorPosition.top = info.insertionMarkerTop
anchorPosition.left = info.insertionMarkerHorizontal
anchorPosition.bottom = info.insertionMarkerBottom
anchorPosition.right = info.insertionMarkerHorizontal
} else {
// for different writing system (e.g. right to left languages),
// we have to calculate the correct RectF
val horizontal = if (candidatesView?.layoutDirection == View.LAYOUT_DIRECTION_RTL) bounds.right else bounds.left
anchorPosition.top = bounds.top
anchorPosition.left = horizontal
anchorPosition.bottom = bounds.bottom
anchorPosition.right = horizontal
}
if (!decorLocationUpdated) {
updateDecorLocation()
}
@Suppress("KotlinConstantConditions")
// Any component of anchorPosition can be NaN,
// meaning it will not equal itself!
if (anchorPosition != anchorPosition) {
workaroundNullCursorAnchorInfo()
return
}
info.matrix.mapRect(anchorPosition)
val (dX, dY) = decorLocation
anchorPosition.offset(-dX, -dY)
candidatesView?.updateCursorAnchor(anchorPosition, contentSize)
}

override fun onUpdateSelection(
Expand Down Expand Up @@ -399,23 +441,23 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
inputView?.updateSelection(newSelStart, newSelEnd)
}

private val inputViewLocation = intArrayOf(0, 0)

override fun onComputeInsets(outInsets: Insets) {
if (inputDeviceManager.isVirtualKeyboard) {
val (_, y) =
intArrayOf(0, 0).also {
if (inputView?.keyboardView?.isVisible == true) {
inputView?.keyboardView?.getLocationInWindow(it)
} else {
initializationUi?.initial?.getLocationInWindow(it)
}
}
if (inputView?.keyboardView?.isVisible == true) {
inputView?.keyboardView?.getLocationInWindow(inputViewLocation)
} else {
initializationUi?.initial?.getLocationInWindow(inputViewLocation)
}
outInsets.apply {
contentTopInsets = y
visibleTopInsets = y
contentTopInsets = inputViewLocation[1]
visibleTopInsets = inputViewLocation[1]
touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
}
} else {
val h = decorView.height
val n = decorView.findViewById<View>(android.R.id.navigationBarBackground)?.height ?: 0
val h = decorView.height - n
outInsets.apply {
contentTopInsets = h
visibleTopInsets = h
Expand Down Expand Up @@ -486,14 +528,13 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
return true
}

private val candidatesMode by AppPrefs.defaultInstance().candidates.mode

override fun onStartInputView(
attribute: EditorInfo,
restarting: Boolean,
) {
Timber.d("onStartInputView: restarting=$restarting")
if (!restarting) {
currentInputConnection?.monitorCursorAnchor()
}
postRimeJob {
updateRimeOption(this)
InputFeedbackManager.loadSoundEffects(this@TrimeInputMethodService)
Expand All @@ -502,9 +543,19 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
showStatusIcon(R.drawable.ic_trime_status) // 狀態欄圖標
}
ContextCompat.getMainExecutor(this@TrimeInputMethodService).execute {
if (inputDeviceManager.evaluateOnStartInputView(attribute, this@TrimeInputMethodService)) {
val useVirtualKeyboard =
inputDeviceManager.evaluateOnStartInputView(attribute, this@TrimeInputMethodService)
if (useVirtualKeyboard) {
inputView?.startInput(attribute, restarting)
}
if (!useVirtualKeyboard || candidatesMode == PopupCandidatesMode.ALWAYS_SHOW) {
if (currentInputConnection?.monitorCursorAnchor() != true) {
if (!decorLocationUpdated) {
updateDecorLocation()
}
workaroundNullCursorAnchorInfo()
}
}
}
when (attribute.inputType and InputType.TYPE_MASK_VARIATION) {
InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
Expand Down Expand Up @@ -578,6 +629,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {

override fun onFinishInputView(finishingInput: Boolean) {
Timber.d("onFinishInputView: finishingInput=$finishingInput")
decorLocationUpdated = false
inputDeviceManager.onFinishInputView()
currentInputConnection?.apply {
if (normalTextEditor) {
Expand Down Expand Up @@ -784,6 +836,7 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
event: KeyEvent,
): Boolean {
if (inputDeviceManager.evaluateOnKeyDown(event, this)) {
decorLocationUpdated = false
forceShowSelf()
}
return forwardKeyEvent(event) || super.onKeyDown(keyCode, event)
Expand All @@ -799,13 +852,15 @@ open class TrimeInputMethodService : LifecycleInputMethodService() {
override fun onViewClicked(focusChanged: Boolean) {
super.onViewClicked(focusChanged)
if (Build.VERSION.SDK_INT < 34) {
decorLocationUpdated = false
inputDeviceManager.evaluateOnViewClicked(this)
}
}

@TargetApi(34)
override fun onUpdateEditorToolType(toolType: Int) {
super.onUpdateEditorToolType(toolType)
decorLocationUpdated = false
inputDeviceManager.evaluateOnUpdateEditorToolType(toolType, this)
}

Expand Down

0 comments on commit f917fa4

Please sign in to comment.