Make CandidatesView support candidate action menu

This commit is contained in:
WhiredPlanck 2026-04-15 22:28:16 +08:00
parent 630f9258dc
commit 3f312292de
6 changed files with 58 additions and 52 deletions

View File

@ -5,19 +5,27 @@
package org.fcitx.fcitx5.android.input
import android.view.View
import android.view.WindowInsets
import android.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.daemon.FcitxConnection
import org.fcitx.fcitx5.android.data.InputFeedbacks
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.data.theme.ThemeManager
import org.fcitx.fcitx5.android.data.theme.ThemePrefs
import org.fcitx.fcitx5.android.utils.item
import org.fcitx.fcitx5.android.utils.navbarFrameHeight
import splitties.resources.styledColor
import kotlin.math.max
abstract class BaseInputView(
@ -57,6 +65,42 @@ abstract class BaseInputView(
}
}
private fun triggerCandidateAction(idx: Int, actionIdx: Int) {
fcitx.runIfReady { triggerCandidateAction(idx, actionIdx) }
}
private var candidateActionMenu: PopupMenu? = null
fun showCandidateActionMenu(idx: Int, text: String, view: View) {
candidateActionMenu?.dismiss()
candidateActionMenu = null
service.lifecycleScope.launch {
val actions = fcitx.runOnReady { getCandidateActions(idx) }
if (actions.isEmpty()) return@launch
InputFeedbacks.hapticFeedback(view, longPress = true)
candidateActionMenu = PopupMenu(context, view).apply {
menu.add(buildSpannedString {
bold {
color(context.styledColor(android.R.attr.colorAccent)) {
append(text)
}
}
}).apply {
isEnabled = false
}
actions.forEach { action ->
menu.item(action.text) {
triggerCandidateAction(idx, action.id)
}
}
setOnDismissListener {
candidateActionMenu = null
}
show()
}
}
}
private val navbarBackground by ThemeManager.prefs.navbarBackground
protected fun getNavBarBottomInset(windowInsets: WindowInsets): Int {

View File

@ -100,6 +100,7 @@ class CandidatesView(
private val candidatesUi = PagedCandidatesUi(
ctx, theme, setupTextView,
onCandidateClick = { index -> fcitx.launchOnReady { it.select(index) } },
onCandidateAction = { index, text, view -> showCandidateActionMenu(index, text, view) },
onPrevPage = { fcitx.launchOnReady { it.offsetCandidatePage(-1) } },
onNextPage = { fcitx.launchOnReady { it.offsetCandidatePage(1) } }
)

View File

@ -32,7 +32,7 @@ class CandidateItemUi(override val ctx: Context, theme: Theme) : Ui {
background = pressHighlightDrawable(theme.keyPressHighlightColor)
/**
* candidate long press feedback is handled by [org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateComponent.showCandidateActionMenu]
* candidate long press feedback is handled by [org.fcitx.fcitx5.android.input.BaseInputView.showCandidateActionMenu]
*/
longPressFeedbackEnabled = false

View File

@ -30,6 +30,7 @@ import org.fcitx.fcitx5.android.input.candidates.expanded.PagingCandidateViewAda
import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateComponent
import org.fcitx.fcitx5.android.input.dependency.fcitx
import org.fcitx.fcitx5.android.input.dependency.inputMethodService
import org.fcitx.fcitx5.android.input.dependency.inputView
import org.fcitx.fcitx5.android.input.dependency.theme
import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener
import org.fcitx.fcitx5.android.input.keyboard.KeyAction
@ -47,6 +48,7 @@ abstract class BaseExpandedCandidateWindow<T : BaseExpandedCandidateWindow<T>> :
protected val service by manager.inputMethodService()
protected val theme by manager.theme()
protected val fcitx by manager.fcitx()
protected val inputView by manager.inputView()
private val commonKeyActionListener: CommonKeyActionListener by manager.must()
private val bar: KawaiiBarComponent by manager.must()
private val horizontalCandidate: HorizontalCandidateComponent by manager.must()
@ -143,7 +145,7 @@ abstract class BaseExpandedCandidateWindow<T : BaseExpandedCandidateWindow<T>> :
fcitx.launchOnReady { it.select(holder.idx) }
}
holder.itemView.setOnLongClickListener {
horizontalCandidate.showCandidateActionMenu(holder)
inputView.showCandidateActionMenu(holder.idx, holder.text, holder.ui.root)
true
}
}

View File

@ -29,6 +29,7 @@ class PagedCandidatesUi(
val theme: Theme,
private val setupTextView: TextView.() -> Unit,
private val onCandidateClick: (Int) -> Unit,
private val onCandidateAction: (Int, String, View) -> Unit,
private val onPrevPage: () -> Unit,
private val onNextPage: () -> Unit
) : Ui {
@ -80,6 +81,10 @@ class PagedCandidatesUi(
holder.ui.root.setOnClickListener {
onCandidateClick.invoke(position)
}
holder.ui.root.setOnLongClickListener { v ->
onCandidateAction.invoke(position, candidate.text, v)
true
}
holder.ui.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
width = if (isVertical) MATCH_PARENT else WRAP_CONTENT
}

View File

@ -8,22 +8,15 @@ package org.fcitx.fcitx5.android.input.candidates.horizontal
import android.content.res.Configuration
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.widget.PopupMenu
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.daemon.launchOnReady
import org.fcitx.fcitx5.android.data.InputFeedbacks
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.ExpandedCandidatesEmpty
import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdated
@ -38,11 +31,10 @@ import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent
import org.fcitx.fcitx5.android.input.dependency.context
import org.fcitx.fcitx5.android.input.dependency.fcitx
import org.fcitx.fcitx5.android.input.dependency.inputMethodService
import org.fcitx.fcitx5.android.input.dependency.inputView
import org.fcitx.fcitx5.android.input.dependency.theme
import org.fcitx.fcitx5.android.utils.item
import org.mechdancer.dependency.manager.must
import splitties.dimensions.dp
import splitties.resources.styledColor
import kotlin.math.max
class HorizontalCandidateComponent :
@ -52,6 +44,7 @@ class HorizontalCandidateComponent :
private val context by manager.context()
private val fcitx by manager.fcitx()
private val theme by manager.theme()
private val inputView by manager.inputView()
private val bar: KawaiiBarComponent by manager.must()
private val fillStyle by AppPrefs.getInstance().keyboard.horizontalCandidateStyle
@ -105,7 +98,7 @@ class HorizontalCandidateComponent :
fcitx.launchOnReady { it.select(holder.idx) }
}
holder.itemView.setOnLongClickListener {
showCandidateActionMenu(holder)
inputView.showCandidateActionMenu(holder.idx, holder.text, holder.ui.root)
true
}
}
@ -204,43 +197,4 @@ class HorizontalCandidateComponent :
refreshExpanded(0)
}
}
private fun triggerCandidateAction(idx: Int, actionIdx: Int) {
fcitx.runIfReady { triggerCandidateAction(idx, actionIdx) }
}
private var candidateActionMenu: PopupMenu? = null
fun showCandidateActionMenu(holder: CandidateViewHolder) {
val idx = holder.idx
val text = holder.text
val view = holder.ui.root
candidateActionMenu?.dismiss()
candidateActionMenu = null
service.lifecycleScope.launch {
val actions = fcitx.runOnReady { getCandidateActions(idx) }
if (actions.isEmpty()) return@launch
InputFeedbacks.hapticFeedback(view, longPress = true)
candidateActionMenu = PopupMenu(context, view).apply {
menu.add(buildSpannedString {
bold {
color(context.styledColor(android.R.attr.colorAccent)) {
append(text)
}
}
}).apply {
isEnabled = false
}
actions.forEach { action ->
menu.item(action.text) {
triggerCandidateAction(idx, action.id)
}
}
setOnDismissListener {
candidateActionMenu = null
}
show()
}
}
}
}
}