Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Physical Keyboard Support with Candidates Window #1528

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import androidx.preference.PreferenceManager
import com.osfans.trime.R
import com.osfans.trime.data.base.DataManager
import com.osfans.trime.ime.candidates.popup.PopupCandidatesMode
import com.osfans.trime.ime.composition.PopupPosition
import com.osfans.trime.ime.core.ComposingTextMode
import com.osfans.trime.ime.enums.FullscreenMode
import com.osfans.trime.util.appContext
import java.lang.ref.WeakReference

Expand Down Expand Up @@ -115,7 +115,6 @@ class AppPrefs(
const val LANDSCAPE_MODE = "keyboard__landscape_mode"
const val SPLIT_SPACE_PERCENT = "keyboard__split_space"
const val SWITCH_ARROW_ENABLED = "keyboard__show_switch_arrow"
const val FULLSCREEN_MODE = "keyboard__fullscreen_mode"

const val HOOK_CTRL_A = "keyboard__hook_ctrl_a"
const val HOOK_CTRL_CV = "keyboard__hook_ctrl_cv"
Expand Down Expand Up @@ -144,7 +143,6 @@ class AppPrefs(
const val REPEAT_INTERVAL = "keyboard__key_repeat_interval"
}

var fullscreenMode by enum(FULLSCREEN_MODE, FullscreenMode.AUTO_SHOW)
val softCursorEnabled by bool(SOFT_CURSOR_ENABLED, true)
val popupKeyPressEnabled = bool(POPUP_KEY_PRESS_ENABLED, false)
val switchesEnabled by bool(SWITCHES_ENABLED, true)
Expand Down Expand Up @@ -189,9 +187,11 @@ class AppPrefs(
) : PreferenceDelegateOwner(shared, R.string.candidates_window) {
companion object {
const val MODE = "candidates__mode"
const val POSITION = "candidates__position"
}

val mode = enum(R.string.candidates_mode, MODE, PopupCandidatesMode.PREEDIT_ONLY)
val mode = enum(R.string.candidates_mode, MODE, PopupCandidatesMode.DISABLED)
val position = enum(R.string.display_position, POSITION, PopupPosition.BOTTOM_LEFT)
}

/**
Expand Down
5 changes: 1 addition & 4 deletions app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.osfans.trime.ime.bar.ui.CandidateUi
import com.osfans.trime.ime.bar.ui.TabUi
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
import com.osfans.trime.ime.candidates.compact.CompactCandidateModule
import com.osfans.trime.ime.candidates.popup.PopupCandidatesMode
import com.osfans.trime.ime.candidates.unrolled.window.FlexboxUnrolledCandidateWindow
import com.osfans.trime.ime.core.TrimeInputMethodService
import com.osfans.trime.ime.dependency.InputScope
Expand Down Expand Up @@ -52,7 +51,6 @@ class QuickBar(
private val prefs = AppPrefs.defaultInstance()

private val showSwitchers get() = prefs.keyboard.switchesEnabled
private val candidatesMode by prefs.candidates.mode

val themedHeight =
theme.generalStyle.candidateViewHeight + theme.generalStyle.commentHeight
Expand Down Expand Up @@ -147,8 +145,7 @@ class QuickBar(
override fun onInputContextUpdate(ctx: RimeProto.Context) {
barStateMachine.push(
QuickBarStateMachine.TransitionEvent.CandidatesUpdated,
QuickBarStateMachine.BooleanKey.CandidateEmpty to
(ctx.menu.candidates.isEmpty() || candidatesMode == PopupCandidatesMode.CURRENT_PAGE),
QuickBarStateMachine.BooleanKey.CandidateEmpty to ctx.menu.candidates.isEmpty(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ import com.osfans.trime.core.CandidateItem
import com.osfans.trime.core.RimeProto
import com.osfans.trime.daemon.RimeSession
import com.osfans.trime.daemon.launchOnReady
import com.osfans.trime.data.prefs.AppPrefs
import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.bar.QuickBar
import com.osfans.trime.ime.bar.UnrollButtonStateMachine
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
import com.osfans.trime.ime.candidates.popup.PopupCandidatesMode
import com.osfans.trime.ime.candidates.unrolled.decoration.FlexboxVerticalDecoration
import com.osfans.trime.ime.core.TrimeInputMethodService
import com.osfans.trime.ime.dependency.InputScope
Expand All @@ -52,8 +50,6 @@ class CompactCandidateModule(
val theme: Theme,
val bar: QuickBar,
) : InputBroadcastReceiver {
private val candidatesMode by AppPrefs.defaultInstance().candidates.mode

private val _unrolledCandidateOffset =
MutableSharedFlow<Int>(
replay = 1,
Expand Down Expand Up @@ -117,7 +113,6 @@ class CompactCandidateModule(
}

override fun onInputContextUpdate(ctx: RimeProto.Context) {
if (candidatesMode != PopupCandidatesMode.PREEDIT_ONLY) return
val candidates = ctx.menu.candidates.map { CandidateItem(it.comment ?: "", it.text) }
val isLastPage = ctx.menu.isLastPage
val previous = ctx.menu.run { pageSize * pageNumber }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package com.osfans.trime.ime.candidates.popup

import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.RecyclerView
Expand Down Expand Up @@ -98,8 +97,6 @@ class PagedCandidatesUi(

override val root =
recyclerView {
visibility = View.GONE

itemAnimator = null
isFocusable = false
adapter = candidatesAdapter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import com.osfans.trime.data.prefs.PreferenceDelegateEnum
enum class PopupCandidatesMode(
override val stringRes: Int,
) : PreferenceDelegateEnum {
CURRENT_PAGE(R.string.current_page_of_candidates),
PREEDIT_ONLY(R.string.preedit_only),
SYSTEM_DEFAULT(R.string.system_default),
INPUT_DEVICE(R.string.depends_on_input_device),
FORCE_SHOW(R.string.force_show),
DISABLED(R.string.disable),
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,34 @@ package com.osfans.trime.ime.composition

import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import com.osfans.trime.R
import com.osfans.trime.core.RimeProto
import com.osfans.trime.daemon.RimeSession
import com.osfans.trime.daemon.launchOnReady
import com.osfans.trime.data.prefs.AppPrefs
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.candidates.popup.PagedCandidatesUi
import com.osfans.trime.ime.candidates.popup.PopupCandidatesMode
import com.osfans.trime.ime.core.TrimeInputMethodService
import splitties.dimensions.dp
import splitties.views.dsl.constraintlayout.below
import splitties.views.dsl.constraintlayout.bottomOfParent
import splitties.views.dsl.constraintlayout.lParams
import splitties.views.dsl.constraintlayout.startOfParent
import splitties.views.dsl.constraintlayout.topOfParent
import splitties.views.dsl.core.add
import splitties.views.dsl.core.withTheme
import splitties.views.dsl.core.wrapContent
import splitties.views.horizontalPadding
import splitties.views.verticalPadding

@SuppressLint("ViewConstructor")
class CandidatesView(
val ctx: Context,
val service: TrimeInputMethodService,
val rime: RimeSession,
val theme: Theme,
) : ConstraintLayout(ctx) {
private val candidatesMode by AppPrefs.defaultInstance().candidates.mode
) : ConstraintLayout(service) {
private val ctx = context.withTheme(android.R.style.Theme_DeviceDefault_Settings)

private var menu = RimeProto.Context.Menu()
private var inputComposition = RimeProto.Context.Composition()
Expand Down Expand Up @@ -69,25 +69,7 @@ class CandidatesView(
// if CandidatesView can be shown, rime engine is ready most of the time,
// so it should be safety to get option immediately
val isHorizontalLayout = rime.run { getRuntimeOption("_horizontal") }
when (candidatesMode) {
PopupCandidatesMode.CURRENT_PAGE -> {
candidatesUi.root.let {
if (it.visibility == View.GONE) {
it.visibility = View.VISIBLE
}
}
candidatesUi.update(menu, isHorizontalLayout)
}

PopupCandidatesMode.PREEDIT_ONLY -> {
candidatesUi.root.let {
if (it.visibility != View.GONE) {
it.visibility = View.GONE
candidatesUi.update(RimeProto.Context.Menu(), isHorizontalLayout)
}
}
}
}
candidatesUi.update(menu, isHorizontalLayout)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later

package com.osfans.trime.ime.composition

import android.graphics.RectF
import android.os.Build
import android.view.Gravity
import android.view.View
import android.view.View.MeasureSpec
import android.view.WindowManager
import android.view.inputmethod.CursorAnchorInfo
import android.widget.PopupWindow
import androidx.core.graphics.component1
import androidx.core.graphics.component2
import androidx.core.graphics.component3
import androidx.core.graphics.component4
import com.osfans.trime.core.RimeCallback
import com.osfans.trime.core.RimeEvent
import com.osfans.trime.daemon.RimeSession
import com.osfans.trime.data.prefs.AppPrefs
import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.core.BaseCallbackHandler
import com.osfans.trime.ime.core.TrimeInputMethodService
import splitties.dimensions.dp
import timber.log.Timber

class ComposingPopupWindow(
private val service: TrimeInputMethodService,
private val rime: RimeSession,
private val theme: Theme,
private val parentView: View,
) {
val root = CandidatesView(service, rime, theme)

var useVirtualKeyboard: Boolean = true

// 悬浮窗口彈出位置
private val position by AppPrefs.defaultInstance().candidates.position

private val window =
PopupWindow(root).apply {
isClippingEnabled = false
inputMethodMode = PopupWindow.INPUT_METHOD_NOT_NEEDED
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
windowLayoutType =
WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG
}
setBackgroundDrawable(
ColorManager.getDrawable(
service,
"text_back_color",
theme.generalStyle.layout.border,
"border_color",
theme.generalStyle.layout.roundCorner,
theme.generalStyle.layout.alpha,
),
)
width = WindowManager.LayoutParams.WRAP_CONTENT
height = WindowManager.LayoutParams.WRAP_CONTENT
elevation =
root.dp(
theme.generalStyle.layout.elevation
.toFloat(),
)
}

private val anchorPosition = RectF()

private val positionUpdater =
Runnable {
val x: Int
val y: Int
root.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
root.requestLayout()
val selfWidth = root.width
val selfHeight = root.height
val (horizontal, top, _, bottom) = anchorPosition
val parentWidth = parentView.width
val parentHeight = parentView.height
val (_, inputViewHeight) =
intArrayOf(0, 0)
.also { service.inputView?.keyboardView?.getLocationInWindow(it) }

val minX = 0
val minY = 0
val maxX = parentWidth - selfWidth
val maxY =
if (useVirtualKeyboard) {
inputViewHeight - selfHeight
} else {
parentHeight - selfHeight
}
when (position) {
PopupPosition.TOP_RIGHT -> {
x = maxX
y = minY
}
PopupPosition.TOP_LEFT -> {
x = minX
y = minY
}
PopupPosition.BOTTOM_RIGHT -> {
x = maxX
y = maxY
}
PopupPosition.BOTTOM_LEFT -> {
x = minX
y = maxY
}
PopupPosition.FOLLOW -> {
x =
if (root.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
val rtlOffset = parentWidth - horizontal
if (rtlOffset + selfWidth > parentWidth) selfWidth - parentWidth else -rtlOffset
} else {
if (horizontal + selfWidth > parentWidth) parentWidth - selfWidth else horizontal
}.toInt()
y = (if (bottom + selfHeight > parentHeight) top - selfHeight else bottom).toInt()
}
}
window.update(x, y, -1, -1)
}

private val baseCallbackHandler =
object : BaseCallbackHandler(service, rime) {
override fun handleRimeCallback(it: RimeCallback) {
if (it is RimeEvent.IpcResponseEvent) {
it.data.context?.let ctx@{
if (it.composition.length > 0) {
root.update(it)
Timber.d("Update! Ready to showup")
updatePosition()
} else {
dismiss()
}
}
}
}
}

var handleCallback: Boolean
get() = baseCallbackHandler.handleCallback
set(value) {
baseCallbackHandler.handleCallback = value
}

fun cancelJob() {
dismiss()
baseCallbackHandler.cancelJob()
}

fun dismiss() {
window.dismiss()
root.removeCallbacks(positionUpdater)
decorLocationUpdated = false
}

private fun updatePosition() {
window.showAtLocation(parentView, Gravity.NO_GRAVITY, 0, 0)
root.post(positionUpdater)
}

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

private fun updateDecorLocation(decorView: View) {
val (dX, dY) =
intArrayOf(0, 0).also { decorView.getLocationOnScreen(it) }
decorLocation[0] = dX.toFloat()
decorLocation[1] = dY.toFloat()
decorLocationUpdated = true
}

fun updateCursorAnchorInfo(
info: CursorAnchorInfo,
decorView: View,
) {
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 (root.layoutDirection == View.LAYOUT_DIRECTION_RTL) bounds.right else bounds.left
anchorPosition.top = bounds.top
anchorPosition.left = horizontal
anchorPosition.bottom = bounds.bottom
anchorPosition.right = horizontal
}
info.matrix.mapRect(anchorPosition)
// avoid calling `decorView.getLocationOnScreen` repeatedly
if (!decorLocationUpdated) {
updateDecorLocation(decorView)
}
val (dX, dY) = decorLocation
anchorPosition.offset(-dX, -dY)
}
}
Loading
Loading