Make CandidatesView support candidate action menu (#875)

This commit is contained in:
Panda527 2026-05-03 17:09:00 +08:00 committed by GitHub
parent bb9cdbdbe9
commit 134880d267
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 58 additions and 52 deletions

View File

@ -5,19 +5,27 @@
package org.fcitx.fcitx5.android.input package org.fcitx.fcitx5.android.input
import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.widget.PopupMenu
import androidx.constraintlayout.widget.ConstraintLayout 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.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.daemon.FcitxConnection 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.prefs.AppPrefs
import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.data.theme.Theme
import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.data.theme.ThemeManager
import org.fcitx.fcitx5.android.data.theme.ThemePrefs import org.fcitx.fcitx5.android.data.theme.ThemePrefs
import org.fcitx.fcitx5.android.utils.item
import org.fcitx.fcitx5.android.utils.navbarFrameHeight import org.fcitx.fcitx5.android.utils.navbarFrameHeight
import splitties.resources.styledColor
import kotlin.math.max import kotlin.math.max
abstract class BaseInputView( 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 private val navbarBackground by ThemeManager.prefs.navbarBackground
protected fun getNavBarBottomInset(windowInsets: WindowInsets): Int { protected fun getNavBarBottomInset(windowInsets: WindowInsets): Int {

View File

@ -100,6 +100,7 @@ class CandidatesView(
private val candidatesUi = PagedCandidatesUi( private val candidatesUi = PagedCandidatesUi(
ctx, theme, setupTextView, ctx, theme, setupTextView,
onCandidateClick = { index -> fcitx.launchOnReady { it.select(index) } }, onCandidateClick = { index -> fcitx.launchOnReady { it.select(index) } },
onCandidateAction = { index, text, view -> showCandidateActionMenu(index, text, view) },
onPrevPage = { fcitx.launchOnReady { it.offsetCandidatePage(-1) } }, onPrevPage = { fcitx.launchOnReady { it.offsetCandidatePage(-1) } },
onNextPage = { 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) 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 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.candidates.horizontal.HorizontalCandidateComponent
import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.fcitx
import org.fcitx.fcitx5.android.input.dependency.inputMethodService 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.dependency.theme
import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener import org.fcitx.fcitx5.android.input.keyboard.CommonKeyActionListener
import org.fcitx.fcitx5.android.input.keyboard.KeyAction 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 service by manager.inputMethodService()
protected val theme by manager.theme() protected val theme by manager.theme()
protected val fcitx by manager.fcitx() protected val fcitx by manager.fcitx()
protected val inputView by manager.inputView()
private val commonKeyActionListener: CommonKeyActionListener by manager.must() private val commonKeyActionListener: CommonKeyActionListener by manager.must()
private val bar: KawaiiBarComponent by manager.must() private val bar: KawaiiBarComponent by manager.must()
private val horizontalCandidate: HorizontalCandidateComponent 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) } fcitx.launchOnReady { it.select(holder.idx) }
} }
holder.itemView.setOnLongClickListener { holder.itemView.setOnLongClickListener {
horizontalCandidate.showCandidateActionMenu(holder) inputView.showCandidateActionMenu(holder.idx, holder.text, holder.ui.root)
true true
} }
} }

View File

@ -29,6 +29,7 @@ class PagedCandidatesUi(
val theme: Theme, val theme: Theme,
private val setupTextView: TextView.() -> Unit, private val setupTextView: TextView.() -> Unit,
private val onCandidateClick: (Int) -> Unit, private val onCandidateClick: (Int) -> Unit,
private val onCandidateAction: (Int, String, View) -> Unit,
private val onPrevPage: () -> Unit, private val onPrevPage: () -> Unit,
private val onNextPage: () -> Unit private val onNextPage: () -> Unit
) : Ui { ) : Ui {
@ -80,6 +81,10 @@ class PagedCandidatesUi(
holder.ui.root.setOnClickListener { holder.ui.root.setOnClickListener {
onCandidateClick.invoke(position) onCandidateClick.invoke(position)
} }
holder.ui.root.setOnLongClickListener { v ->
onCandidateAction.invoke(position, candidate.text, v)
true
}
holder.ui.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> { holder.ui.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
width = if (isVertical) MATCH_PARENT else WRAP_CONTENT 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.content.res.Configuration
import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape 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.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import org.fcitx.fcitx5.android.R import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.core.FcitxEvent
import org.fcitx.fcitx5.android.daemon.launchOnReady 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.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.ExpandedCandidatesEmpty import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.BooleanKey.ExpandedCandidatesEmpty
import org.fcitx.fcitx5.android.input.bar.ExpandButtonStateMachine.TransitionEvent.ExpandedCandidatesUpdated 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.context
import org.fcitx.fcitx5.android.input.dependency.fcitx import org.fcitx.fcitx5.android.input.dependency.fcitx
import org.fcitx.fcitx5.android.input.dependency.inputMethodService 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.dependency.theme
import org.fcitx.fcitx5.android.utils.item
import org.mechdancer.dependency.manager.must import org.mechdancer.dependency.manager.must
import splitties.dimensions.dp import splitties.dimensions.dp
import splitties.resources.styledColor
import kotlin.math.max import kotlin.math.max
class HorizontalCandidateComponent : class HorizontalCandidateComponent :
@ -52,6 +44,7 @@ class HorizontalCandidateComponent :
private val context by manager.context() private val context by manager.context()
private val fcitx by manager.fcitx() private val fcitx by manager.fcitx()
private val theme by manager.theme() private val theme by manager.theme()
private val inputView by manager.inputView()
private val bar: KawaiiBarComponent by manager.must() private val bar: KawaiiBarComponent by manager.must()
private val fillStyle by AppPrefs.getInstance().keyboard.horizontalCandidateStyle private val fillStyle by AppPrefs.getInstance().keyboard.horizontalCandidateStyle
@ -105,7 +98,7 @@ class HorizontalCandidateComponent :
fcitx.launchOnReady { it.select(holder.idx) } fcitx.launchOnReady { it.select(holder.idx) }
} }
holder.itemView.setOnLongClickListener { holder.itemView.setOnLongClickListener {
showCandidateActionMenu(holder) inputView.showCandidateActionMenu(holder.idx, holder.text, holder.ui.root)
true true
} }
} }
@ -204,43 +197,4 @@ class HorizontalCandidateComponent :
refreshExpanded(0) 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()
}
}
}
}