Make voice input selectable (#824)

Co-authored-by: Rocka <i@rocka.me>
This commit is contained in:
Panda527 2026-03-11 01:50:21 +08:00 committed by GitHub
parent ac490f743d
commit a586493f34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 110 additions and 9 deletions

View File

@ -141,8 +141,13 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
"keep_keyboard_letters_uppercase",
false
)
val showVoiceInputButton =
switch(R.string.show_voice_input_button, "show_voice_input_button", false)
val preferredVoiceInput = voiceInputPreference(
R.string.preferred_voice_input, "preferred_voice_input", ""
) { showVoiceInputButton.getValue() }
val expandKeypressArea =
switch(R.string.expand_keypress_area, "expand_keypress_area", false)
val swipeSymbolDirection = enumList(

View File

@ -64,6 +64,20 @@ abstract class ManagedPreferenceCategory(
return list(title, key, defaultValue, codec, entryValues, entryLabels, enableUiOn)
}
protected fun voiceInputPreference(
@StringRes
title: Int,
key: String,
defaultValue: String,
enableUiOn: (() -> Boolean)? = null
): ManagedPreference.PString {
val pref = ManagedPreference.PString(sharedPreferences, key, defaultValue)
val ui = ManagedPreferenceUi.VoiceInputList(title, key, defaultValue, enableUiOn)
pref.register()
ui.registerUi()
return pref
}
protected fun int(
@StringRes
title: Int,

View File

@ -9,10 +9,13 @@ import androidx.annotation.StringRes
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.Preference
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.ui.main.modified.MySwitchPreference
import org.fcitx.fcitx5.android.ui.main.settings.DialogSeekBarPreference
import org.fcitx.fcitx5.android.ui.main.settings.EditTextIntPreference
import org.fcitx.fcitx5.android.ui.main.settings.TwinSeekBarPreference
import org.fcitx.fcitx5.android.utils.InputMethodUtil
import org.fcitx.fcitx5.android.utils.includes
abstract class ManagedPreferenceUi<T : Preference>(
val key: String,
@ -67,6 +70,38 @@ abstract class ManagedPreferenceUi<T : Preference>(
}
}
class VoiceInputList(
@StringRes
val title: Int,
key: String,
val defaultValue: String,
enableUiOn: (() -> Boolean)? = null
) : ManagedPreferenceUi<ListPreference>(key, enableUiOn) {
override fun createUi(context: Context) = ListPreference(context).apply {
key = this@VoiceInputList.key
isIconSpaceReserved = false
isSingleLineTitle = false
summaryProvider = ListPreference.SimpleSummaryProvider.getInstance()
setDefaultValue(defaultValue)
setTitle(this@VoiceInputList.title)
setDialogTitle(this@VoiceInputList.title)
val voiceInputMethods = InputMethodUtil.listVoiceInputMethods()
entryValues = arrayOf("", *voiceInputMethods.map { it.first.id }.toTypedArray())
entries = arrayOf(
context.getString(R.string.system_default),
*voiceInputMethods.map { it.first.loadLabel(context.packageManager) }.toTypedArray()
)
// shows "(Not Available)" if selected id is not present
summaryProvider = Preference.SummaryProvider<ListPreference> { preference ->
if (preference.entryValues.includes(preference.value)) {
preference.entry
} else {
context.getString(R.string._not_available_)
}
}
}
}
class EditTextInt(
@StringRes
val title: Int,

View File

@ -105,6 +105,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
private val expandToolbarByDefault by prefs.keyboard.expandToolbarByDefault
private val toolbarNumRowOnPassword by prefs.keyboard.toolbarNumRowOnPassword
private val showVoiceInputButton by prefs.keyboard.showVoiceInputButton
private val preferredVoiceInput by prefs.keyboard.preferredVoiceInput
private var clipboardTimeoutJob: Job? = null
@ -450,7 +451,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
idleUi.inlineSuggestionsBar.clear()
}
voiceInputSubtype = InputMethodUtil.firstVoiceInput()
voiceInputSubtype = InputMethodUtil.findVoiceSubtype(preferredVoiceInput)
val shouldShowVoiceInput =
showVoiceInputButton && voiceInputSubtype != null && !capFlags.has(CapabilityFlag.Password)
idleUi.setHideKeyboardIsVoiceInput(

View File

@ -0,0 +1,19 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2026 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.utils
import android.view.inputmethod.InputMethodInfo
import android.view.inputmethod.InputMethodSubtype
fun InputMethodInfo.firstVoiceSubtype() : InputMethodSubtype? {
for (index in 0 until subtypeCount) {
val subtype = getSubtypeAt(index)
if (subtype.mode.lowercase() == "voice") {
return subtype
}
}
return null
}

View File

@ -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.utils
@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.view.inputmethod.InputMethodInfo
import android.view.inputmethod.InputMethodSubtype
import org.fcitx.fcitx5.android.BuildConfig
import org.fcitx.fcitx5.android.input.FcitxInputMethodService
@ -45,13 +46,36 @@ object InputMethodUtil {
fun showPicker() = appContext.inputMethodManager.showInputMethodPicker()
fun firstVoiceInput(): Pair<String, InputMethodSubtype>? =
appContext.inputMethodManager
.shortcutInputMethodsAndSubtypes
.firstNotNullOfOrNull {
it.value.find { subType -> subType.mode.lowercase() == "voice" }
?.let { subType -> it.key.id to subType }
fun listVoiceInputMethods(): List<Pair<InputMethodInfo, InputMethodSubtype>> {
return appContext.inputMethodManager.enabledInputMethodList
.mapNotNull { info ->
info.firstVoiceSubtype()?.let { info to it }
}
}
/**
* Find input method with `"voice"` subtype, preferring one with [id]
*/
fun findVoiceSubtype(id: String): Pair<String, InputMethodSubtype>? {
val inputMethods = appContext.inputMethodManager.enabledInputMethodList
if (inputMethods.isEmpty()) return null
var firstId: String? = null
var firstSubtype: InputMethodSubtype? = null
inputMethods.forEach {
val voiceSubtype = it.firstVoiceSubtype() ?: return@forEach
if (it.id == id) {
return id to voiceSubtype
}
if (firstId == null) {
firstId = it.id
firstSubtype = voiceSubtype
}
}
if (firstId != null && firstSubtype != null) {
return firstId to firstSubtype
}
return null
}
fun switchInputMethod(
service: FcitxInputMethodService,

View File

@ -316,4 +316,5 @@
<string name="emoji_skin_tone_type_6">🏿 深肤色 (Type-6)</string>
<string name="hide_unsupported_emojis">隐藏不支持的表情</string>
<string name="key_border_stroke">启用按键边框描边</string>
<string name="preferred_voice_input">首选语音输入</string>
</resources>

View File

@ -315,4 +315,5 @@
<string name="emoji_skin_tone_type_5">🏾 中等深膚色 (Type-5)</string>
<string name="emoji_skin_tone_type_6">🏿 深膚色 (Type-6)</string>
<string name="hide_unsupported_emojis">隱藏不支援的表情</string>
</resources>
<string name="preferred_voice_input">首選語音輸入</string>
</resources>

View File

@ -316,4 +316,5 @@
<string name="emoji_skin_tone_type_6">🏿 Type-6</string>
<string name="hide_unsupported_emojis">Hide unsupported Emojis</string>
<string name="key_border_stroke">Enable stroke for key border</string>
<string name="preferred_voice_input">Preferred voice input</string>
</resources>