mirror of
https://github.com/fcitx5-android/fcitx5-android.git
synced 2026-08-02 04:44:35 +08:00
Introduce PickerPolicy and Popup.Keyboard.Explicit for decoupling emoji picker
This commit is contained in:
parent
4dda210837
commit
2e36b2bf1b
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.keyboard
|
||||
|
||||
@ -117,7 +117,12 @@ open class KeyDef(
|
||||
|
||||
class AltPreview(content: String, val alternative: String) : Preview(content)
|
||||
|
||||
class Keyboard(val label: String) : Popup()
|
||||
sealed class Keyboard : Popup() {
|
||||
data class Preset(val label: String, val transformPunctuation: Boolean = true) :
|
||||
Keyboard()
|
||||
|
||||
class Explicit(val items: Array<String>) : Keyboard()
|
||||
}
|
||||
|
||||
class Menu(val items: Array<Item>) : Popup() {
|
||||
class Item(val label: String, @DrawableRes val icon: Int, val action: KeyAction)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.keyboard
|
||||
|
||||
@ -35,7 +35,7 @@ class SymbolKey(
|
||||
),
|
||||
popup ?: arrayOf(
|
||||
Popup.Preview(symbol),
|
||||
Popup.Keyboard(symbol)
|
||||
Popup.Keyboard.Preset(symbol)
|
||||
)
|
||||
)
|
||||
|
||||
@ -57,7 +57,7 @@ class AlphabetKey(
|
||||
),
|
||||
popup ?: arrayOf(
|
||||
Popup.AltPreview(character, punctuation),
|
||||
Popup.Keyboard(character)
|
||||
Popup.Keyboard.Preset(character)
|
||||
)
|
||||
)
|
||||
|
||||
@ -78,7 +78,7 @@ class AlphabetDigitKey(
|
||||
),
|
||||
popup ?: arrayOf(
|
||||
Popup.AltPreview(character, altText),
|
||||
Popup.Keyboard(character)
|
||||
Popup.Keyboard.Preset(character)
|
||||
)
|
||||
) {
|
||||
constructor(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.keyboard
|
||||
|
||||
@ -185,10 +185,17 @@ class TextKeyboard(
|
||||
is PopupAction.PreviewAction -> action.copy(content = transformPopupPreview(action.content))
|
||||
is PopupAction.PreviewUpdateAction -> action.copy(content = transformPopupPreview(action.content))
|
||||
is PopupAction.ShowKeyboardAction -> {
|
||||
val label = action.keyboard.label
|
||||
if (label.length == 1 && label[0].isLetter())
|
||||
action.copy(keyboard = KeyDef.Popup.Keyboard(transformAlphabet(label)))
|
||||
else action
|
||||
when (action.keyboard) {
|
||||
is KeyDef.Popup.Keyboard.Preset -> {
|
||||
val label = action.keyboard.label
|
||||
if (label.length == 1 && label[0].isLetter())
|
||||
action.copy(
|
||||
keyboard = action.keyboard.copy(label = transformAlphabet(label))
|
||||
)
|
||||
else action
|
||||
}
|
||||
is KeyDef.Popup.Keyboard.Explicit -> action
|
||||
}
|
||||
}
|
||||
else -> action
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.picker
|
||||
|
||||
@ -29,7 +29,6 @@ import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance.Border
|
||||
import org.fcitx.fcitx5.android.input.keyboard.KeyDef.Appearance.Variant
|
||||
import org.fcitx.fcitx5.android.input.keyboard.KeyView
|
||||
import org.fcitx.fcitx5.android.input.keyboard.TextKeyView
|
||||
import org.fcitx.fcitx5.android.input.popup.EmojiModifier
|
||||
import org.fcitx.fcitx5.android.input.popup.PopupAction
|
||||
import org.fcitx.fcitx5.android.input.popup.PopupActionListener
|
||||
import splitties.views.dsl.constraintlayout.below
|
||||
@ -72,10 +71,7 @@ class PickerPageUi(
|
||||
Low(12, 4, 3, 19f, true, false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val BackspaceAction = SymAction(KeySym(FcitxKeyMapping.FcitxKey_BackSpace))
|
||||
private var popupOnKeyPress by AppPrefs.getInstance().keyboard.popupOnKeyPress
|
||||
}
|
||||
private val popupOnKeyPress by AppPrefs.getInstance().keyboard.popupOnKeyPress
|
||||
|
||||
var keyActionListener: KeyActionListener? = null
|
||||
var popupActionListener: PopupActionListener? = null
|
||||
@ -106,8 +102,9 @@ class PickerPageUi(
|
||||
)
|
||||
|
||||
private val backspaceKey by lazy {
|
||||
val backspaceAction = SymAction(KeySym(FcitxKeyMapping.FcitxKey_BackSpace))
|
||||
val action: (View) -> Unit = {
|
||||
keyActionListener?.onKeyAction(BackspaceAction, Source.Keyboard)
|
||||
keyActionListener?.onKeyAction(backspaceAction, Source.Keyboard)
|
||||
}
|
||||
val listener = View.OnClickListener { action.invoke(it) }
|
||||
ImageKeyView(ctx, theme, backspaceAppearance).apply {
|
||||
@ -175,11 +172,26 @@ class PickerPageUi(
|
||||
layoutParams = ViewGroup.LayoutParams(matchParent, matchParent)
|
||||
}
|
||||
|
||||
private fun onSymbolClick(str: String) {
|
||||
keyActionListener?.onKeyAction(CommitAction(str), Source.Keyboard)
|
||||
fun setItems(items: List<String>) {
|
||||
keyViews.forEachIndexed { i, keyView ->
|
||||
keyView.apply {
|
||||
if (i >= items.size) {
|
||||
isEnabled = false
|
||||
mainText.text = ""
|
||||
setOnClickListener(null)
|
||||
} else {
|
||||
isEnabled = true
|
||||
mainText.text = items[i]
|
||||
setOnClickListener { onItemClick(items[i]) }
|
||||
}
|
||||
swipeEnabled = false
|
||||
onGestureListener = null
|
||||
setOnLongClickListener(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setItems(items: List<String>, withSkinTone: Boolean = false) {
|
||||
fun setItems(items: List<String>, policy: PickerPolicy) {
|
||||
keyViews.forEachIndexed { i, keyView ->
|
||||
keyView.apply {
|
||||
if (i >= items.size) {
|
||||
@ -191,28 +203,15 @@ class PickerPageUi(
|
||||
onGestureListener = null
|
||||
} else {
|
||||
isEnabled = true
|
||||
val label = items[i]
|
||||
val commitString =
|
||||
if (withSkinTone) EmojiModifier.getPreferredTone(label) else label
|
||||
mainText.text = commitString
|
||||
val item = items[i]
|
||||
mainText.text = item
|
||||
setOnClickListener {
|
||||
onSymbolClick(commitString)
|
||||
onItemClick(item)
|
||||
}
|
||||
setOnLongClickListener { view ->
|
||||
view as KeyView
|
||||
if (!popupOnKeyPress) {
|
||||
// in case "popup on keypress" is disabled, popup keyboard need to know
|
||||
// the actual bounds on press. see [^1] as well
|
||||
view.updateBounds()
|
||||
}
|
||||
// TODO: maybe popup keyboard should just accept String as label?
|
||||
onPopupAction(
|
||||
PopupAction.ShowKeyboardAction(
|
||||
view.id,
|
||||
KeyDef.Popup.Keyboard(label),
|
||||
bounds
|
||||
)
|
||||
)
|
||||
setOnLongClickListener longClick@{ view ->
|
||||
if (view !is KeyView) return@longClick false
|
||||
val popup = policy.popup(item) ?: return@longClick false
|
||||
onItemLongClick(view, popup)
|
||||
false
|
||||
}
|
||||
swipeEnabled = true
|
||||
@ -220,23 +219,13 @@ class PickerPageUi(
|
||||
view as KeyView
|
||||
when (event.type) {
|
||||
CustomGestureView.GestureType.Down -> {
|
||||
if (popupOnKeyPress) {
|
||||
// [^1]: bounds is first calculated in KeyView's onLayout(), it
|
||||
// not in screen viewport at the time of layout.
|
||||
// eg. it's inside the next page of ViewPager
|
||||
// so update bounds when it's pressed
|
||||
view.updateBounds()
|
||||
onPopupAction(
|
||||
PopupAction.PreviewAction(view.id, label, view.bounds)
|
||||
)
|
||||
}
|
||||
onPopupShow(view, item)
|
||||
// never "consume" the gesture on touch down
|
||||
false
|
||||
}
|
||||
|
||||
CustomGestureView.GestureType.Move -> {
|
||||
onPopupChangeFocus(view.id, event.x, event.y)
|
||||
}
|
||||
|
||||
CustomGestureView.GestureType.Up -> {
|
||||
onPopupTrigger(view.id).also {
|
||||
onPopupAction(PopupAction.DismissAction(view.id))
|
||||
@ -249,22 +238,45 @@ class PickerPageUi(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onItemClick(item: String) {
|
||||
keyActionListener?.onKeyAction(CommitAction(item), Source.Keyboard)
|
||||
}
|
||||
|
||||
private fun onPopupAction(action: PopupAction) {
|
||||
popupActionListener?.onPopupAction(action)
|
||||
}
|
||||
|
||||
private fun onItemLongClick(view: KeyView, popup: KeyDef.Popup.Keyboard) {
|
||||
if (!popupOnKeyPress) {
|
||||
// in case "popup on keypress" is disabled, popup keyboard need to know
|
||||
// the actual bounds on press. see [^1] as well
|
||||
view.updateBounds()
|
||||
}
|
||||
onPopupAction(PopupAction.ShowKeyboardAction(view.id, popup, view.bounds))
|
||||
}
|
||||
|
||||
private fun onPopupShow(view: KeyView, item: String) {
|
||||
if (!popupOnKeyPress) return
|
||||
// [^1]: bounds is first calculated in KeyView's onLayout(), it
|
||||
// not in screen viewport at the time of layout.
|
||||
// e.g. it's inside the next page of ViewPager
|
||||
// so update bounds when it's pressed
|
||||
view.updateBounds()
|
||||
onPopupAction(PopupAction.PreviewAction(view.id, item, view.bounds))
|
||||
return
|
||||
}
|
||||
|
||||
private fun onPopupChangeFocus(viewId: Int, x: Float, y: Float): Boolean {
|
||||
val changeFocusAction = PopupAction.ChangeFocusAction(viewId, x, y)
|
||||
popupActionListener?.onPopupAction(changeFocusAction)
|
||||
onPopupAction(changeFocusAction)
|
||||
return changeFocusAction.outResult
|
||||
}
|
||||
|
||||
private fun onPopupTrigger(viewId: Int): Boolean {
|
||||
val triggerAction = PopupAction.TriggerAction(viewId)
|
||||
// TODO: maybe popup keyboard should just yield String value?
|
||||
onPopupAction(triggerAction)
|
||||
val action = triggerAction.outAction as? FcitxKeyAction ?: return false
|
||||
onSymbolClick(action.act)
|
||||
onItemClick(action.act)
|
||||
onPopupAction(PopupAction.DismissAction(viewId))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.picker
|
||||
|
||||
import android.text.TextPaint
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.fcitx.fcitx5.android.data.RecentlyUsed
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.data.theme.Theme
|
||||
import org.fcitx.fcitx5.android.input.keyboard.KeyActionListener
|
||||
import org.fcitx.fcitx5.android.input.popup.PopupActionListener
|
||||
@ -20,8 +19,8 @@ class PickerPagesAdapter(
|
||||
private val rawData: List<Pair<PickerData.Category, Array<String>>>,
|
||||
private val density: PickerPageUi.Density,
|
||||
recentlyUsedFileName: String,
|
||||
private val bordered: Boolean = false,
|
||||
private val isEmoji: Boolean = false
|
||||
private val bordered: Boolean,
|
||||
private val policy: PickerPolicy
|
||||
) : RecyclerView.Adapter<PickerPagesAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(val ui: PickerPageUi) : RecyclerView.ViewHolder(ui.root)
|
||||
@ -38,17 +37,9 @@ class PickerPagesAdapter(
|
||||
*/
|
||||
private val pages: MutableList<List<String>> = mutableListOf(listOf())
|
||||
|
||||
private fun buildCategories(
|
||||
data: List<Pair<PickerData.Category, Array<String>>>,
|
||||
knowGraphOnly: Boolean = false
|
||||
) {
|
||||
val textPaint = if (knowGraphOnly) TextPaint() else null
|
||||
private fun buildCategories(data: List<Pair<PickerData.Category, Array<String>>>) {
|
||||
data.forEach { (cat, arr) ->
|
||||
val list = if (textPaint != null) {
|
||||
arr.filter { textPaint.hasGlyph(it) }
|
||||
} else {
|
||||
arr.asList()
|
||||
}
|
||||
val list = arr.filter(policy::filter).map(policy::transform)
|
||||
val chunks = list.chunked(density.pageSize)
|
||||
categories.add(cat to IntRange(pages.size, pages.size + chunks.size - 1))
|
||||
pages.addAll(chunks)
|
||||
@ -56,20 +47,29 @@ class PickerPagesAdapter(
|
||||
}
|
||||
|
||||
init {
|
||||
buildCategories(
|
||||
rawData,
|
||||
isEmoji && AppPrefs.getInstance().symbols.hideUnsupportedEmojis.getValue()
|
||||
)
|
||||
buildCategories(rawData)
|
||||
}
|
||||
|
||||
fun rebuildCategories(knowGraphOnly: Boolean = false) {
|
||||
private fun rebuildCategories() {
|
||||
categories.clear()
|
||||
// empty "RecentlyUsed" category
|
||||
categories.add(PickerData.RecentlyUsedCategory to IntRange(0, 0))
|
||||
pages.clear()
|
||||
// empty "RecentlyUsed" page
|
||||
pages.add(emptyList())
|
||||
buildCategories(rawData, knowGraphOnly)
|
||||
buildCategories(rawData)
|
||||
}
|
||||
|
||||
private var lastInvalidateKey = policy.invalidateKey()
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun refreshIfNeeded() {
|
||||
val newKey = policy.invalidateKey()
|
||||
if (lastInvalidateKey != newKey) {
|
||||
lastInvalidateKey = newKey
|
||||
rebuildCategories()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
private val recentlyUsed = RecentlyUsed(recentlyUsedFileName, density.pageSize)
|
||||
@ -102,18 +102,21 @@ class PickerPagesAdapter(
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.ui.setItems(pages[position], isEmoji)
|
||||
if (position == 0) {
|
||||
// RecentlyUsed content should be displayed as-is, without popups
|
||||
holder.ui.setItems(recentlyUsed.items)
|
||||
} else {
|
||||
holder.ui.setItems(pages[position], policy)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: ViewHolder) {
|
||||
holder.ui.keyActionListener = keyActionListener
|
||||
if (holder.bindingAdapterPosition == 0) {
|
||||
holder.ui.popupActionListener = if (holder.bindingAdapterPosition == 0) {
|
||||
// prevent popup on RecentlyUsed page
|
||||
holder.ui.popupActionListener = null
|
||||
// RecentlyUsed content are already modified with skin tones
|
||||
holder.ui.setItems(recentlyUsed.items, withSkinTone = false)
|
||||
null
|
||||
} else {
|
||||
holder.ui.popupActionListener = popupActionListener
|
||||
popupActionListener
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
|
||||
package org.fcitx.fcitx5.android.input.picker
|
||||
|
||||
import android.text.TextPaint
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.input.keyboard.KeyDef
|
||||
import org.fcitx.fcitx5.android.input.popup.EmojiModifier
|
||||
|
||||
interface PickerPolicy {
|
||||
/**
|
||||
* Whether Picker should include this symbol
|
||||
*/
|
||||
fun filter(raw: String): Boolean
|
||||
|
||||
/**
|
||||
* Actual string to commit when this symbol was selected
|
||||
*/
|
||||
fun transform(raw: String): String
|
||||
|
||||
/**
|
||||
* Long press options for this symbol
|
||||
*/
|
||||
fun popup(raw: String): KeyDef.Popup.Keyboard?
|
||||
|
||||
/**
|
||||
* Return different value to make the Picker rebuild all pages on attach
|
||||
*/
|
||||
fun invalidateKey(): Any
|
||||
}
|
||||
|
||||
/**
|
||||
* Default policy: show all symbols as provided and never need to rebuild
|
||||
*/
|
||||
class DefaultPickerPolicy : PickerPolicy {
|
||||
override fun filter(raw: String) = true
|
||||
override fun transform(raw: String) = raw
|
||||
override fun popup(raw: String) =
|
||||
KeyDef.Popup.Keyboard.Preset(label = raw, transformPunctuation = false)
|
||||
|
||||
override fun invalidateKey() = Unit
|
||||
}
|
||||
|
||||
/**
|
||||
* Emoji policy: filter unsupported glyph, transform skintone, rebuild on preference changes
|
||||
*/
|
||||
class EmojiPickerPolicy : PickerPolicy {
|
||||
data class Prefs(
|
||||
val hideUnsupportedEmojis: Boolean,
|
||||
val defaultSkinTone: EmojiModifier.SkinTone
|
||||
)
|
||||
|
||||
private val symbolPrefs = AppPrefs.getInstance().symbols
|
||||
private val hideUnsupportedEmojis by symbolPrefs.hideUnsupportedEmojis
|
||||
private val defaultSkinTone by symbolPrefs.defaultEmojiSkinTone
|
||||
|
||||
private val prefs get() = Prefs(hideUnsupportedEmojis, defaultSkinTone)
|
||||
|
||||
override fun filter(raw: String): Boolean {
|
||||
return if (hideUnsupportedEmojis) TextPaint().hasGlyph(raw) else true
|
||||
}
|
||||
|
||||
override fun transform(raw: String): String {
|
||||
return EmojiModifier.getPreferredTone(raw, defaultSkinTone)
|
||||
}
|
||||
|
||||
override fun popup(raw: String): KeyDef.Popup.Keyboard? {
|
||||
val items = EmojiModifier.produceSkinTones(raw, defaultSkinTone) ?: return null
|
||||
return KeyDef.Popup.Keyboard.Explicit(items)
|
||||
}
|
||||
|
||||
override fun invalidateKey(): Prefs = prefs
|
||||
}
|
||||
@ -1,15 +1,12 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.picker
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.transition.Transition
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.data.prefs.ManagedPreference
|
||||
import org.fcitx.fcitx5.android.data.theme.ThemeManager
|
||||
import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent
|
||||
import org.fcitx.fcitx5.android.input.dependency.theme
|
||||
@ -32,7 +29,8 @@ class PickerWindow(
|
||||
private val density: PickerPageUi.Density,
|
||||
private val switchKey: KeyDef,
|
||||
private val popupPreview: Boolean = true,
|
||||
private val followKeyBorder: Boolean = true
|
||||
private val followKeyBorder: Boolean = true,
|
||||
private val policy: PickerPolicy = DefaultPickerPolicy()
|
||||
) : InputWindow.ExtendedInputWindow<PickerWindow>(), EssentialWindow {
|
||||
|
||||
enum class Key : EssentialWindow.Key {
|
||||
@ -104,14 +102,12 @@ class PickerWindow(
|
||||
}
|
||||
}
|
||||
|
||||
private val isEmoji = key === Key.Emoji
|
||||
|
||||
override fun onCreateView() = PickerLayout(context, theme, switchKey).apply {
|
||||
pickerLayout = this
|
||||
val bordered = followKeyBorder && keyBorder
|
||||
pickerPagesAdapter = PickerPagesAdapter(
|
||||
theme, keyActionListener, popupActionListener, data,
|
||||
density, key.name, bordered, isEmoji
|
||||
density, key.name, bordered, policy
|
||||
)
|
||||
tabsUi.apply {
|
||||
setTabs(pickerPagesAdapter.getCategoryList())
|
||||
@ -149,39 +145,17 @@ class PickerWindow(
|
||||
|
||||
override fun onCreateBarExtension() = pickerLayout.tabsUi.root
|
||||
|
||||
val symbolPrefs = AppPrefs.getInstance().symbols
|
||||
private val hideUnsupportedEmojisPrefs = symbolPrefs.hideUnsupportedEmojis
|
||||
private val defaultEmojiSkinTonePrefs = symbolPrefs.defaultEmojiSkinTone
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private val initDataListener = ManagedPreference.OnChangeListener<Any> { _, _ ->
|
||||
pickerPagesAdapter.rebuildCategories(hideUnsupportedEmojisPrefs.getValue())
|
||||
pickerPagesAdapter.notifyDataSetChanged()
|
||||
}.takeIf { isEmoji }
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
private val refreshPagesListener = ManagedPreference.OnChangeListener<Any> { _, _ ->
|
||||
pickerPagesAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onAttached() {
|
||||
pickerLayout.embeddedKeyboard.also {
|
||||
pickerPagesAdapter.refreshIfNeeded()
|
||||
it.onReturnDrawableUpdate(returnKeyDrawable.resourceId)
|
||||
it.keyActionListener = keyActionListener
|
||||
}
|
||||
if (isEmoji) {
|
||||
hideUnsupportedEmojisPrefs.registerOnChangeListener(initDataListener!!)
|
||||
defaultEmojiSkinTonePrefs.registerOnChangeListener(refreshPagesListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetached() {
|
||||
popup.dismissAll()
|
||||
pickerLayout.embeddedKeyboard.keyActionListener = null
|
||||
if (isEmoji) {
|
||||
hideUnsupportedEmojisPrefs.unregisterOnChangeListener(initDataListener!!)
|
||||
defaultEmojiSkinTonePrefs.unregisterOnChangeListener(refreshPagesListener)
|
||||
}
|
||||
}
|
||||
|
||||
override val showTitle = false
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
package org.fcitx.fcitx5.android.input.picker
|
||||
|
||||
@ -23,7 +23,8 @@ fun emojiPicker(): PickerWindow = PickerWindow(
|
||||
density = PickerPageUi.Density.Medium,
|
||||
switchKey = TextPickerSwitchKey(":-)", PickerWindow.Key.Emoticon),
|
||||
popupPreview = false,
|
||||
followKeyBorder = false
|
||||
followKeyBorder = false,
|
||||
policy = EmojiPickerPolicy()
|
||||
)
|
||||
|
||||
fun emoticonPicker(): PickerWindow = PickerWindow(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2025-2026 Fcitx5 for Android Contributors
|
||||
*/
|
||||
|
||||
package org.fcitx.fcitx5.android.input.popup
|
||||
@ -13,7 +13,6 @@ import android.os.Build
|
||||
import android.text.TextPaint
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.fcitx.fcitx5.android.R
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceEnum
|
||||
|
||||
object EmojiModifier {
|
||||
@ -45,8 +44,6 @@ object EmojiModifier {
|
||||
0x1F9D1, 0x200D, 0x1F91D, 0x200D, 0x1F9D1,
|
||||
)
|
||||
|
||||
private val defaultSkinTone by AppPrefs.getInstance().symbols.defaultEmojiSkinTone
|
||||
|
||||
fun isSupported(): Boolean {
|
||||
// UProperty.EMOJI_MODIFIER_BASE requires API 28
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
|
||||
@ -107,22 +104,20 @@ object EmojiModifier {
|
||||
return DefaultTextPaint.hasGlyph(emoji)
|
||||
}
|
||||
|
||||
fun getPreferredTone(emoji: String): String {
|
||||
fun getPreferredTone(emoji: String, tone: SkinTone): String {
|
||||
if (!isSupported()) return emoji
|
||||
val (codePoints, modifiable) = getCodePoints(emoji)
|
||||
val tone = defaultSkinTone
|
||||
if (tone == SkinTone.Default || !isModifiable(modifiable)) return emoji
|
||||
val candidate = buildEmoji(codePoints, modifiable, tone)
|
||||
return if (isValidEmoji(candidate)) candidate else emoji
|
||||
}
|
||||
|
||||
fun produceSkinTones(emoji: String): Array<String>? {
|
||||
fun produceSkinTones(emoji: String, excludeTone: SkinTone): Array<String>? {
|
||||
if (!isSupported()) return null
|
||||
val (codePoints, modifiable) = getCodePoints(emoji)
|
||||
val tone = defaultSkinTone
|
||||
if (!isModifiable(modifiable)) return null
|
||||
val candidates = SkinTone.entries
|
||||
.filter { it != tone }
|
||||
.filter { it != excludeTone }
|
||||
.map { buildEmoji(codePoints, modifiable, it) }
|
||||
.filter { isValidEmoji(it) }
|
||||
return if (candidates.isEmpty()) null else candidates.toTypedArray()
|
||||
|
||||
@ -116,18 +116,23 @@ class PopupComponent :
|
||||
}
|
||||
|
||||
private fun showKeyboard(viewId: Int, keyboard: KeyDef.Popup.Keyboard, bounds: Rect) {
|
||||
val keys = PopupPreset[keyboard.label]
|
||||
?: EmojiModifier.produceSkinTones(keyboard.label)
|
||||
?: return
|
||||
var keys: Array<String>
|
||||
var labels: Array<String>
|
||||
when (keyboard) {
|
||||
is KeyDef.Popup.Keyboard.Preset -> {
|
||||
val preset = PopupPreset[keyboard.label] ?: return
|
||||
keys = preset
|
||||
labels = if (keyboard.transformPunctuation && punctuation.enabled) {
|
||||
Array(keys.size) { punctuation.transform(keys[it]) }
|
||||
} else keys
|
||||
}
|
||||
is KeyDef.Popup.Keyboard.Explicit -> {
|
||||
keys = keyboard.items
|
||||
labels = keyboard.items
|
||||
}
|
||||
}
|
||||
// clear popup preview text OR create empty popup preview
|
||||
showingEntryUi[viewId]?.setText("") ?: showPopup(viewId, "", bounds)
|
||||
reallyShowKeyboard(viewId, keys, bounds)
|
||||
}
|
||||
|
||||
private fun reallyShowKeyboard(viewId: Int, keys: Array<String>, bounds: Rect) {
|
||||
val labels = if (punctuation.enabled) {
|
||||
Array(keys.size) { punctuation.transform(keys[it]) }
|
||||
} else keys
|
||||
val keyboardUi = PopupKeyboardUi(
|
||||
context,
|
||||
theme,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user