diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt index 7d676e860..7c7f51245 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt @@ -198,6 +198,7 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp } } when (primaryCode) { + KeyCode.TOGGLE_LITERAL_MODE -> return latinIME.toggleLiteralMode() KeyCode.TOGGLE_SELECTION_MODE -> { sPersistentSelectionModeActive = !sPersistentSelectionModeActive keyboardSwitcher.mainKeyboardView?.invalidateAllKeys() diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt index 9613135af..0fc749100 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/KeyboardIconsSet.kt @@ -165,6 +165,7 @@ class KeyboardIconsSet private constructor() { ToolbarKey.TOUCHPAD -> R.drawable.ic_touchpad ToolbarKey.TEXT_EDIT -> R.drawable.ic_text_edit ToolbarKey.AUTOCORRECT -> R.drawable.ic_autocorrect + ToolbarKey.LITERAL -> R.drawable.ic_literal ToolbarKey.CLEAR_CLIPBOARD -> R.drawable.ic_bin ToolbarKey.CLOSE_HISTORY -> R.drawable.ic_close ToolbarKey.HANDWRITING -> R.drawable.ic_edit @@ -245,6 +246,7 @@ class KeyboardIconsSet private constructor() { ToolbarKey.TOUCHPAD -> R.drawable.ic_touchpad ToolbarKey.TEXT_EDIT -> R.drawable.ic_text_edit ToolbarKey.AUTOCORRECT -> R.drawable.ic_autocorrect + ToolbarKey.LITERAL -> R.drawable.ic_literal ToolbarKey.CLEAR_CLIPBOARD -> R.drawable.ic_bin ToolbarKey.CLOSE_HISTORY -> R.drawable.ic_close ToolbarKey.HANDWRITING -> R.drawable.ic_edit @@ -325,6 +327,7 @@ class KeyboardIconsSet private constructor() { ToolbarKey.TOUCHPAD -> R.drawable.ic_touchpad_rounded ToolbarKey.TEXT_EDIT -> R.drawable.ic_text_edit ToolbarKey.AUTOCORRECT -> R.drawable.ic_autocorrect_rounded + ToolbarKey.LITERAL -> R.drawable.ic_literal ToolbarKey.CLEAR_CLIPBOARD -> R.drawable.ic_bin ToolbarKey.CLOSE_HISTORY -> R.drawable.ic_close_rounded ToolbarKey.HANDWRITING -> R.drawable.ic_edit diff --git a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt index 056c083a3..8d09604b5 100644 --- a/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt +++ b/app/src/main/java/helium314/keyboard/keyboard/internal/keyboard_parser/floris/KeyCode.kt @@ -205,6 +205,7 @@ object KeyCode { const val CLEAR_HANDWRITING = -10075 const val SWITCH_TO_USER_IME = -10076 const val OCR = -10077 + const val TOGGLE_LITERAL_MODE = -10078 // Intents @@ -230,7 +231,7 @@ object KeyCode { TIMESTAMP, CTRL_LEFT, CTRL_RIGHT, ALT_LEFT, ALT_RIGHT, META_LEFT, META_RIGHT, SEND_INTENT_ONE, SEND_INTENT_TWO, SEND_INTENT_THREE, INLINE_EMOJI_SEARCH_DONE, META_LOCK, PROOFREAD, TRANSLATE, SHOW_TRANSLATE_LANGUAGES, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, CUSTOM_AI_4, CUSTOM_AI_5, - CUSTOM_AI_6, CUSTOM_AI_7, CUSTOM_AI_8, CUSTOM_AI_9, CUSTOM_AI_10, CLIPBOARD_SEARCH, TOGGLE_FLOATING_KEYBOARD, TOGGLE_TOUCHPAD_MODE, TOGGLE_TEXT_EDIT_MODE, TOGGLE_SELECTION_MODE, HANDWRITING, CLEAR_HANDWRITING, OCR, + CUSTOM_AI_6, CUSTOM_AI_7, CUSTOM_AI_8, CUSTOM_AI_9, CUSTOM_AI_10, CLIPBOARD_SEARCH, TOGGLE_FLOATING_KEYBOARD, TOGGLE_TOUCHPAD_MODE, TOGGLE_TEXT_EDIT_MODE, TOGGLE_SELECTION_MODE, HANDWRITING, CLEAR_HANDWRITING, OCR, TOGGLE_LITERAL_MODE, CUSTOM1, CUSTOM2, CUSTOM3, CUSTOM4, CUSTOM5, SWITCH_TO_USER_IME -> this diff --git a/app/src/main/java/helium314/keyboard/latin/LatinIME.kt b/app/src/main/java/helium314/keyboard/latin/LatinIME.kt index c59c55c28..e9f66efd8 100644 --- a/app/src/main/java/helium314/keyboard/latin/LatinIME.kt +++ b/app/src/main/java/helium314/keyboard/latin/LatinIME.kt @@ -184,6 +184,46 @@ class LatinIME : InputMethodService(), private val mTempRect = Rect() private val mFloatingTouchableRect = Rect() @Volatile var isCursorGestureActive = false + @Volatile var isLiteralMode = false + private set + private var literalEditor: EditorInfo? = null + private var literalEditorIdentity: LiteralEditorIdentity? = null + private var literalEditorConnection: android.view.inputmethod.InputConnection? = null + + private data class LiteralEditorIdentity( + val packageName: String?, val fieldId: Int, val fieldName: String?, + val inputType: Int, val imeOptions: Int + ) + + fun toggleLiteralMode() { + if (currentInputEditorInfo == null) return + // Finish the visible composition as typed, without correction, expansion, or learning. + inputLogic.finishInput() + isLiteralMode = !isLiteralMode + handler.cancelPendingSuggestions() + sSettingsDirty = true + loadSettings() + setNeutralSuggestionStrip() + keyboardSwitcher.mainKeyboardView?.apply { + setGestureHandlingEnabledByUser( + settings.current.mGestureInputEnabled, settings.current.mGestureTrailEnabled, + settings.current.mGestureFloatingPreviewTextEnabled + ) + showGestureFloatingPreviewText(SuggestedWords.getEmptyInstance(), true) + } + keyboardSwitcher.requestUpdatingShiftState(currentAutoCapsState, currentRecapitalizeState) + suggestionStripView?.updateToolbarButtonsActivatedState() + } + + private fun resetLiteralMode() { + isLiteralMode = false + literalEditor = null + literalEditorIdentity = null + literalEditorConnection = null + inputLogic.invalidateSuggestions() + handler.cancelPendingSuggestions() + sSettingsDirty = true + } private var voicePluginManager: VoicePluginManager? = null private var voiceInputManager: VoiceInputManager? = null @@ -370,6 +410,7 @@ class LatinIME : InputMethodService(), val localeAndConfidenceInfo: String? get() = dictionaryFacilitator.localesAndConfidences() override fun onDestroy() { + resetLiteralMode() if (sInstance === this) sInstance = null if (isShowingOptionDialog()) { optionsDialog?.dismiss() @@ -641,6 +682,16 @@ class LatinIME : InputMethodService(), fun onStartInputInternal(editorInfo: EditorInfo?, restarting: Boolean) { super.onStartInput(editorInfo, restarting) + val identity = editorInfo?.let { + LiteralEditorIdentity(it.packageName, it.fieldId, it.fieldName, it.inputType, it.imeOptions) + } + val sameEditor = restarting && identity != null && identity == literalEditorIdentity && + (identity.fieldId != 0 || editorInfo === literalEditor || + (currentInputConnection != null && currentInputConnection === literalEditorConnection)) + if (!sameEditor) resetLiteralMode() + literalEditor = editorInfo + literalEditorIdentity = identity + literalEditorConnection = currentInputConnection floatingKeyboardManager?.resetDragAndResizeState() inputLogic.connection.onStartInput() @@ -700,7 +751,7 @@ class LatinIME : InputMethodService(), updateFullscreenMode() - if (isDifferentTextField || !currentSettingsValues.hasSameOrientation(resources.configuration)) { + if (sSettingsDirty || isDifferentTextField || !currentSettingsValues.hasSameOrientation(resources.configuration)) { loadSettings() currentSettingsValues = settings.current suggestionStripView?.updateVoiceKey() @@ -816,6 +867,7 @@ class LatinIME : InputMethodService(), } fun onFinishInputInternal() { + resetLiteralMode() super.onFinishInput() Log.i(TAG, "onFinishInput") floatingKeyboardManager?.resetDragAndResizeState() @@ -903,6 +955,7 @@ class LatinIME : InputMethodService(), } override fun onDisplayCompletions(applicationSpecifiedCompletions: Array?) { + if (isLiteralMode) return if (DebugFlags.DEBUG_ENABLED) { Log.i(TAG, "Received completions:") applicationSpecifiedCompletions?.forEachIndexed { i, info -> Log.i(TAG, " #$i: $info") } @@ -1350,6 +1403,7 @@ class LatinIME : InputMethodService(), private fun hasSuggestionStripView(): Boolean = suggestionStripView != null private fun setSuggestedWords(suggestedWords: SuggestedWords) { + if (isLiteralMode && !suggestedWords.isEmpty) return val currentSettingsValues = settings.current inputLogic.setSuggestedWords(suggestedWords) if (!hasSuggestionStripView()) return @@ -1374,6 +1428,10 @@ class LatinIME : InputMethodService(), } override fun setSuggestions(suggestedWords: SuggestedWords) { + if (isLiteralMode) { + setSuggestedWords(SuggestedWords.getEmptyInstance()) + return + } if (tryShowMathSuggestion()) return if (suggestedWords.isEmpty) { if (keyboardSwitcher.isHandwritingShowing) { @@ -1392,7 +1450,7 @@ class LatinIME : InputMethodService(), } override fun pickSuggestionManually(suggestionInfo: SuggestedWordInfo?) { - if (suggestionInfo == null) return + if (suggestionInfo == null || isLiteralMode) return val completeInputTransaction = inputLogic.onPickSuggestionManually(settings.current, suggestionInfo, keyboardSwitcher.keyboardShiftMode, handler) updateStateAfterInputTransaction(completeInputTransaction) if (keyboardSwitcher.isHandwritingShowing) keyboardSwitcher.clearHandwritingCanvas() @@ -1403,6 +1461,7 @@ class LatinIME : InputMethodService(), } fun tryShowOtpSuggestion(): Boolean { + if (isLiteralMode) return false val strip = suggestionStripView ?: return false val otpView = otpSuggestionManager.getOtpSuggestionView(strip) if (otpView != null) { @@ -1413,6 +1472,7 @@ class LatinIME : InputMethodService(), } fun tryShowMathSuggestion(): Boolean { + if (isLiteralMode) return false val strip = suggestionStripView ?: return false val mathView = mathSuggestionManager.getMathSuggestionView(strip) if (mathView != null) { @@ -1423,6 +1483,7 @@ class LatinIME : InputMethodService(), } fun tryShowClipboardSuggestion(): Boolean { + if (isLiteralMode) return false val strip = suggestionStripView ?: return false val clipboardView = clipboardHistoryManager.getClipboardSuggestionView(currentInputEditorInfo, strip) if (clipboardView != null) { @@ -1440,6 +1501,11 @@ class LatinIME : InputMethodService(), get() = suggestionStripView?.isExternalSuggestionVisible == true override fun setNeutralSuggestionStrip() { + if (isLiteralMode) { + suggestionStripView?.setExternalSuggestionView(null, false) + setSuggestedWords(SuggestedWords.getEmptyInstance()) + return + } if (keyboardSwitcher.isHandwritingShowing) return val currentSettings = settings.current if (tryShowExternalSuggestion()) { @@ -1741,6 +1807,9 @@ class LatinIME : InputMethodService(), override fun handleMessage(msg: Message) { val latinIme = ownerInstance ?: return + if (msg.what in listOf(MSG_UPDATE_SUGGESTION_STRIP, MSG_RESUME_SUGGESTIONS, + MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS, MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED) && + !latinIme.inputLogic.isSuggestionRequestCurrent(msg.arg2)) return when (msg.what) { MSG_UPDATE_SUGGESTION_STRIP -> { cancelUpdateSuggestionStrip() @@ -1776,9 +1845,9 @@ class LatinIME : InputMethodService(), } fun postUpdateSuggestionStrip(inputStyle: Int) { - val latinIme = ownerInstance - if (latinIme != null && latinIme.keyboardSwitcher.isHandwritingShowing) return - sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP, inputStyle, 0), delayInMillisecondsToUpdateSuggestions.toLong()) + val latinIme = ownerInstance ?: return + if (latinIme.isLiteralMode || latinIme.keyboardSwitcher.isHandwritingShowing) return + sendMessageDelayed(obtainMessage(MSG_UPDATE_SUGGESTION_STRIP, inputStyle, latinIme.inputLogic.suggestionGeneration), delayInMillisecondsToUpdateSuggestions.toLong()) } fun postReopenDictionaries() { sendMessage(obtainMessage(MSG_REOPEN_DICTIONARIES)) } @@ -1789,8 +1858,9 @@ class LatinIME : InputMethodService(), if (latinIme.keyboardSwitcher.isHandwritingShowing) return if (!latinIme.settings.current.needsToLookupSuggestions()) return removeMessages(MSG_RESUME_SUGGESTIONS) - if (shouldDelay) sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), delayInMillisecondsToUpdateSuggestions.toLong()) - else sendMessage(obtainMessage(MSG_RESUME_SUGGESTIONS)) + val message = obtainMessage(MSG_RESUME_SUGGESTIONS, 0, latinIme.inputLogic.suggestionGeneration) + if (shouldDelay) sendMessageDelayed(message, delayInMillisecondsToUpdateSuggestions.toLong()) + else sendMessage(message) } fun postResetCaches(tryResumeSuggestions: Boolean, remainingTries: Int) { @@ -1803,6 +1873,12 @@ class LatinIME : InputMethodService(), fun hasPendingWaitForDictionaryLoad(): Boolean = hasMessages(MSG_WAIT_FOR_DICTIONARY_LOAD) fun cancelUpdateSuggestionStrip() { removeMessages(MSG_UPDATE_SUGGESTION_STRIP) } fun cancelResumeSuggestions() { removeMessages(MSG_RESUME_SUGGESTIONS) } + fun cancelPendingSuggestions() { + cancelUpdateSuggestionStrip() + cancelResumeSuggestions() + removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS) + removeMessages(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED) + } fun hasPendingUpdateSuggestions(): Boolean = hasMessages(MSG_UPDATE_SUGGESTION_STRIP) fun hasPendingResumeSuggestions(): Boolean = hasMessages(MSG_RESUME_SUGGESTIONS) fun hasPendingReopenDictionaries(): Boolean = hasMessages(MSG_REOPEN_DICTIONARIES) @@ -1820,19 +1896,25 @@ class LatinIME : InputMethodService(), for (i in 0..MSG_LAST) removeMessages(i) } - fun showGesturePreviewAndSetSuggestions(suggestedWords: SuggestedWords, dismissGestureFloatingPreviewText: Boolean) { + fun showGesturePreviewAndSetSuggestions( + suggestedWords: SuggestedWords, dismissGestureFloatingPreviewText: Boolean, + generation: Int = ownerInstance?.inputLogic?.suggestionGeneration ?: 0 + ) { + if (ownerInstance?.inputLogic?.isSuggestionRequestCurrent(generation) != true) return removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS) val arg1 = if (dismissGestureFloatingPreviewText) ARG1_DISMISS_GESTURE_FLOATING_PREVIEW_TEXT else ARG1_SHOW_GESTURE_FLOATING_PREVIEW_TEXT - obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS, arg1, ARG2_UNUSED, suggestedWords).sendToTarget() + obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS, arg1, generation, suggestedWords).sendToTarget() } - fun setSuggestions(suggestedWords: SuggestedWords) { + fun setSuggestions(suggestedWords: SuggestedWords, generation: Int = ownerInstance?.inputLogic?.suggestionGeneration ?: 0) { + if (ownerInstance?.inputLogic?.isSuggestionRequestCurrent(generation) != true) return removeMessages(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS) - obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS, ARG1_NOT_GESTURE_INPUT, ARG2_UNUSED, suggestedWords).sendToTarget() + obtainMessage(MSG_SHOW_GESTURE_PREVIEW_AND_SET_SUGGESTIONS, ARG1_NOT_GESTURE_INPUT, generation, suggestedWords).sendToTarget() } - fun showTailBatchInputResult(suggestedWords: SuggestedWords) { - obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, suggestedWords).sendToTarget() + fun showTailBatchInputResult(suggestedWords: SuggestedWords, generation: Int) { + if (ownerInstance?.inputLogic?.isSuggestionRequestCurrent(generation) != true) return + obtainMessage(MSG_UPDATE_TAIL_BATCH_INPUT_COMPLETED, 0, generation, suggestedWords).sendToTarget() } fun postSwitchLanguage(subtype: InputMethodSubtype) { diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt index cbaabc8d7..fb5045326 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt @@ -115,6 +115,13 @@ class InputLogic( val suggestedWords: SuggestedWords get() = mSuggestedWords + val suggestionGeneration: Int get() = mInputLogicHandler.generation + + fun isSuggestionRequestCurrent(generation: Int): Boolean = + generation == suggestionGeneration && !mLatinIME.isLiteralMode + + fun invalidateSuggestions() = mInputLogicHandler.reset() + fun startInput(combiningSpec: String?, settingsValues: SettingsValues) { mEnteredText = null mWordBeingCorrectedByCursor = null @@ -150,13 +157,14 @@ class InputLogic( } fun finishInput() { + invalidateSuggestions() if (mWordComposer.isComposingWord()) { mConnection.finishComposingText() StatsUtils.onWordCommitUserTyped(mWordComposer.getTypedWord(), mWordComposer.isBatchMode()) } resetComposingState(true) - mInputLogicHandler.reset() mSpaceState = SpaceState.NONE + cancelDoubleSpacePeriodCountdown() mConnection.ensureBatchEditClosed() } @@ -251,7 +259,7 @@ class InputLogic( resetComposingState(true /* alsoResetLastComposedWord */) } handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING) - val text = performSpecificTldProcessingOnTextInput(rawText) + val text = if (settingsValues.mLiteralMode) rawText else performSpecificTldProcessingOnTextInput(rawText) if (SpaceState.PHANTOM == mSpaceState) { insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues) } @@ -429,6 +437,7 @@ class InputLogic( keyboardSwitcher: KeyboardSwitcher, handler: LatinIME.UIHandler ) { + if (mLatinIME.isLiteralMode) return mWordBeingCorrectedByCursor = null mInputLogicHandler.onStartBatchInput() handler.showGesturePreviewAndSetSuggestions(SuggestedWords.getEmptyBatchInstance(), false) @@ -488,6 +497,7 @@ class InputLogic( } fun setSuggestedWords(suggestedWords: SuggestedWords) { + if (mLatinIME.isLiteralMode && !suggestedWords.isEmpty) return if (!suggestedWords.isEmpty) { val suggestedWordInfo = if (suggestedWords.mWillAutoCorrect) { suggestedWords.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION) @@ -1037,7 +1047,7 @@ class InputLogic( if (!isComposingWord && settingsValues.isWordCodePoint(codePoint) - && settingsValues.needsToLookupSuggestions() + && settingsValues.shouldComposeInput() && !settingsValues.isDirectCommitApp && (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces || !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, !mConnection.hasSlowInputConnection()) @@ -1057,7 +1067,7 @@ class InputLogic( var didSetComposingText = false var didExpand = false var shouldDeferSegmentation = false - if (TextExpanderUtils.isEnabled(mLatinIME) + if (!mLatinIME.isLiteralMode && TextExpanderUtils.isEnabled(mLatinIME) && TextExpanderUtils.isImmediateEnabled(mLatinIME) ) { val typedWord = mWordComposer.getTypedWord() @@ -1106,7 +1116,7 @@ class InputLogic( inputTransaction.setRequiresUpdateSuggestions() } } - if (TextExpanderUtils.isEnabled(mLatinIME) + if (!mLatinIME.isLiteralMode && TextExpanderUtils.isEnabled(mLatinIME) && TextExpanderUtils.isImmediateEnabled(mLatinIME) ) { val textBefore = mConnection.getTextBeforeCursor(50, 0) @@ -1225,7 +1235,7 @@ class InputLogic( } else { commitTyped(settingsValues, StringUtils.newSingleCodePointString(codePoint)) } - } else if (TextExpanderUtils.isEnabled(mLatinIME)) { + } else if (!mLatinIME.isLiteralMode && TextExpanderUtils.isEnabled(mLatinIME)) { val textBefore = mConnection.getTextBeforeCursor(50, 0) if (textBefore != null) { val result = TextExpanderUtils.getExpandedWordForTyped(null, textBefore.toString(), mLatinIME) @@ -1409,7 +1419,7 @@ class InputLogic( if (mWordComposer.isComposingWord()) { val typedWord = mWordComposer.getTypedWord() setComposingTextInternal(getTextWithUnderline(typedWord), 1) - if (TextExpanderUtils.isEnabled(mLatinIME) + if (!mLatinIME.isLiteralMode && TextExpanderUtils.isEnabled(mLatinIME) && TextExpanderUtils.isImmediateEnabled(mLatinIME) ) { val textBefore = mConnection.getTextBeforeCursor(50, 0) @@ -1603,6 +1613,7 @@ class InputLogic( } internal fun unlearnWord(word: String, settingsValues: SettingsValues, eventType: Int) { + if (mLatinIME.isLiteralMode) return val ngramContext = mConnection.getNgramContextFromNthPreviousWord(settingsValues.mSpacingAndPunctuations, 2) val timeStampInSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) mDictionaryFacilitator.unlearnFromUserHistory(word, ngramContext, timeStampInSeconds, eventType) @@ -1631,6 +1642,7 @@ class InputLogic( event: Event, inputTransaction: InputTransaction ): Boolean { + if (mLatinIME.isLiteralMode) return false val codePoint = event.codePoint val isFromSuggestionStrip = event.isSuggestionStripPress if (Constants.CODE_ENTER == codePoint @@ -1745,6 +1757,7 @@ class InputLogic( suggestion: String, ngramContext: NgramContext ) { + if (mLatinIME.isLiteralMode) return if (!settingsValues.isSuggestionsEnabledPerUserSettings() || TextUtils.isEmpty(suggestion)) { return } @@ -1935,12 +1948,13 @@ class InputLogic( ) } if (suggestions.size <= 1) { + val generation = suggestionGeneration mInputLogicHandler.getSuggestedWords { getSuggestedWords( SuggestedWords.INPUT_STYLE_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER ) { words -> - if (words != null) doShowSuggestionsAndClearAutoCorrectionIndicator(words) + if (words != null) doShowSuggestionsAndClearAutoCorrectionIndicator(words, generation) } } } else { @@ -1952,9 +1966,12 @@ class InputLogic( } } - private fun doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords: SuggestedWords) { + private fun doShowSuggestionsAndClearAutoCorrectionIndicator( + suggestedWords: SuggestedWords, generation: Int = suggestionGeneration + ) { + if (!isSuggestionRequestCurrent(generation)) return mIsAutoCorrectionIndicatorOn = false - mLatinIME.mHandler.setSuggestions(suggestedWords) + mLatinIME.mHandler.setSuggestions(suggestedWords, generation) } private fun revertCommit(inputTransaction: InputTransaction) { @@ -2039,7 +2056,7 @@ class InputLogic( get() = mWordComposer.isComposingWord() fun getCurrentAutoCapsState(settingsValues: SettingsValues): Int { - if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF + if (mLatinIME.isLiteralMode || !settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF val ei = getCurrentInputEditorInfo() ?: return Constants.TextUtils.CAP_MODE_OFF var inputType = ei.inputType @@ -2217,6 +2234,7 @@ class InputLogic( suggestedWords: SuggestedWords, keyboardSwitcher: KeyboardSwitcher ) { + if (mLatinIME.isLiteralMode) return val batchInputText = if (suggestedWords.isEmpty) null else suggestedWords.getWord(0) if (batchInputText.isNullOrEmpty()) { return @@ -2260,6 +2278,10 @@ class InputLogic( separator: String, handler: LatinIME.UIHandler ) { + if (settingsValues.mLiteralMode) { + commitTyped(settingsValues, separator) + return + } if (handler.hasPendingUpdateSuggestions()) { handler.cancelUpdateSuggestionStrip() performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING) @@ -2300,7 +2322,7 @@ class InputLogic( startTimeMillis = System.currentTimeMillis() Log.d(TAG, "commitChosenWord() : [$chosenWord]") } - val isEnabled = TextExpanderUtils.isEnabled(mLatinIME) + val isEnabled = !mLatinIME.isLiteralMode && TextExpanderUtils.isEnabled(mLatinIME) if (isEnabled) { val textBefore = mConnection.getTextBeforeCursor(50, 0) if (textBefore != null) { @@ -2399,6 +2421,8 @@ class InputLogic( sequenceNumber: Int, callback: Suggest.OnGetSuggestedWordsCallback ) { + val generation = suggestionGeneration + if (!isSuggestionRequestCurrent(generation)) return val keyboard = KeyboardSwitcher.getInstance().keyboard if (keyboard == null) { callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance()) @@ -2427,9 +2451,10 @@ class InputLogic( settingsValues.mAutoCorrectEnabled, inputStyle, sequenceNumber ) - callback.onGetSuggestedWords(suggestedWords) + if (isSuggestionRequestCurrent(generation)) callback.onGetSuggestedWords(suggestedWords) } catch (e: Exception) { Log.e(TAG, "Error fetching suggested words, using empty words instead", e) + if (!isSuggestionRequestCurrent(generation)) return callback.onGetSuggestedWords(SuggestedWords.getEmptyInstance()) KeyboardSwitcher.getInstance().showToast("Error getting suggestions", true) } diff --git a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.kt b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.kt index 56bb81ab0..d046735e5 100644 --- a/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.kt +++ b/app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogicHandler.kt @@ -24,6 +24,8 @@ class InputLogicHandler( private val nonUIThreadHandler: Handler private val lock = Any() private var inBatchInput = false // synchronized using lock. + @Volatile var generation = 0 + private set init { val handlerThread = HandlerThread(InputLogicHandler::class.java.simpleName) @@ -32,7 +34,11 @@ class InputLogicHandler( } fun reset() { - nonUIThreadHandler.removeCallbacksAndMessages(null) + synchronized(lock) { + generation++ + inBatchInput = false + nonUIThreadHandler.removeCallbacksAndMessages(null) + } } /** @@ -83,14 +89,15 @@ class InputLogicHandler( return } inputLogic.wordComposer.setBatchInputPointers(batchPointers) + val requestGeneration = generation getSuggestedWords { inputLogic.getSuggestedWords( if (isTailBatchInput) SuggestedWords.INPUT_STYLE_TAIL_BATCH else SuggestedWords.INPUT_STYLE_UPDATE_BATCH, sequenceNumber, object : Suggest.OnGetSuggestedWordsCallback { override fun onGetSuggestedWords(suggestedWords: SuggestedWords?) { - if (suggestedWords != null) { - showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput) + if (suggestedWords != null && inputLogic.isSuggestionRequestCurrent(requestGeneration)) { + showGestureSuggestionsWithPreviewVisuals(suggestedWords, isTailBatchInput, requestGeneration) } } } @@ -101,7 +108,8 @@ class InputLogicHandler( private fun showGestureSuggestionsWithPreviewVisuals( suggestedWordsForBatchInput: SuggestedWords, - isTailBatchInput: Boolean + isTailBatchInput: Boolean, + requestGeneration: Int ) { // We're now inside the callback. This always runs on the Non-UI thread, // no matter what thread updateBatchInput was originally called on. @@ -114,12 +122,15 @@ class InputLogicHandler( } else { suggestedWordsForBatchInput } - latinIMEHandler.showGesturePreviewAndSetSuggestions(suggestedWordsToShowSuggestions, isTailBatchInput) + latinIMEHandler.showGesturePreviewAndSetSuggestions(suggestedWordsToShowSuggestions, isTailBatchInput, requestGeneration) if (isTailBatchInput) { - inBatchInput = false + synchronized(lock) { + if (!inputLogic.isSuggestionRequestCurrent(requestGeneration)) return + inBatchInput = false + } // The following call schedules onEndBatchInputInternal // to be called on the UI thread. - latinIMEHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions) + latinIMEHandler.showTailBatchInputResult(suggestedWordsToShowSuggestions, requestGeneration) } } @@ -173,7 +184,11 @@ class InputLogicHandler( } fun getSuggestedWords(callback: Runnable) { - nonUIThreadHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, callback).sendToTarget() + val requestGeneration = generation + val guardedCallback = Runnable { + if (inputLogic.isSuggestionRequestCurrent(requestGeneration)) callback.run() + } + nonUIThreadHandler.obtainMessage(MSG_GET_SUGGESTED_WORDS, guardedCallback).sendToTarget() } companion object { diff --git a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt index 25ef8accb..ab7f82bbc 100644 --- a/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt +++ b/app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt @@ -21,6 +21,7 @@ import helium314.keyboard.keyboard.KeyboardActionListener import helium314.keyboard.keyboard.KeyboardTheme import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL import helium314.keyboard.latin.InputAttributes +import helium314.keyboard.latin.LatinIME import helium314.keyboard.latin.R import helium314.keyboard.latin.RichInputMethodManager import helium314.keyboard.latin.common.Colors @@ -48,6 +49,7 @@ open class SettingsValues( val mInputAttributes: InputAttributes, currentKeyboardScript: String ) { + val mLiteralMode = LatinIME.getInstance()?.isLiteralMode == true // From resources: val mSpacingAndPunctuations: SpacingAndPunctuations val mDoubleSpacePeriodTimeout: Long @@ -248,7 +250,7 @@ open class SettingsValues( mToolbarKeysAlignment = prefs.getString(Settings.PREF_TOOLBAR_KEYS_ALIGNMENT, fallbackAlign) ?: fallbackAlign mClipboardKeysAlignment = mToolbarKeysAlignment mShowDownloadButtonInToolbar = prefs.getBoolean(Settings.PREF_SHOW_DOWNLOAD_BUTTON_IN_TOOLBAR, Defaults.PREF_SHOW_DOWNLOAD_BUTTON_IN_TOOLBAR) - mAutoCap = prefs.getBoolean(Settings.PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP) && ScriptUtils.scriptSupportsUppercase(mLocale) + mAutoCap = !mLiteralMode && prefs.getBoolean(Settings.PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP) && ScriptUtils.scriptSupportsUppercase(mLocale) mVibrateOn = Settings.readVibrationEnabled(prefs) mVibrateInDndMode = prefs.getBoolean(Settings.PREF_VIBRATE_IN_DND_MODE, Defaults.PREF_VIBRATE_IN_DND_MODE) mSoundOn = prefs.getBoolean(Settings.PREF_SOUND_ON, Defaults.PREF_SOUND_ON) @@ -295,12 +297,12 @@ open class SettingsValues( mEnableClipboardListener = prefs.getBoolean(Settings.PREF_ENABLE_CLIPBOARD_LISTENER, Defaults.PREF_ENABLE_CLIPBOARD_LISTENER) mEnableSmsOtpReceiver = prefs.getBoolean(Settings.PREF_ENABLE_SMS_OTP_RECEIVER, Defaults.PREF_ENABLE_SMS_OTP_RECEIVER) mEnableAppSyncListener = prefs.getBoolean(Settings.PREF_ENABLE_APP_SYNC_LISTENER, Defaults.PREF_ENABLE_APP_SYNC_LISTENER) - mUseDoubleSpacePeriod = prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, Defaults.PREF_KEY_USE_DOUBLE_SPACE_PERIOD) && mInputAttributes.mIsGeneralTextInput + mUseDoubleSpacePeriod = !mLiteralMode && prefs.getBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, Defaults.PREF_KEY_USE_DOUBLE_SPACE_PERIOD) && mInputAttributes.mIsGeneralTextInput mBlockPotentiallyOffensive = prefs.getBoolean(Settings.PREF_BLOCK_POTENTIALLY_OFFENSIVE, Defaults.PREF_BLOCK_POTENTIALLY_OFFENSIVE) mUrlDetectionEnabled = prefs.getBoolean(Settings.PREF_URL_DETECTION, Defaults.PREF_URL_DETECTION) val globalAutoCorrect = prefs.getBoolean(Settings.PREF_AUTO_CORRECTION, Defaults.PREF_AUTO_CORRECTION) val appAutoCorrection = AppQuirksManager.getAutoCorrectionOverride(mInputAttributes.mTargetApplicationPackageName) - mAutoCorrectionEnabledPerUserSettings = when (appAutoCorrection) { + mAutoCorrectionEnabledPerUserSettings = !mLiteralMode && when (appAutoCorrection) { AppQuirksManager.AUTOCORRECT_FORCE_ENABLE -> true AppQuirksManager.AUTOCORRECT_FORCE_DISABLE -> false else -> globalAutoCorrect @@ -323,7 +325,7 @@ open class SettingsValues( mPersistTextEditMode = prefs.getBoolean(Settings.PREF_PERSIST_TEXT_EDIT_MODE, Defaults.PREF_PERSIST_TEXT_EDIT_MODE) mBackspaceRevertsAutocorrect = prefs.getBoolean(Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT, Defaults.PREF_BACKSPACE_REVERTS_AUTOCORRECT) mDisableMultiWordSuggestions = prefs.getBoolean(Settings.PREF_DISABLE_MULTI_WORD_SUGGESTIONS, Defaults.PREF_DISABLE_MULTI_WORD_SUGGESTIONS) - mBigramPredictionEnabled = prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, Defaults.PREF_BIGRAM_PREDICTIONS) + mBigramPredictionEnabled = !mLiteralMode && prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, Defaults.PREF_BIGRAM_PREDICTIONS) mPrioritizePersonalSuggestions = prefs.getBoolean(Settings.PREF_PRIORITIZE_PERSONAL_SUGGESTIONS, Defaults.PREF_PRIORITIZE_PERSONAL_SUGGESTIONS) mSuggestionBalance = prefs.getInt(Settings.PREF_SUGGESTION_BALANCE, Defaults.PREF_SUGGESTION_BALANCE) @@ -335,7 +337,7 @@ open class SettingsValues( } mNextWordBoostLevel = boostLevel mNextWordStrictNgram = prefs.getBoolean(Settings.PREF_NEXT_WORD_STRICT_NGRAM, Defaults.PREF_NEXT_WORD_STRICT_NGRAM) - mFirstWordPredictionEnabled = prefs.getBoolean(Settings.PREF_FIRST_WORD_PREDICTIONS, Defaults.PREF_FIRST_WORD_PREDICTIONS) + mFirstWordPredictionEnabled = !mLiteralMode && prefs.getBoolean(Settings.PREF_FIRST_WORD_PREDICTIONS, Defaults.PREF_FIRST_WORD_PREDICTIONS) mSuggestPunctuation = prefs.getBoolean(Settings.PREF_SUGGEST_PUNCTUATION, Defaults.PREF_SUGGEST_PUNCTUATION) mSuggestClipboardContent = prefs.getBoolean(Settings.PREF_SUGGEST_CLIPBOARD_CONTENT, Defaults.PREF_SUGGEST_CLIPBOARD_CONTENT) mSuggestScreenshots = prefs.getBoolean(Settings.PREF_SUGGEST_SCREENSHOTS, Defaults.PREF_SUGGEST_SCREENSHOTS) @@ -373,7 +375,7 @@ open class SettingsValues( mSoundVolModifiers = prefs.getFloat(Settings.PREF_SOUND_VOL_MODIFIERS, Defaults.PREF_SOUND_VOL_MODIFIERS) mEnableEmojiAltPhysicalKey = prefs.getBoolean(Settings.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY, Defaults.PREF_ENABLE_EMOJI_ALT_PHYSICAL_KEY) mGestureMethod = prefs.getString(Settings.PREF_GESTURE_METHOD, "fallback") ?: "fallback" - mGestureInputEnabled = JniUtils.sHaveGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) + mGestureInputEnabled = !mLiteralMode && JniUtils.sHaveGestureLib && prefs.getBoolean(Settings.PREF_GESTURE_INPUT, Defaults.PREF_GESTURE_INPUT) mGestureTrailEnabled = prefs.getBoolean(Settings.PREF_GESTURE_PREVIEW_TRAIL, Defaults.PREF_GESTURE_PREVIEW_TRAIL) mGestureFloatingPreviewTextEnabled = !mInputAttributes.mDisableGestureFloatingPreviewText && prefs.getBoolean(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, Defaults.PREF_GESTURE_FLOATING_PREVIEW_TEXT) mGestureFloatingPreviewDynamicEnabled = Settings.readGestureDynamicPreviewEnabled(prefs) @@ -386,7 +388,7 @@ open class SettingsValues( mOverrideShowingSuggestions = mInputAttributes.mMayOverrideShowingSuggestions && (prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS) || (moreAutoCorrection && !isUriOrEmail)) && ((mInputAttributes.mInputType and InputType.TYPE_MASK_VARIATION) != InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT || !prefs.getBoolean(Settings.PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT, Defaults.PREF_ALWAYS_SHOW_SUGGESTIONS_EXCEPT_WEB_TEXT)) val suggestionsEnabled = prefs.getBoolean(Settings.PREF_SHOW_SUGGESTIONS, Defaults.PREF_SHOW_SUGGESTIONS) - mSuggestionsEnabledPerUserSettings = suggestionsEnabled && (mInputAttributes.mShouldShowSuggestions || mOverrideShowingSuggestions) && !mSuggestionStripHiddenPerUserSettings + mSuggestionsEnabledPerUserSettings = !mLiteralMode && suggestionsEnabled && (mInputAttributes.mShouldShowSuggestions || mOverrideShowingSuggestions) && !mSuggestionStripHiddenPerUserSettings mSecondaryStripVisible = mToolbarMode != ToolbarMode.HIDDEN || !mToolbarHidingGlobal mIncognitoModeEnabled = if (AppQuirksManager.isNonIncognitoApp(mInputAttributes.mTargetApplicationPackageName)) { mInputAttributes.mIsPasswordField @@ -400,14 +402,14 @@ open class SettingsValues( mLanguageSwipeDistance = prefs.getInt(Settings.PREF_LANGUAGE_SWIPE_DISTANCE, Defaults.PREF_LANGUAGE_SWIPE_DISTANCE) mTouchpadSensitivity = prefs.getInt(Settings.PREF_TOUCHPAD_SENSITIVITY, Defaults.PREF_TOUCHPAD_SENSITIVITY) mTouchpadFullscreen = prefs.getBoolean(Settings.PREF_TOUCHPAD_FULLSCREEN, Defaults.PREF_TOUCHPAD_FULLSCREEN) - mForceAutoCaps = prefs.getBoolean(Settings.PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS) + mForceAutoCaps = !mLiteralMode && prefs.getBoolean(Settings.PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS) mDeleteSwipeEnabled = prefs.getBoolean(Settings.PREF_DELETE_SWIPE, Defaults.PREF_DELETE_SWIPE) - mAutospaceAfterPunctuation = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION) - mAutospaceAfterEmoji = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_EMOJI, Defaults.PREF_AUTOSPACE_AFTER_EMOJI) - mAutospaceAfterSuggestion = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, Defaults.PREF_AUTOSPACE_AFTER_SUGGESTION) - mImmediateAutoSpace = prefs.getBoolean(Settings.PREF_IMMEDIATE_AUTO_SPACE, Defaults.PREF_IMMEDIATE_AUTO_SPACE) - mAutospaceAfterGestureTyping = prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_GESTURE_TYPING, Defaults.PREF_AUTOSPACE_AFTER_GESTURE_TYPING) - mAutospaceBeforeGestureTyping = prefs.getBoolean(Settings.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING, Defaults.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING) + mAutospaceAfterPunctuation = !mLiteralMode && prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, Defaults.PREF_AUTOSPACE_AFTER_PUNCTUATION) + mAutospaceAfterEmoji = !mLiteralMode && prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_EMOJI, Defaults.PREF_AUTOSPACE_AFTER_EMOJI) + mAutospaceAfterSuggestion = !mLiteralMode && prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_SUGGESTION, Defaults.PREF_AUTOSPACE_AFTER_SUGGESTION) + mImmediateAutoSpace = !mLiteralMode && prefs.getBoolean(Settings.PREF_IMMEDIATE_AUTO_SPACE, Defaults.PREF_IMMEDIATE_AUTO_SPACE) + mAutospaceAfterGestureTyping = !mLiteralMode && prefs.getBoolean(Settings.PREF_AUTOSPACE_AFTER_GESTURE_TYPING, Defaults.PREF_AUTOSPACE_AFTER_GESTURE_TYPING) + mAutospaceBeforeGestureTyping = !mLiteralMode && prefs.getBoolean(Settings.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING, Defaults.PREF_AUTOSPACE_BEFORE_GESTURE_TYPING) mShiftRemovesAutospace = prefs.getBoolean(Settings.PREF_SHIFT_REMOVES_AUTOSPACE, Defaults.PREF_SHIFT_REMOVES_AUTOSPACE) mPreserveSpaceBeforePunctuation = prefs.getBoolean(Settings.PREF_PRESERVE_SPACE_BEFORE_PUNCTUATION, Defaults.PREF_PRESERVE_SPACE_BEFORE_PUNCTUATION) mClipboardHistoryEnabled = prefs.getBoolean(Settings.PREF_ENABLE_CLIPBOARD_HISTORY, Defaults.PREF_ENABLE_CLIPBOARD_HISTORY) @@ -468,13 +470,15 @@ open class SettingsValues( } fun isApplicationSpecifiedCompletionsOn(): Boolean { - return mInputAttributes.mApplicationSpecifiedCompletionOn + return !mLiteralMode && mInputAttributes.mApplicationSpecifiedCompletionOn } fun needsToLookupSuggestions(): Boolean { - return (mInputAttributes.mShouldShowSuggestions || mOverrideShowingSuggestions || isSymbolComposingApp) && (mAutoCorrectEnabled || mSuggestionsEnabledPerUserSettings || isSymbolComposingApp) + return !mLiteralMode && (mInputAttributes.mShouldShowSuggestions || mOverrideShowingSuggestions || isSymbolComposingApp) && (mAutoCorrectEnabled || mSuggestionsEnabledPerUserSettings || isSymbolComposingApp) } + fun shouldComposeInput(): Boolean = mLiteralMode || needsToLookupSuggestions() + fun isSuggestionsEnabledPerUserSettings(): Boolean { return mSuggestionsEnabledPerUserSettings } @@ -503,7 +507,7 @@ open class SettingsValues( val isAutoSpaceDisabled: Boolean = AppQuirksManager.isAutoSpaceDisabled(mInputAttributes.mTargetApplicationPackageName) fun shouldInsertSpacesAutomatically(): Boolean { - return mInputAttributes.mShouldInsertSpacesAutomatically && !isAutoSpaceDisabled + return !mLiteralMode && mInputAttributes.mShouldInsertSpacesAutomatically && !isAutoSpaceDisabled } fun isLanguageSwitchKeyEnabled(): Boolean { diff --git a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt index 451e78caf..14c0606d5 100644 --- a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt +++ b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt @@ -18,6 +18,7 @@ import helium314.keyboard.keyboard.internal.KeyboardIconsSet import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode import helium314.keyboard.latin.BuildConfig import helium314.keyboard.latin.R +import helium314.keyboard.latin.LatinIME import helium314.keyboard.latin.common.Constants.Separators import helium314.keyboard.latin.common.ColorType import helium314.keyboard.latin.settings.Defaults @@ -202,12 +203,13 @@ fun setToolbarButtonActivatedState(button: ImageButton) { ONE_HANDED -> Settings.getValues().mOneHandedModeEnabled SPLIT -> Settings.getValues().mIsSplitKeyboardEnabled AUTOCORRECT -> Settings.getValues().mAutoCorrectionEnabledPerUserSettings + LITERAL -> LatinIME.getInstance()?.isLiteralMode == true SELECT_MODE -> helium314.keyboard.keyboard.KeyboardActionListenerImpl.sPersistentSelectionModeActive else -> true } button.isActivated = activated val colors = Settings.getValues().mColors - if (activated && button.tag in listOf(INCOGNITO, ONE_HANDED, SPLIT, AUTOCORRECT, SELECT_MODE)) { + if (activated && button.tag in listOf(INCOGNITO, ONE_HANDED, SPLIT, AUTOCORRECT, LITERAL, SELECT_MODE)) { colors.setColor(button.background, ColorType.TOOL_BAR_KEY_ENABLED_BACKGROUND) if (button.drawable != null) { button.clearColorFilter() @@ -243,6 +245,7 @@ fun getCodeForToolbarKey(key: ToolbarKey) = Settings.getInstance().getCustomTool TOUCHPAD -> KeyCode.TOGGLE_TOUCHPAD_MODE TEXT_EDIT -> KeyCode.TOGGLE_TEXT_EDIT_MODE AUTOCORRECT -> KeyCode.TOGGLE_AUTOCORRECT + LITERAL -> KeyCode.TOGGLE_LITERAL_MODE CLEAR_CLIPBOARD -> KeyCode.CLIPBOARD_CLEAR_HISTORY CLOSE_HISTORY -> KeyCode.ALPHA EMOJI -> KeyCode.EMOJI @@ -299,7 +302,7 @@ fun getCodeForToolbarKeyLongClick(key: ToolbarKey) = Settings.getInstance().getC // names need to be aligned with resources strings (using lowercase of key.name) enum class ToolbarKey { VOICE, CLIPBOARD, CLIPBOARD_SEARCH, NUMPAD, HANDWRITING, UNDO, REDO, SETTINGS, SELECT_ALL, SELECT_WORD, COPY, CUT, PASTE, ONE_HANDED, SPLIT, FLOATING, - INCOGNITO, TOUCHPAD, TEXT_EDIT, AUTOCORRECT, CLEAR_CLIPBOARD, CLOSE_HISTORY, EMOJI, LEFT, RIGHT, UP, DOWN, WORD_LEFT, WORD_RIGHT, + INCOGNITO, TOUCHPAD, TEXT_EDIT, AUTOCORRECT, LITERAL, CLEAR_CLIPBOARD, CLOSE_HISTORY, EMOJI, LEFT, RIGHT, UP, DOWN, WORD_LEFT, WORD_RIGHT, PAGE_UP, PAGE_DOWN, FULL_LEFT, FULL_RIGHT, PAGE_START, PAGE_END, PROOFREAD, TRANSLATE, OCR, SELECT_MODE, CUSTOM_AI_1, CUSTOM_AI_2, CUSTOM_AI_3, CUSTOM_AI_4, CUSTOM_AI_5, CUSTOM_AI_6, CUSTOM_AI_7, CUSTOM_AI_8, CUSTOM_AI_9, CUSTOM_AI_10 diff --git a/app/src/main/res/drawable/ic_literal.xml b/app/src/main/res/drawable/ic_literal.xml new file mode 100644 index 000000000..36af1d827 --- /dev/null +++ b/app/src/main/res/drawable/ic_literal.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6071907c..f2575dcc5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -149,6 +149,7 @@ Do not suggest potentially offensive words Auto-correction + Literal typing Spacebar and punctuation automatically correct mistyped words Auto-correct on diff --git a/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt b/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt index 5af7ad7a4..c77682a99 100644 --- a/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt +++ b/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt @@ -407,6 +407,23 @@ f""", // no newline at the end assertEquals(KeyCode.UNDO, key.toKeyParams(params).mPopupKeys?.first()?.mCode) } + @Test fun literalToolbarKey() { + assertIsExpected("""[[{ "label": "literal" }]]""", + Expected(KeyCode.TOGGLE_LITERAL_MODE, icon = "literal")) + assertIsExpected("""[[{ "code": -10078, "label": "Raw" }]]""", + Expected(KeyCode.TOGGLE_LITERAL_MODE, label = "Raw")) + } + + @Test fun literalPopupToolbarKey() { + val key = LayoutParser.parseJsonString("""[[{ "label": "x", "popup": { + "main": { "label": "literal" } + } }]]""").map { row -> row.mapNotNull { it.compute(params) } }.flatten().single() + val popup = key.toKeyParams(params).mPopupKeys?.first() + assertEquals(null, popup?.mLabel) + assertEquals("literal", popup?.mIconName) + assertEquals(KeyCode.TOGGLE_LITERAL_MODE, popup?.mCode) + } + @Test fun popupKeyWithIconAndImplicitText() { val key = LayoutParser.parseJsonString("""[[{ "label": "a", "popup": { "relevant": [ { "label": "!icon/go_key|aa" } diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt index d9b5b2324..b91e5cdf1 100644 --- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt +++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt @@ -13,6 +13,9 @@ import org.junit.Ignore import helium314.keyboard.ShadowInputMethodManager2 import helium314.keyboard.ShadowLocaleManagerCompat import helium314.keyboard.event.Event +import helium314.keyboard.compat.AppQuirk +import helium314.keyboard.compat.AppQuirksManager +import helium314.keyboard.keyboard.Keyboard import helium314.keyboard.keyboard.KeyboardSwitcher import helium314.keyboard.keyboard.MainKeyboardView import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode @@ -26,10 +29,15 @@ import helium314.keyboard.latin.common.LocaleUtils.constructLocale import helium314.keyboard.latin.common.StringUtils import helium314.keyboard.latin.dictionary.Dictionary import helium314.keyboard.latin.inputlogic.InputLogic +import helium314.keyboard.latin.inputlogic.InputLogicHandler import helium314.keyboard.latin.inputlogic.SpaceState import helium314.keyboard.latin.settings.Settings import helium314.keyboard.latin.utils.ScriptUtils import helium314.keyboard.latin.utils.SubtypeSettings +import helium314.keyboard.latin.utils.TextExpanderUtils +import helium314.keyboard.latin.utils.ToolbarKey +import helium314.keyboard.latin.utils.createToolbarKey +import helium314.keyboard.latin.suggestions.SuggestionStripView import helium314.keyboard.latin.utils.getTimestampFormatter import helium314.keyboard.latin.utils.prefs import org.junit.runner.RunWith @@ -89,6 +97,283 @@ class InputLogicTest { assertEquals("", composingText) } + @Test fun literalActionPreservesTypedCompositionAndRestoresPreferences() { + reset() + setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) + typeNoAssert("cps") + composer.setAutoCorrection( + SuggestedWordInfo("CPP", "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0) + ) + val prefsBefore = latinIME.prefs().all.toMap() + val cursorBefore = cursor + toggleLiteral() + assertEquals("cps", text) + assertEquals(cursorBefore, cursor) + assertEquals("", composingText) + assertEquals(false, settingsValues.mAutoCorrectEnabled) + assertEquals(false, settingsValues.mAutoCap) + assertEquals(false, settingsValues.isSuggestionsEnabledPerUserSettings()) + assertEquals(false, settingsValues.mGestureInputEnabled) + assertEquals(prefsBefore, latinIME.prefs().all) + toggleLiteral() + assertEquals("cps", text) + assertEquals(cursorBefore, cursor) + assertEquals(true, settingsValues.mAutoCorrectEnabled) + assertEquals(prefsBefore, latinIME.prefs().all) + } + + @Test fun literalTypingKeepsCaseSpacesPunctuationAndDoesNotExpandOrLearn() { + configureExpansion("cpp", true) + setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE) + editorInfoOverride = EditorInfo().apply { + inputType = currentInputType + imeOptions = EditorInfo.IME_ACTION_NONE + } + latinIME.prefs().edit { + putBoolean(Settings.PREF_AUTO_CAP, true) + putBoolean(Settings.PREF_FORCE_AUTO_CAPS, true) + putBoolean(Settings.PREF_KEY_USE_DOUBLE_SPACE_PERIOD, true) + putBoolean(Settings.PREF_AUTOSPACE_AFTER_PUNCTUATION, true) + } + toggleLiteral() + val literal = "cpp https://Host/x_Y?ID=ABC C:\\Src\\a.py .\n" + typeNoAssert(literal) + assertEquals(literal, text) + assertEquals(emptyList(), addedWords) + checkConnectionConsistency() + } + + private fun toggleLiteral() { + latinIME.keyboardActionListener.onCodeInput(KeyCode.TOGGLE_LITERAL_MODE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false) + handleMessages() + checkConnectionConsistency() + } + + private fun configureExpansion(shortcut: String, immediate: Boolean) { + reset() + latinIME.prefs().edit { + putBoolean(TextExpanderUtils.PREF_ENABLED, true) + putBoolean(TextExpanderUtils.PREF_IMMEDIATE, immediate) + putBoolean(TextExpanderUtils.PREF_BACKSPACE_REVERTS, true) + } + TextExpanderUtils.saveShortcuts( + latinIME, mapOf(shortcut to TextExpanderUtils.ShortcutEntry("EXPANDED")) + ) + } + + @Test fun literalModePreservesHangulCompositionAndExplicitEditing() { + reset() + currentScript = ScriptUtils.SCRIPT_HANGUL + latinIME.switchToSubtype(SubtypeSettings.getResourceSubtypesForLocale("ko".constructLocale()).first()) + toggleLiteral() + typeNoAssert("ㅎㅏㄴ") + assertEquals("한", java.text.Normalizer.normalize(text, java.text.Normalizer.Form.NFC)) + assertEquals(true, composer.isComposingWord()) + functionalKeyPress(KeyCode.DELETE) + assertEquals("하", java.text.Normalizer.normalize(text, java.text.Normalizer.Form.NFC)) + latinIME.onTextInput("PASTED") + handleMessages() + assertEquals("하PASTED", java.text.Normalizer.normalize(text, java.text.Normalizer.Form.NFC)) + assertEquals(emptyList(), addedWords) + } + + @Test fun literalModeRejectsCompletionsManualStalePicksAndGlideResults() { + reset() + typeNoAssert("raw") + toggleLiteral() + val word = SuggestedWordInfo("REPLACED", "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0) + val words = SuggestedWords(arrayListOf(word), null, word, true, true, false, 0, 0) + latinIME.onDisplayCompletions(arrayOf(CompletionInfo(1, 0, "REPLACED"))) + latinIME.setSuggestions(words) + latinIME.pickSuggestionManually(word) + inputLogic.setSuggestedWords(words) + inputLogic.onStartBatchInput(settingsValues, latinIME.keyboardSwitcher, latinIME.handler) + inputLogic.onUpdateTailBatchInputCompleted(settingsValues, words, latinIME.keyboardSwitcher) + assertEquals("raw", text) + assertEquals(true, inputLogic.suggestedWords.isEmpty) + assertEquals(SpaceState.NONE, spaceState) + } + + @Test fun literalModeSurvivesHideAndSameFieldRestartButNotNewEditorOrFinish() { + reset() + fun editor(id: Int = 42, input: Int = currentInputType) = EditorInfo().apply { + packageName = "test.literal.editor" + fieldId = id + inputType = input + } + fun start(info: EditorInfo, restarting: Boolean) { + editorInfoOverride = info + latinIME.onStartInputInternal(info, restarting) + latinIME.onStartInputViewInternal(info, restarting) + } + start(editor(), false) + toggleLiteral() + latinIME.onFinishInputViewInternal(false) + latinIME.onStartInputViewInternal(requireNotNull(editorInfoOverride), true) + assertEquals(true, latinIME.isLiteralMode) + start(editor(), true) + assertEquals(true, latinIME.isLiteralMode) + start(editor(43), true) + assertEquals(false, latinIME.isLiteralMode) + toggleLiteral() + start(editor(43, InputType.TYPE_CLASS_NUMBER), true) + assertEquals(false, latinIME.isLiteralMode) + start(editor(0), false) + toggleLiteral() + start(editor(0), true) // Same connection is positive evidence for a zero-ID restart. + assertEquals(true, latinIME.isLiteralMode) + inputConnectionOverride = InputConnectionWrapper(ic, false) + start(editor(0), true) + assertEquals(false, latinIME.isLiteralMode) + toggleLiteral() + start(editor(0), false) + assertEquals(false, latinIME.isLiteralMode) + toggleLiteral() + latinIME.onFinishInputInternal() + assertEquals(false, latinIME.isLiteralMode) + start(editor(), false) + toggleLiteral() + latinIME.onDestroy() + assertEquals(false, latinIME.isLiteralMode) + } + + @Test fun literalModeOverridesForcedAppCorrectionAcrossSettingsReloadWithoutChangingPrefs() { + reset() + val packageName = "test.literal.forced" + editorInfoOverride = EditorInfo().apply { + inputType = currentInputType + this.packageName = packageName + } + AppQuirksManager.saveQuirk(AppQuirk(packageName, autoCorrectionMode = AppQuirksManager.AUTOCORRECT_FORCE_ENABLE)) + try { + latinIME.onStartInputInternal(editorInfoOverride, false) + latinIME.onStartInputViewInternal(requireNotNull(editorInfoOverride), false) + val personalized = settingsValues.mUsePersonalizedDicts + val prefsBefore = latinIME.prefs().all.toMap() + assertEquals(true, settingsValues.mAutoCorrectEnabled) + toggleLiteral() + latinIME.settings.loadSettings(latinIME, Locale.US, settingsValues.mInputAttributes, currentScript) + assertEquals(true, latinIME.isLiteralMode) + assertEquals(false, settingsValues.mAutoCorrectEnabled) + assertEquals(personalized, settingsValues.mUsePersonalizedDicts) + assertEquals(prefsBefore, latinIME.prefs().all) + toggleLiteral() + assertEquals(true, settingsValues.mAutoCorrectEnabled) + assertEquals(prefsBefore, latinIME.prefs().all) + } finally { + AppQuirksManager.removeQuirk(packageName) + } + } + + @Test fun literalModeInvalidatesQueuedWorkerAndUiTailAcrossOnOff() { + reset() + val worker = InputLogic::class.java.getDeclaredField("mInputLogicHandler").apply { isAccessible = true } + .get(inputLogic) as InputLogicHandler + var workerCalls = 0 + worker.getSuggestedWords { workerCalls++ } + val queued = messages.last().obj as Runnable + val word = SuggestedWordInfo("STALE", "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0) + val words = SuggestedWords(arrayListOf(word), null, word, true, true, false, 0, 0) + latinIME.handler.setSuggestions(words) + val queuedSuggestions = Message.obtain(messages.last()) + latinIME.handler.showTailBatchInputResult(words, inputLogic.suggestionGeneration) + val queuedTail = Message.obtain(messages.last()) + toggleLiteral() + toggleLiteral() + queued.run() + latinIME.handler.handleMessage(queuedSuggestions) + latinIME.handler.handleMessage(queuedTail) + assertEquals(0, workerCalls) + assertEquals("", text) + assertEquals(true, inputLogic.suggestedWords.isEmpty) + queuedSuggestions.recycle() + queuedTail.recycle() + } + + @Test fun literalModeDiscardsRunningSuggestionResultEvenAfterTurningOff() { + reset() + latinIME.prefs().edit { + putBoolean(Settings.PREF_FIRST_WORD_PREDICTIONS, false) + putBoolean(Settings.PREF_BIGRAM_PREDICTIONS, false) + } + val view = Mockito.mock(MainKeyboardView::class.java) + Mockito.`when`(view.keyboard).thenReturn(Mockito.mock(Keyboard::class.java, Mockito.RETURNS_DEEP_STUBS)) + val viewField = KeyboardSwitcher::class.java.getDeclaredField("mKeyboardView").apply { isAccessible = true } + viewField.set(latinIME.keyboardSwitcher, view) + var computations = 0 + val suggest = Mockito.mock(Suggest::class.java) { invocation -> + if (invocation.method.name == "getSuggestedWords") { + computations++ + if (computations == 1) { + toggleLiteral() + toggleLiteral() + } + SuggestedWords.getEmptyInstance() + } else Mockito.RETURNS_DEFAULTS.answer(invocation) + } + InputLogic::class.java.getDeclaredField("mSuggest").apply { isAccessible = true }.set(inputLogic, suggest) + var delivered = 0 + try { + inputLogic.getSuggestedWords(SuggestedWords.INPUT_STYLE_TYPING, 1) { delivered++ } + assertEquals(1, computations) + assertEquals(0, delivered) + assertEquals(false, latinIME.isLiteralMode) + } finally { + viewField.set(latinIME.keyboardSwitcher, null) + } + } + + @Test fun literalModeKeepsExplicitCaseEmojiAndTextKeysWithoutAutomaticSpacingOrTldRewriting() { + reset() + toggleLiteral() + assertEquals(Constants.TextUtils.CAP_MODE_OFF, latinIME.inputLogic.getCurrentAutoCapsState(Settings.getInstance().current)) + latinIME.keyboardActionListener.onCodeInput('A'.code, 0, 0, false) + latinIME.keyboardActionListener.onCodeInput('b'.code, 0, 0, false) + latinIME.onTextInput(".") + latinIME.onTextInput(".com") + latinIME.onTextInput("🙂") + latinIME.keyboardActionListener.onCodeInput('C'.code, 0, 0, false) + handleMessages() + assertEquals("Ab..com🙂C", text) + assertEquals(SpaceState.NONE, spaceState) + assertEquals(emptyList(), addedWords) + checkConnectionConsistency() + } + + @Test fun literalToolbarAndPinnedButtonsShareDispatchAndActivation() { + reset() + val strip = SuggestionStripView(latinIME, null) + val keyboardView = MainKeyboardView(latinIME, null).apply { id = R.id.keyboard_view } + strip.setListener(latinIME, keyboardView) + LatinIME::class.java.getDeclaredField("suggestionStripView").apply { isAccessible = true }.set(latinIME, strip) + val toolbar = strip.findViewById(R.id.toolbar) + val pinned = strip.findViewById(R.id.pinned_keys) + toolbar.removeAllViews() + pinned.removeAllViews() + val mainButton = createToolbarKey(latinIME, ToolbarKey.LITERAL) + val pinnedButton = createToolbarKey(latinIME, ToolbarKey.LITERAL) + val correctionButton = createToolbarKey(latinIME, ToolbarKey.AUTOCORRECT) + toolbar.addView(mainButton) + toolbar.addView(correctionButton) + pinned.addView(pinnedButton) + mainButton.setOnClickListener(strip) + pinnedButton.setOnClickListener(strip) + val prefsBefore = latinIME.prefs().all.toMap() + assertEquals("Literal typing", mainButton.contentDescription) + assertEquals(false, mainButton.isActivated) + mainButton.performClick() + assertEquals(true, latinIME.isLiteralMode) + assertEquals(true, mainButton.isActivated) + assertEquals(true, pinnedButton.isActivated) + assertEquals(false, correctionButton.isActivated) + assertEquals(true, createToolbarKey(latinIME, ToolbarKey.LITERAL).isActivated) + pinnedButton.performClick() + assertEquals(false, mainButton.isActivated) + assertEquals(false, pinnedButton.isActivated) + assertEquals(true, correctionButton.isActivated) + assertEquals(prefsBefore, latinIME.prefs().all) + } + @Test fun `english space-separated typing keeps composing word`() { reset() chainInput("hello") @@ -1260,6 +1545,8 @@ class InputLogicTest { // should be called before every test, so the same state is guaranteed private fun reset() { + editorInfoOverride = null + inputConnectionOverride = null // reset input connection & facilitator currentScript = ScriptUtils.SCRIPT_LATIN text = "" @@ -1703,18 +1990,21 @@ private val ic = object : InputConnection { // could also extend LatinIME, it's not final anyway @Implements(InputMethodService::class) -class ShadowInputMethodService { +class ShadowInputMethodService : org.robolectric.shadows.ShadowService() { @Implementation - fun getCurrentInputEditorInfo() = EditorInfo().apply { + fun getCurrentInputEditorInfo() = editorInfoOverride ?: EditorInfo().apply { inputType = currentInputType // anything else? } @Implementation - fun getCurrentInputConnection() = ic + fun getCurrentInputConnection(): InputConnection = inputConnectionOverride ?: ic @Implementation fun isInputViewShown() = true // otherwise selection updates will do nothing } +private var editorInfoOverride: EditorInfo? = null +private var inputConnectionOverride: InputConnection? = null + @Implements(Handler::class) class ShadowHandler { @Implementation diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 797ce075f..6bbceaa9d 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -347,6 +347,16 @@ LeanType features an advanced **Native IME Floating Window** architecture that r Split your toolbar and suggestion strip into two independent rows for fast, unhindered access to both word predictions and quick actions. - Configure via **Settings → Appearance → Split toolbar & suggestions**. +### Optional Literal typing + +For URLs, code, or other text that should stay as typed, add **Literal typing** in **Settings → Toolbar → Toolbar keys** (or **Pinned toolbar keys**, where available). It is off by default and is not automatically added to your enabled toolbar actions. + +Tap the action to turn it on for the current field; tap again to restore normal typing. The highlighted toolbar and pinned buttons share the same state. A toggle finishes any pending composition as typed, without applying a correction or moving the cursor. The mode resets when input finishes or a new field starts; hiding the keyboard and identifiable same-field restarts preserve it. An ambiguous restart without a stable field identity resets it rather than carrying it to another field. + +While active, Literal typing suppresses automatic capitalization (including Force auto-caps), auto-correction (including per-app force-enable), word predictions, application completions, automatic spacing, double-space periods, text expansion, word-glide typing, and dictionary learning. Manual Shift/caps, script composition such as Hangul, explicit editing, paste, emoji insertion, and the editor's Enter action remain available. Normal typing preferences and learned data are not changed or deleted. + +Custom JSON layouts can use `{"label":"literal"}` or `{"code":-10078,"label":"Raw"}`. This is an explicit per-field mode, not automatic URL/code detection or a fix for all capitalization or contraction behavior. + --- ## 12. Versatile Text Expander & Modifiers @@ -716,6 +726,7 @@ LeanType provides an extensible keycode architecture that allows customizing key | **`-10075`** | `CLEAR_HANDWRITING` | Clears all current drawing strokes from the handwriting canvas. | | **`-10077`** | `OCR` | Opens the live camera viewfinder and screenshot OCR scanner. | | **`-10076`** | `SWITCH_TO_USER_IME` | Instantly switches to the user's configured target IME keyboard. | +| **`-10078`** | `TOGGLE_LITERAL_MODE` | Toggles optional Literal typing for the current field; JSON keyword `literal`. | --- @@ -829,4 +840,3 @@ LeanType provides an extensible keycode architecture that allows customizing key | **`-10008`** | `NOT_SPECIFIED` | Structural dummy spacer key (disables key interaction, renders blank). | -