From 8cdd708c3f55b3e58138184f63cf8c9c233f0c7f Mon Sep 17 00:00:00 2001 From: igugyj Date: Thu, 4 Jun 2026 18:44:23 +0800 Subject: [PATCH] feat: Calculator candidates --- .../fcitx5/android/data/prefs/AppPrefs.kt | 6 + .../fcitx/fcitx5/android/input/InputView.kt | 89 ++++++++++++ .../input/calculator/CalculatorEngine.kt | 130 ++++++++++++++++++ .../input/calculator/ExpressionEvaluator.kt | 104 ++++++++++++++ .../HorizontalCandidateComponent.kt | 30 +++- .../input/keyboard/CommonKeyActionListener.kt | 16 ++- .../android/input/keyboard/KeyboardWindow.kt | 3 + app/src/main/res/values-de/strings.xml | 2 + app/src/main/res/values-es/strings.xml | 2 + app/src/main/res/values-ja/strings.xml | 2 + app/src/main/res/values-ko/strings.xml | 2 + app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values-zh-rTW/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 15 files changed, 386 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/calculator/CalculatorEngine.kt create mode 100644 app/src/main/java/org/fcitx/fcitx5/android/input/calculator/ExpressionEvaluator.kt diff --git a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt index 5ec27647..5f27d34c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/data/prefs/AppPrefs.kt @@ -144,6 +144,12 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) { val showVoiceInputButton = switch(R.string.show_voice_input_button, "show_voice_input_button", false) + val calculatorCandidates = switch( + R.string.calculator_candidates, + "calculator_candidates", + true, + R.string.calculator_candidates_summary + ) val preferredVoiceInput = voiceInputPreference( R.string.preferred_voice_input, "preferred_voice_input", "" ) { showVoiceInputButton.getValue() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt index 2f2d0641..fa326081 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/InputView.kt @@ -17,17 +17,21 @@ import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.core.view.updateLayoutParams import org.fcitx.fcitx5.android.R +import org.fcitx.fcitx5.android.core.CandidateWord import org.fcitx.fcitx5.android.core.CapabilityFlags import org.fcitx.fcitx5.android.core.FcitxEvent import org.fcitx.fcitx5.android.daemon.FcitxConnection import org.fcitx.fcitx5.android.daemon.launchOnReady import org.fcitx.fcitx5.android.data.prefs.AppPrefs +import org.fcitx.fcitx5.android.data.prefs.ManagedPreference import org.fcitx.fcitx5.android.data.prefs.ManagedPreferenceProvider import org.fcitx.fcitx5.android.data.theme.Theme import org.fcitx.fcitx5.android.data.theme.ThemeManager import org.fcitx.fcitx5.android.input.bar.KawaiiBarComponent import org.fcitx.fcitx5.android.input.broadcast.InputBroadcaster +import org.fcitx.fcitx5.android.input.calculator.CalculatorEngine import org.fcitx.fcitx5.android.input.broadcast.PreeditEmptyStateComponent +import timber.log.Timber import org.fcitx.fcitx5.android.input.broadcast.PunctuationComponent import org.fcitx.fcitx5.android.input.broadcast.ReturnKeyDrawableComponent import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateComponent @@ -105,6 +109,7 @@ class InputView( private val kawaiiBar = KawaiiBarComponent() private val horizontalCandidate = HorizontalCandidateComponent() private val keyboardWindow = KeyboardWindow() + private val calculatorEngine = CalculatorEngine() private val symbolPicker = symbolPicker() private val emojiPicker = emojiPicker() private val emoticonPicker = emoticonPicker() @@ -131,6 +136,21 @@ class InputView( private val keyboardPrefs = AppPrefs.getInstance().keyboard private val focusChangeResetKeyboard by keyboardPrefs.focusChangeResetKeyboard + private var calculatorCandidates by keyboardPrefs.calculatorCandidates + + @Keep + private val onCalculatorCandidatesChangeListener = + ManagedPreference.OnChangeListener { _, value -> + if (!value && calculatorEngine.hasExpression) { + calculatorEngine.reset() + horizontalCandidate.calculatorMode = false + horizontalCandidate.adapter.updateCandidates(emptyArray(), 0) + broadcaster.onPreeditEmptyStateUpdate(true) + broadcaster.onCandidateUpdate( + FcitxEvent.CandidateListEvent.Data(0, emptyArray()) + ) + } + } private val keyboardHeightPercent = keyboardPrefs.keyboardHeightPercent private val keyboardHeightPercentLandscape = keyboardPrefs.keyboardHeightPercentLandscape @@ -188,6 +208,62 @@ class InputView( // MUST call before any operation setupScope() + commonKeyActionListener.onSymAction = fun(sym: Int): Boolean { + if (!calculatorCandidates) return false + Timber.d("Calculator: onSymAction called sym=0x%04x, isNumberKeyboard=%s, hasExpression=%s", + sym, keyboardWindow.isNumberKeyboardActive, calculatorEngine.hasExpression) + if (!keyboardWindow.isNumberKeyboardActive) { + if (calculatorEngine.hasExpression) { + Timber.d("Calculator: resetting stale expression") + calculatorEngine.reset() + } + return false + } + val canHandle = calculatorEngine.canHandleSym(sym) + val result = canHandle && calculatorEngine.handleSym(sym) + Timber.d("Calculator: canHandle=%s, handleSym result=%s, expression=%s", + canHandle, result, calculatorEngine.currentExpression) + return result + } + + calculatorEngine.onCandidatesChanged = { candidates, expression -> + Timber.d("Calculator: onCandidatesChanged candidates=%d expression=%s mode=%s", + candidates.size, expression, horizontalCandidate.calculatorMode) + if (expression.isNotEmpty()) { + service.currentInputConnection?.setComposingText(expression, 1) + } else { + service.currentInputConnection?.finishComposingText() + } + if (candidates.isNotEmpty()) { + horizontalCandidate.calculatorMode = true + horizontalCandidate.onCalculatorCandidateClick = { text -> + Timber.d("Calculator: candidate clicked text=%s", text) + service.currentInputConnection?.commitText(text, 1) + calculatorEngine.reset() + } + horizontalCandidate.adapter.updateCandidates(candidates.toTypedArray(), candidates.size) + } else { + if (horizontalCandidate.calculatorMode) { + horizontalCandidate.calculatorMode = false + horizontalCandidate.adapter.updateCandidates(emptyArray(), 0) + } + } + // trigger bar state machine transition to show/hide candidate view + broadcaster.onPreeditEmptyStateUpdate(expression.isEmpty()) + broadcaster.onCandidateUpdate( + FcitxEvent.CandidateListEvent.Data(candidates.size, candidates.toTypedArray()) + ) + } + + calculatorEngine.onCommitResult = { result -> + Timber.d("Calculator: onCommitResult result=%s", result) + service.currentInputConnection?.commitText(result, 1) + horizontalCandidate.calculatorMode = false + horizontalCandidate.adapter.updateCandidates(emptyArray(), 0) + broadcaster.onPreeditEmptyStateUpdate(true) + broadcaster.onCandidateUpdate(FcitxEvent.CandidateListEvent.Data(0, emptyArray())) + } + // restore punctuation mapping in case of InputView recreation fcitx.launchOnReady { punctuation.updatePunctuationMapping(it.statusAreaActionsCached) @@ -258,6 +334,7 @@ class InputView( }) keyboardPrefs.registerOnChangeListener(onKeyboardSizeChangeListener) + keyboardPrefs.calculatorCandidates.registerOnChangeListener(onCalculatorCandidatesChangeListener) } private fun updateKeyboardSize() { @@ -309,6 +386,11 @@ class InputView( * called when [InputView] is about to show, or restart */ fun startInput(info: EditorInfo, capFlags: CapabilityFlags, restarting: Boolean = false) { + Timber.d("Calculator: startInput inputType=0x%08x", info.inputType) + calculatorEngine.reset() + horizontalCandidate.calculatorMode = false + broadcaster.onPreeditEmptyStateUpdate(true) + broadcaster.onCandidateUpdate(FcitxEvent.CandidateListEvent.Data(0, emptyArray())) broadcaster.onStartInput(info, capFlags) returnKeyDrawable.updateDrawableOnEditorInfo(info) if (focusChangeResetKeyboard || !restarting) { @@ -330,6 +412,12 @@ class InputView( } override fun handleFcitxEvent(it: FcitxEvent<*>) { + if (calculatorEngine.hasExpression && !keyboardWindow.isNumberKeyboardActive) { + Timber.d("Calculator: stale expression reset on fcitx event") + calculatorEngine.reset() + broadcaster.onPreeditEmptyStateUpdate(true) + broadcaster.onCandidateUpdate(FcitxEvent.CandidateListEvent.Data(0, emptyArray())) + } when (it) { is FcitxEvent.CandidateListEvent -> { broadcaster.onCandidateUpdate(it.data) @@ -364,6 +452,7 @@ class InputView( override fun onDetachedFromWindow() { keyboardPrefs.unregisterOnChangeListener(onKeyboardSizeChangeListener) + keyboardPrefs.calculatorCandidates.unregisterOnChangeListener(onCalculatorCandidatesChangeListener) // clear DynamicScope, implies that InputView should not be attached again after detached. scope.clear() super.onDetachedFromWindow() diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/calculator/CalculatorEngine.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/calculator/CalculatorEngine.kt new file mode 100644 index 00000000..5a360873 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/calculator/CalculatorEngine.kt @@ -0,0 +1,130 @@ +package org.fcitx.fcitx5.android.input.calculator + +import org.fcitx.fcitx5.android.core.CandidateWord +import timber.log.Timber + +class CalculatorEngine { + + companion object { + const val KP_0 = 0xffb0 + const val KP_1 = 0xffb1 + const val KP_2 = 0xffb2 + const val KP_3 = 0xffb3 + const val KP_4 = 0xffb4 + const val KP_5 = 0xffb5 + const val KP_6 = 0xffb6 + const val KP_7 = 0xffb7 + const val KP_8 = 0xffb8 + const val KP_9 = 0xffb9 + const val KP_Add = 0xffab + const val KP_Subtract = 0xffad + const val KP_Multiply = 0xffaa + const val KP_Divide = 0xffaf + const val KP_Decimal = 0xffae + const val KP_Equal = 0xffbd + const val Key_BackSpace = 0xff08 + + val calculatorSyms = setOf( + KP_0, KP_1, KP_2, KP_3, KP_4, KP_5, KP_6, KP_7, KP_8, KP_9, + KP_Add, KP_Subtract, KP_Multiply, KP_Divide, KP_Decimal, KP_Equal, + Key_BackSpace + ) + } + + private val expression = StringBuilder() + + var onCandidatesChanged: ((List, String) -> Unit)? = null + var onCommitResult: ((String) -> Unit)? = null + + val hasExpression: Boolean get() = expression.isNotEmpty() + val currentExpression: String get() = expression.toString() + + fun canHandleSym(sym: Int): Boolean = sym in calculatorSyms + + fun handleSym(sym: Int): Boolean { + if (sym !in calculatorSyms) return false + Timber.d("Calculator: handleSym sym=0x%04x expr=\"%s\"", sym, expression) + when (sym) { + KP_0, KP_1, KP_2, KP_3, KP_4, KP_5, KP_6, KP_7, KP_8, KP_9 -> { + val digit = (sym - KP_0).toString()[0] + expression.append(digit) + update() + } + KP_Add -> { + appendOperator('+') + } + KP_Subtract -> { + appendOperator('-') + } + KP_Multiply -> { + appendOperator('*') + } + KP_Divide -> { + appendOperator('/') + } + KP_Decimal -> { + if (expression.isEmpty() || expression.last() !in "0123456789.") { + expression.append("0") + } + val lastNumStart = expression.lastIndexOfAny( + charArrayOf('+', '-', '*', '/') + ) + 1 + val lastNum = expression.substring(lastNumStart) + if ('.' !in lastNum) { + expression.append('.') + } + update() + } + KP_Equal -> { + reset() + return false + } + Key_BackSpace -> { + if (expression.isNotEmpty()) { + expression.deleteCharAt(expression.length - 1) + update() + return true + } + return false + } + } + return true + } + + private fun appendOperator(op: Char) { + if (expression.isEmpty()) return + val last = expression.last() + if (last in "+-*/") { + expression.setCharAt(expression.length - 1, op) + } else { + expression.append(op) + } + update() + } + + private fun update() { + val expr = expression.toString() + val candidates = mutableListOf() + val result = ExpressionEvaluator.evaluate(expr) + Timber.d("Calculator: update expr=\"%s\" evaluateResult=%s", expr, result) + if (result != null) { + val resultStr = formatResult(result) + candidates.add(CandidateWord("", resultStr, "")) + candidates.add(CandidateWord("", "${expr}=${resultStr}", "")) + } + onCandidatesChanged?.invoke(candidates, expr) + } + + private fun formatResult(value: Double): String { + return if (value == value.toLong().toDouble()) { + value.toLong().toString() + } else { + value.toString() + } + } + + fun reset() { + expression.clear() + onCandidatesChanged?.invoke(emptyList(), "") + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/calculator/ExpressionEvaluator.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/calculator/ExpressionEvaluator.kt new file mode 100644 index 00000000..d9882da6 --- /dev/null +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/calculator/ExpressionEvaluator.kt @@ -0,0 +1,104 @@ +package org.fcitx.fcitx5.android.input.calculator + +object ExpressionEvaluator { + + fun evaluate(expression: String): Double? { + if (expression.isBlank()) return null + val tokens = tokenize(expression) ?: return null + if (tokens.isEmpty()) return null + return try { + val parser = Parser(tokens) + val result = parser.parseExpression() + if (parser.pos != tokens.size) null else result + } catch (_: Exception) { + null + } + } + + private sealed class Token { + data class Number(val value: Double) : Token() + data class Op(val op: Char) : Token() + } + + private fun tokenize(expr: String): List? { + val tokens = mutableListOf() + var i = 0 + while (i < expr.length) { + val c = expr[i] + when { + c.isWhitespace() -> i++ + c in "0123456789." -> { + val start = i + var dotCount = 0 + while (i < expr.length && (expr[i].isDigit() || (expr[i] == '.' && dotCount++ == 0))) i++ + val numStr = expr.substring(start, i) + if (numStr == "." || numStr.endsWith(".") || numStr.count { it == '.' } > 1) return null + val value = numStr.toDoubleOrNull() ?: return null + tokens.add(Token.Number(value)) + } + c in "+-*/" -> { + tokens.add(Token.Op(c)) + i++ + } + else -> return null + } + } + return tokens + } + + private class Parser(private val tokens: List) { + var pos = 0 + + fun parseExpression(): Double { + var left = parseTerm() + while (pos < tokens.size) { + when (val token = tokens[pos]) { + is Token.Op -> when (token.op) { + '+', '-' -> { + pos++ + val right = parseTerm() + left = if (token.op == '+') left + right else left - right + } + else -> break + } + else -> break + } + } + return left + } + + private fun parseTerm(): Double { + var left = parseFactor() + while (pos < tokens.size) { + when (val token = tokens[pos]) { + is Token.Op -> when (token.op) { + '*', '/' -> { + pos++ + val right = parseFactor() + left = if (token.op == '*') { + left * right + } else { + if (right == 0.0) throw ArithmeticException("Division by zero") + left / right + } + } + else -> break + } + else -> break + } + } + return left + } + + private fun parseFactor(): Double { + val token = tokens.getOrNull(pos) ?: throw IllegalArgumentException("Unexpected end") + return when (token) { + is Token.Number -> { + pos++ + token.value + } + else -> throw IllegalArgumentException("Unexpected token: $token") + } + } + } +} diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt index 6121a824..02fcf113 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/candidates/horizontal/HorizontalCandidateComponent.kt @@ -84,6 +84,16 @@ class HorizontalCandidateComponent : ) } + var calculatorMode: Boolean = false + set(value) { + field = value + if (!value) { + onCalculatorCandidateClick = null + } + } + + var onCalculatorCandidateClick: ((String) -> Unit)? = null + val adapter: HorizontalCandidateViewAdapter by lazy { object : HorizontalCandidateViewAdapter(theme) { override fun onBindViewHolder(holder: CandidateViewHolder, position: Int) { @@ -92,12 +102,19 @@ class HorizontalCandidateComponent : minWidth = layoutMinWidth flexGrow = layoutFlexGrow } - holder.itemView.setOnClickListener { - fcitx.launchOnReady { it.select(holder.idx) } - } - holder.itemView.setOnLongClickListener { - inputView.showCandidateActionMenu(holder.idx, holder.candidate.text, holder.ui.root) - true + if (calculatorMode) { + holder.itemView.setOnClickListener { + onCalculatorCandidateClick?.invoke(holder.candidate.text) + } + holder.itemView.setOnLongClickListener(null) + } else { + holder.itemView.setOnClickListener { + fcitx.launchOnReady { it.select(holder.idx) } + } + holder.itemView.setOnLongClickListener { + inputView.showCandidateActionMenu(holder.idx, holder.candidate.text, holder.ui.root) + true + } } } @@ -167,6 +184,7 @@ class HorizontalCandidateComponent : } override fun onCandidateUpdate(data: FcitxEvent.CandidateListEvent.Data) { + if (calculatorMode) return val candidates = data.candidates val total = data.total val maxSpanCount = maxSpanCountPref.getValue() diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt index 3847b97f..6d115893 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/CommonKeyActionListener.kt @@ -64,6 +64,12 @@ class CommonKeyActionListener : private var backspaceSwipeState = Stopped + /** + * Called when a [SymAction] is received before forwarding to fcitx. + * Return true to consume the action (prevents forwarding). + */ + var onSymAction: ((sym: Int) -> Boolean)? = null + // there should be a new fcitx API for this private suspend fun FcitxAPI.commitAndReset() { if (inputMethodEntryCached.languageCode.startsWith("zh")) { @@ -93,8 +99,14 @@ class CommonKeyActionListener : is FcitxKeyAction -> service.postFcitxJob { sendKey(action.act, action.states.states, action.code) } - is SymAction -> service.postFcitxJob { - sendKey(action.sym, action.states) + is SymAction -> { + if (onSymAction?.invoke(action.sym.sym) == true) { + // consumed by calculator engine, do nothing + } else { + service.postFcitxJob { + sendKey(action.sym, action.states) + } + } } is CommitAction -> service.postFcitxJob { commitAndReset() diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt index 26dcded0..78fa291b 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/keyboard/KeyboardWindow.kt @@ -76,6 +76,9 @@ class KeyboardWindow : InputWindow.SimpleInputWindow(), Essentia private val currentKeyboard: BaseKeyboard? get() = keyboards[currentKeyboardName] + val isNumberKeyboardActive: Boolean + get() = currentKeyboardName == NumberKeyboard.Name + private val keyActionListener = KeyActionListener { it, source -> if (it is KeyAction.LayoutSwitchAction) { switchLayout(it.act) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index c225eeb5..bc7fff23 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -154,6 +154,8 @@ %1$d Element(e) gelöscht Plugins Taste Spracheingabe zeigen + Taschenrechner-Kandidaten + Ergebnisse als Kandidaten auf der Nummerntastatur anzeigen Fcitx Daemon %1$d Element(e) entfernt Wollen Sie diese Einstellung auf den Standard zurücksetzen? diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 39b18a1f..1a469c8e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -110,6 +110,8 @@ programa principal Editar tema Mostrar botón de entrada por voz + Candidatos de calculadora + Mostrar resultados de cálculos como candidatos en el teclado numérico No hay ningún complemento disponible ¿Quiere restablecer la preferencia a su valor predeterminado? Activado diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c591c279..05eaafbf 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -220,6 +220,8 @@ メインプログラム テーマを編集 音声入力ボタンを表示する + 計算機の候補 + 数字キーボードで式を入力すると計算結果が候補として表示されます Fcitxデーモン Fcitxを再起動中 プラグインの変更を除去しました。ここをクリックして再読み込みします diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ca797e48..b51f9c95 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -91,4 +91,6 @@ 구문 구성 새로고침 입력기 옵션 + 계산기 후보 + 숫자 키보드에서 표현식을 입력할 때 계산 결과를 후보로 표시 diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 98e6c364..0010be26 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -225,6 +225,8 @@ основная программа Изменить тему Показывать кнопку голосового ввода + Кандидаты калькулятора + Показывать результаты вычислений в виде кандидатов на цифровой клавиатуре Демон Fcitx Перезапуск Fcitx Обнаружены изменения плагина, нажмите здесь, чтобы перезагрузить diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d2c3c348..89303365 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -225,6 +225,8 @@ 主程序 编辑主题 显示语音输入按钮 + 计算器候选词 + 在数字键盘上输入表达式时显示计算结果候选词 Fcitx 守护程序 正在重新启动 Fcitx 检测到插件更改,点此重新加载 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fa9bc4e7..75dfc3da 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -225,6 +225,8 @@ 主程式 編輯主題 顯示語音輸入鍵 + 計算機候選詞 + 在數字鍵盤上輸入表達式時顯示計算結果候選詞 Fcitx 常駐程式 重新啟動 Fcitx 偵測到有外掛程式變化,點此重新載入 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d19fe469..678accd3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,6 +225,8 @@ main program Edit Theme Show voice input button + Calculator candidates + Show calculation results as candidates on the number keyboard Fcitx Daemon Restarting Fcitx Plugin changes detected, click here to reload