diff --git a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
index 7d53c6707..1edd12c45 100644
--- a/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
+++ b/app/src/main/java/helium314/keyboard/keyboard/KeyboardActionListenerImpl.kt
@@ -237,6 +237,16 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
return
}
KeyCode.TOGGLE_AUTOCORRECT -> return settings.toggleAutoCorrect()
+ KeyCode.TOGGLE_AUTO_CAP, KeyCode.TOGGLE_FORCE_AUTO_CAPS -> {
+ if (primaryCode == KeyCode.TOGGLE_AUTO_CAP) settings.toggleAutoCapitalization()
+ else settings.toggleForceAutoCapitalization()
+ // An automatic shift refresh must not consume an explicit one-shot Shift.
+ if (keyboardSwitcher.keyboard?.mId?.isAlphabetShiftedManually != true) {
+ keyboardSwitcher.requestUpdatingShiftState(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
+ }
+ keyboardSwitcher.suggestionStripView?.updateToolbarButtonsActivatedState()
+ return
+ }
KeyCode.TOGGLE_INCOGNITO_MODE -> {
settings.toggleAlwaysIncognitoMode()
// Invalidate keyboard to update spacebar incognito icon immediately
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..a7ed92147 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,8 @@ 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.AUTO_CAP -> R.drawable.ic_auto_cap
+ ToolbarKey.FORCE_AUTO_CAPS -> R.drawable.ic_force_auto_caps
ToolbarKey.CLEAR_CLIPBOARD -> R.drawable.ic_bin
ToolbarKey.CLOSE_HISTORY -> R.drawable.ic_close
ToolbarKey.HANDWRITING -> R.drawable.ic_edit
@@ -245,6 +247,8 @@ 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.AUTO_CAP -> R.drawable.ic_auto_cap
+ ToolbarKey.FORCE_AUTO_CAPS -> R.drawable.ic_force_auto_caps
ToolbarKey.CLEAR_CLIPBOARD -> R.drawable.ic_bin
ToolbarKey.CLOSE_HISTORY -> R.drawable.ic_close
ToolbarKey.HANDWRITING -> R.drawable.ic_edit
@@ -325,6 +329,8 @@ 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.AUTO_CAP -> R.drawable.ic_auto_cap
+ ToolbarKey.FORCE_AUTO_CAPS -> R.drawable.ic_force_auto_caps
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..a11e2033e 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,8 @@ object KeyCode {
const val CLEAR_HANDWRITING = -10075
const val SWITCH_TO_USER_IME = -10076
const val OCR = -10077
+ const val TOGGLE_AUTO_CAP = -10079
+ const val TOGGLE_FORCE_AUTO_CAPS = -10080
// Intents
@@ -231,7 +233,7 @@ object KeyCode {
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,
- CUSTOM1, CUSTOM2, CUSTOM3, CUSTOM4, CUSTOM5, SWITCH_TO_USER_IME
+ CUSTOM1, CUSTOM2, CUSTOM3, CUSTOM4, CUSTOM5, SWITCH_TO_USER_IME, TOGGLE_AUTO_CAP, TOGGLE_FORCE_AUTO_CAPS
-> this
// conversion
diff --git a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
index c9f00f4af..fa92de8d9 100644
--- a/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
+++ b/app/src/main/java/helium314/keyboard/latin/settings/Settings.kt
@@ -131,6 +131,18 @@ class Settings private constructor() : SharedPreferences.OnSharedPreferenceChang
prefs.edit().putBoolean(PREF_AUTO_CORRECTION, !oldValue).apply()
}
+ fun toggleAutoCapitalization() {
+ val prefs = mPrefs ?: return
+ val oldValue = prefs.getBoolean(PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP)
+ prefs.edit().putBoolean(PREF_AUTO_CAP, !oldValue).apply()
+ }
+
+ fun toggleForceAutoCapitalization() {
+ val prefs = mPrefs ?: return
+ val oldValue = prefs.getBoolean(PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS)
+ prefs.edit().putBoolean(PREF_FORCE_AUTO_CAPS, !oldValue).apply()
+ }
+
fun toggleAlwaysIncognitoMode() {
val prefs = mPrefs ?: return
val oldValue = prefs.getBoolean(PREF_ALWAYS_INCOGNITO_MODE, Defaults.PREF_ALWAYS_INCOGNITO_MODE)
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..2e71f453b 100644
--- a/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
+++ b/app/src/main/java/helium314/keyboard/latin/utils/ToolbarUtils.kt
@@ -14,6 +14,7 @@ import android.os.Handler
import android.os.Looper
import android.annotation.SuppressLint
import androidx.core.view.forEach
+import androidx.core.view.ViewCompat
import helium314.keyboard.keyboard.internal.KeyboardIconsSet
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.BuildConfig
@@ -179,6 +180,8 @@ class TagDrawable(private val text: String) : Drawable() {
fun setToolbarButtonsActivatedStateOnPrefChange(buttonsGroup: ViewGroup, key: String?) {
// settings need to be updated when buttons change
if (key != Settings.PREF_AUTO_CORRECTION
+ && key != Settings.PREF_AUTO_CAP
+ && key != Settings.PREF_FORCE_AUTO_CAPS
&& key != Settings.PREF_ALWAYS_INCOGNITO_MODE
&& key?.startsWith(Settings.PREF_ONE_HANDED_MODE_PREFIX) == false)
return
@@ -202,12 +205,26 @@ fun setToolbarButtonActivatedState(button: ImageButton) {
ONE_HANDED -> Settings.getValues().mOneHandedModeEnabled
SPLIT -> Settings.getValues().mIsSplitKeyboardEnabled
AUTOCORRECT -> Settings.getValues().mAutoCorrectionEnabledPerUserSettings
+ AUTO_CAP -> Settings.getValues().mAutoCap
+ FORCE_AUTO_CAPS -> Settings.getValues().mAutoCap && Settings.getValues().mForceAutoCaps
SELECT_MODE -> helium314.keyboard.keyboard.KeyboardActionListenerImpl.sPersistentSelectionModeActive
else -> true
}
button.isActivated = activated
+ if (button.tag == AUTO_CAP || button.tag == FORCE_AUTO_CAPS) {
+ val prefs = button.context.prefs()
+ val saved = if (button.tag == AUTO_CAP) prefs.getBoolean(Settings.PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP)
+ else prefs.getBoolean(Settings.PREF_FORCE_AUTO_CAPS, Defaults.PREF_FORCE_AUTO_CAPS)
+ val state = when {
+ !saved -> R.string.caps_control_off
+ activated -> R.string.caps_control_on
+ !prefs.getBoolean(Settings.PREF_AUTO_CAP, Defaults.PREF_AUTO_CAP) -> R.string.caps_control_paused_auto_cap
+ else -> R.string.caps_control_no_uppercase
+ }
+ ViewCompat.setStateDescription(button, button.context.getString(state))
+ }
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, AUTO_CAP, FORCE_AUTO_CAPS, SELECT_MODE)) {
colors.setColor(button.background, ColorType.TOOL_BAR_KEY_ENABLED_BACKGROUND)
if (button.drawable != null) {
button.clearColorFilter()
@@ -243,6 +260,8 @@ fun getCodeForToolbarKey(key: ToolbarKey) = Settings.getInstance().getCustomTool
TOUCHPAD -> KeyCode.TOGGLE_TOUCHPAD_MODE
TEXT_EDIT -> KeyCode.TOGGLE_TEXT_EDIT_MODE
AUTOCORRECT -> KeyCode.TOGGLE_AUTOCORRECT
+ AUTO_CAP -> KeyCode.TOGGLE_AUTO_CAP
+ FORCE_AUTO_CAPS -> KeyCode.TOGGLE_FORCE_AUTO_CAPS
CLEAR_CLIPBOARD -> KeyCode.CLIPBOARD_CLEAR_HISTORY
CLOSE_HISTORY -> KeyCode.ALPHA
EMOJI -> KeyCode.EMOJI
@@ -299,7 +318,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, AUTO_CAP, FORCE_AUTO_CAPS, 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_auto_cap.xml b/app/src/main/res/drawable/ic_auto_cap.xml
new file mode 100644
index 000000000..683fe26c1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_auto_cap.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_force_auto_caps.xml b/app/src/main/res/drawable/ic_force_auto_caps.xml
new file mode 100644
index 000000000..97b1fde76
--- /dev/null
+++ b/app/src/main/res/drawable/ic_force_auto_caps.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0891d1399..b2df3e71d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1555,6 +1555,11 @@ New dictionary:
Touchpad mode
Touchpad sensitivity
Force auto-capitalization
+ @string/force_auto_caps_title
+ On
+ Off
+ Saved on; requires Auto-capitalization
+ Saved on; this language has no uppercase letters
Force sentence capitalization on all text fields except passwords
Variable toolbar direction
diff --git a/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt b/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt
index 5af7ad7a4..679b58514 100644
--- a/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt
+++ b/app/src/test/java/helium314/keyboard/KeyboardParserTest.kt
@@ -3,6 +3,7 @@ package helium314.keyboard
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodSubtype
+import androidx.core.content.edit
import com.android.inputmethod.keyboard.ProximityInfo
import helium314.keyboard.keyboard.Key
import helium314.keyboard.keyboard.Key.KeyParams
@@ -19,16 +20,22 @@ import helium314.keyboard.keyboard.internal.keyboard_parser.POPUP_KEYS_NORMAL
import helium314.keyboard.keyboard.internal.keyboard_parser.addLocaleKeyTextsToParams
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.LatinIME
+import helium314.keyboard.latin.BuildConfig
import helium314.keyboard.latin.RichInputMethodSubtype
import helium314.keyboard.latin.common.Constants
+import helium314.keyboard.latin.settings.Settings
import helium314.keyboard.latin.utils.LayoutType
import helium314.keyboard.latin.utils.LayoutType.Companion.toExtraValue
import helium314.keyboard.latin.utils.LayoutUtilsCustom
import helium314.keyboard.latin.utils.POPUP_KEYS_LAYOUT
import helium314.keyboard.latin.utils.SubtypeUtilsAdditional
+import helium314.keyboard.latin.utils.clearCustomToolbarKeyCodes
+import helium314.keyboard.latin.utils.prefs
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
@@ -51,6 +58,9 @@ class ParserTest {
private lateinit var params: KeyboardParams
@BeforeTest fun setUp() {
+ shadowOf(RuntimeEnvironment.getApplication()).grantPermissions(
+ "${BuildConfig.APPLICATION_ID}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
+ )
latinIME = Robolectric.setupService(LatinIME::class.java)
ShadowLog.setupLogging()
ShadowLog.stream = System.out
@@ -154,6 +164,50 @@ f""", // no newline at the end
assertIsExpected("""[[{ "label": "a" }]]""", Expected('a'.code, "a"))
}
+ @Test @Config(sdk = [32, 35])
+ fun capsControlsHaveKeywordNumericAndPopupActions() {
+ for ((label, code) in listOf("auto_cap" to -10079, "force_auto_caps" to -10080)) {
+ assertEquals(1, KeyCode::class.java.fields.count {
+ it.type == Int::class.javaPrimitiveType && it.getInt(null) == code
+ }, "$label must have a unique keycode")
+ assertIsExpected("""[[{"label":"$label"}]]""", Expected(code, icon = label))
+ assertIsExpected("""[[{"label":"caps","code":$code}]]""", Expected(code, "caps"))
+ assertIsExpected("""[[{"label":"x","popup":{"main":{"label":"$label"}}}]]""",
+ Expected('x'.code, "x", popups = listOf(null to code)))
+ val keyword = LayoutParser.parseJsonString("""[[{"label":"$label"}]]""")
+ .single().single().compute(params)!!.toKeyParams(params)
+ assertEquals(null, keyword.outputText)
+ val popup = LayoutParser.parseJsonString("""[[{"label":"x","popup":{"main":{"label":"$label"}}}]]""")
+ .single().single().compute(params)!!.toKeyParams(params).mPopupKeys!!.single()
+ assertEquals(label, popup.mIconName)
+ assertEquals(null, popup.mOutputText)
+ }
+ assertIsExpected("""[[{"label":"force_auto_cap"}]]""",
+ Expected(KeyCode.MULTIPLE_CODE_POINTS, "force_auto_cap", text = "force_auto_cap"))
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsControlKeywordsRespectToolbarRemapsButNumericCodesStayFixed() {
+ val prefs = latinIME.prefs()
+ val original = prefs.getString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, null)
+ try {
+ prefs.edit { putString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, "AUTO_CAP,-7,null;FORCE_AUTO_CAPS,-8,null") }
+ clearCustomToolbarKeyCodes()
+ for ((label, code, remapped) in listOf(
+ Triple("auto_cap", -10079, KeyCode.DELETE),
+ Triple("force_auto_caps", -10080, KeyCode.DELETE_WORD)
+ )) {
+ assertIsExpected("""[[{"label":"$label"}]]""", Expected(remapped, icon = label))
+ assertIsExpected("""[[{"label":"caps","code":$code}]]""", Expected(code, "caps"))
+ assertIsExpected("""[[{"label":"x","popup":{"main":{"label":"$label"}}}]]""",
+ Expected('x'.code, "x", popups = listOf(null to remapped)))
+ }
+ } finally {
+ prefs.edit { putString(Settings.PREF_TOOLBAR_CUSTOM_KEY_CODES, original) }
+ clearCustomToolbarKeyCodes()
+ }
+ }
+
@Test fun labelAndExplicitCode() {
assertIsExpected("""[[{ "$": "text_key" "label": "a", "code": 98 }]]""", Expected('b'.code, "a"))
}
diff --git a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt
index d9b5b2324..a099cc7b8 100644
--- a/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt
+++ b/app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt
@@ -6,15 +6,27 @@ import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.text.InputType
+import android.text.TextUtils
import android.view.KeyEvent
import android.view.inputmethod.*
import androidx.core.content.edit
+import androidx.core.view.ViewCompat
import org.junit.Ignore
import helium314.keyboard.ShadowInputMethodManager2
import helium314.keyboard.ShadowLocaleManagerCompat
+import helium314.keyboard.ShadowProximityInfo
+import helium314.keyboard.compat.AppQuirk
+import helium314.keyboard.compat.AppQuirksManager
import helium314.keyboard.event.Event
import helium314.keyboard.keyboard.KeyboardSwitcher
+import helium314.keyboard.keyboard.Keyboard
+import helium314.keyboard.keyboard.KeyboardId
+import helium314.keyboard.keyboard.KeyboardLayoutSet
+import helium314.keyboard.keyboard.KeyboardTheme
import helium314.keyboard.keyboard.MainKeyboardView
+import helium314.keyboard.keyboard.internal.KeyboardIconsSet
+import helium314.keyboard.keyboard.internal.KeyboardParams
+import helium314.keyboard.keyboard.internal.KeyboardState
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
import helium314.keyboard.latin.ShadowFacilitator2.Companion.addedWords
import helium314.keyboard.latin.ShadowFacilitator2.Companion.lastAddedWord
@@ -22,20 +34,31 @@ import helium314.keyboard.latin.ShadowFacilitator2.Companion.lastNgramContext
import helium314.keyboard.latin.ShadowFacilitator2.Companion.ngramContexts
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
import helium314.keyboard.latin.common.Constants
+import helium314.keyboard.latin.common.Constants.Separators
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.SpaceState
import helium314.keyboard.latin.settings.Settings
+import helium314.keyboard.latin.suggestions.SuggestionStripView
import helium314.keyboard.latin.utils.ScriptUtils
import helium314.keyboard.latin.utils.SubtypeSettings
+import helium314.keyboard.latin.utils.ToolbarKey
+import helium314.keyboard.latin.utils.createToolbarKey
+import helium314.keyboard.latin.utils.getEnabledToolbarKeys
+import helium314.keyboard.latin.utils.getPinnedToolbarKeys
+import helium314.keyboard.latin.utils.getEnabledClipboardToolbarKeys
+import helium314.keyboard.latin.utils.setToolbarButtonActivatedState
+import helium314.keyboard.latin.utils.upgradeToolbarPrefs
import helium314.keyboard.latin.utils.getTimestampFormatter
import helium314.keyboard.latin.utils.prefs
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config
import org.robolectric.annotation.Implementation
import org.robolectric.annotation.Implements
@@ -46,6 +69,9 @@ import kotlin.streams.asSequence
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
@RunWith(RobolectricTestRunner::class)
@Config(shadows = [
@@ -72,6 +98,9 @@ class InputLogicTest {
@BeforeTest
fun setUp() {
+ shadowOf(RuntimeEnvironment.getApplication()).grantPermissions(
+ "${BuildConfig.APPLICATION_ID}.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
+ )
latinIME = Robolectric.setupService(LatinIME::class.java)
// start logging only after latinIME is created, avoids showing the stack traces if library is not found
ShadowLog.setupLogging()
@@ -89,6 +118,247 @@ class InputLogicTest {
assertEquals("", composingText)
}
+ @Test @Config(sdk = [32, 35])
+ fun capsActionsToggleOnlyExistingPreferencesAndRefreshAutoShift() {
+ reset()
+ val state = Mockito.mock(KeyboardState::class.java)
+ val stateField = KeyboardSwitcher::class.java.getDeclaredField("mState").apply { isAccessible = true }
+ val originalState = stateField.get(latinIME.keyboardSwitcher)
+ stateField.set(latinIME.keyboardSwitcher, state)
+ try {
+ for ((code, pref, initial) in listOf(
+ Triple(-10079, Settings.PREF_AUTO_CAP, true),
+ Triple(-10080, Settings.PREF_FORCE_AUTO_CAPS, false)
+ )) {
+ val before = latinIME.prefs().all.toMap()
+ repeat(2) { pass ->
+ Mockito.clearInvocations(state)
+ latinIME.keyboardActionListener.onCodeInput(code, 0, 0, false)
+ val enabled = if (pass == 0) !initial else initial
+ assertEquals(before + (pref to enabled), latinIME.prefs().all)
+ assertEquals(if (pref == Settings.PREF_AUTO_CAP) enabled else true, settingsValues.mAutoCap)
+ assertEquals(if (pref == Settings.PREF_FORCE_AUTO_CAPS) enabled else false, settingsValues.mForceAutoCaps)
+ val caps = if (settingsValues.mAutoCap) TextUtils.CAP_MODE_SENTENCES else 0
+ assertEquals(caps, inputLogic.getCurrentAutoCapsState(settingsValues))
+ Mockito.verify(state).onUpdateShiftState(caps, null)
+ assertEquals("", text)
+ }
+ }
+ } finally {
+ stateField.set(latinIME.keyboardSwitcher, originalState)
+ }
+ }
+
+ @Test @Config(sdk = [32, 35], shadows = [ShadowProximityInfo::class])
+ fun capsActionsPreserveManualShiftAndCapsLock() {
+ reset()
+ val switcher = latinIME.keyboardSwitcher
+ val stateField = KeyboardSwitcher::class.java.getDeclaredField("mState").apply { isAccessible = true }
+ val viewField = KeyboardSwitcher::class.java.getDeclaredField("mKeyboardView").apply { isAccessible = true }
+ val originalState = stateField.get(switcher)
+ val originalView = viewField.get(switcher)
+ val state = Mockito.mock(KeyboardState::class.java)
+ val view = Mockito.mock(MainKeyboardView::class.java)
+ stateField.set(switcher, state)
+ viewField.set(switcher, view)
+ try {
+ for (element in listOf(
+ KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED,
+ KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED,
+ KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED
+ )) {
+ val keyboard = Keyboard(KeyboardParams().apply {
+ mId = KeyboardLayoutSet.getFakeKeyboardId(element)
+ GRID_WIDTH = 1
+ GRID_HEIGHT = 1
+ })
+ Mockito.`when`(view.keyboard).thenReturn(keyboard)
+ for (code in listOf(-10079, -10080)) {
+ val before = latinIME.prefs().all.toMap()
+ latinIME.keyboardActionListener.onCodeInput(code, 0, 0, false)
+ assertTrue(before != latinIME.prefs().all)
+ assertEquals(keyboard, switcher.keyboard)
+ }
+ }
+ Mockito.verifyNoInteractions(state)
+ } finally {
+ stateField.set(switcher, originalState)
+ viewField.set(switcher, originalView)
+ }
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsActionsKeepCompositionSelectionAndPendingCorrectionUntouched() {
+ reset()
+ setText("keep ")
+ typeNoAssert("raw")
+ val correction = SuggestedWordInfo("RAW", "", 0, 0, Mockito.mock(Dictionary::class.java), 0, 0)
+ composer.setAutoCorrection(correction)
+ val before = Triple(text, cursor, composingText)
+ val wordsBefore = addedWords.toList()
+ for (code in listOf(-10079, -10080)) {
+ latinIME.keyboardActionListener.onCodeInput(code, 0, 0, false)
+ assertEquals(before, Triple(text, cursor, composingText))
+ assertEquals(correction, composer.getAutoCorrectionOrNull())
+ assertEquals(wordsBefore, addedWords)
+ checkConnectionConsistency()
+ }
+ setText("keep selected text")
+ setCursorPosition(5, 13)
+ for (code in listOf(-10079, -10080)) {
+ latinIME.keyboardActionListener.onCodeInput(code, 0, 0, false)
+ assertEquals("keep selected text", text)
+ assertEquals(5 to 13, selectionStart to selectionEnd)
+ assertEquals("selected", selectedText)
+ checkConnectionConsistency()
+ }
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsActionsStoreChoicesWithoutChangingAppAutocorrectOverrides() {
+ reset()
+ val packageName = "test.caps.controls"
+ val editorInfo = EditorInfo().apply {
+ inputType = currentInputType
+ this.packageName = packageName
+ }
+ editorInfoOverride = editorInfo
+ AppQuirksManager.saveQuirk(AppQuirk(packageName, autoCorrectionMode = AppQuirksManager.AUTOCORRECT_FORCE_ENABLE))
+ try {
+ latinIME.onStartInputInternal(editorInfo, false)
+ latinIME.onStartInputViewInternal(editorInfo, false)
+ assertTrue(settingsValues.mAutoCorrectEnabled)
+ val before = latinIME.prefs().all.toMap()
+ latinIME.keyboardActionListener.onCodeInput(-10079, 0, 0, false)
+ latinIME.keyboardActionListener.onCodeInput(-10080, 0, 0, false)
+ assertEquals(before + mapOf(Settings.PREF_AUTO_CAP to false, Settings.PREF_FORCE_AUTO_CAPS to true), latinIME.prefs().all)
+ assertFalse(settingsValues.mAutoCap)
+ assertTrue(settingsValues.mForceAutoCaps)
+ assertTrue(settingsValues.mAutoCorrectEnabled)
+ assertEquals(0, inputLogic.getCurrentAutoCapsState(settingsValues))
+ } finally {
+ AppQuirksManager.removeQuirk(packageName)
+ editorInfoOverride = null
+ }
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsToolbarAndPinnedControlsShareEffectiveStateAndDispatch() {
+ reset()
+ val auto = ToolbarKey.valueOf("AUTO_CAP")
+ val force = ToolbarKey.valueOf("FORCE_AUTO_CAPS")
+ val strip = SuggestionStripView(latinIME, null)
+ val keyboardView = MainKeyboardView(latinIME, null).apply { id = R.id.keyboard_view }
+ strip.setListener(latinIME, keyboardView)
+ val imeStripField = LatinIME::class.java.getDeclaredField("suggestionStripView").apply { isAccessible = true }
+ val switcherStripField = KeyboardSwitcher::class.java.getDeclaredField("mSuggestionStripView").apply { isAccessible = true }
+ val originalImeStrip = imeStripField.get(latinIME)
+ val originalSwitcherStrip = switcherStripField.get(latinIME.keyboardSwitcher)
+ imeStripField.set(latinIME, strip)
+ switcherStripField.set(latinIME.keyboardSwitcher, strip)
+ try {
+ val toolbar = strip.findViewById(R.id.toolbar)
+ val pinned = strip.findViewById(R.id.pinned_keys)
+ toolbar.removeAllViews()
+ pinned.removeAllViews()
+ val mainAuto = createToolbarKey(latinIME, auto)
+ val pinnedAuto = createToolbarKey(latinIME, auto)
+ val mainForce = createToolbarKey(latinIME, force)
+ val pinnedForce = createToolbarKey(latinIME, force)
+ listOf(mainAuto, mainForce).forEach { toolbar.addView(it); it.setOnClickListener(strip) }
+ listOf(pinnedAuto, pinnedForce).forEach { pinned.addView(it); it.setOnClickListener(strip) }
+ assertEquals("Auto-capitalization", mainAuto.contentDescription)
+ assertEquals("Force auto-capitalization", mainForce.contentDescription)
+ assertTrue(mainAuto.isActivated)
+ assertFalse(mainForce.isActivated)
+ assertEquals("On", ViewCompat.getStateDescription(mainAuto))
+ assertEquals("Off", ViewCompat.getStateDescription(mainForce))
+ mainForce.performClick()
+ assertTrue(mainForce.isActivated)
+ assertTrue(pinnedForce.isActivated)
+ pinnedAuto.performClick()
+ assertFalse(mainAuto.isActivated)
+ assertFalse(pinnedAuto.isActivated)
+ assertFalse(mainForce.isActivated)
+ assertFalse(pinnedForce.isActivated)
+ assertTrue(latinIME.prefs().getBoolean(Settings.PREF_FORCE_AUTO_CAPS, false))
+ assertEquals("Saved on; requires Auto-capitalization", ViewCompat.getStateDescription(mainForce))
+ assertEquals(ViewCompat.getStateDescription(mainForce), ViewCompat.getStateDescription(pinnedForce))
+ mainAuto.performClick()
+ assertTrue(mainAuto.isActivated)
+ assertTrue(pinnedAuto.isActivated)
+ assertTrue(mainForce.isActivated)
+ assertTrue(pinnedForce.isActivated)
+ assertEquals("On", ViewCompat.getStateDescription(pinnedForce))
+ } finally {
+ imeStripField.set(latinIME, originalImeStrip)
+ switcherStripField.set(latinIME.keyboardSwitcher, originalSwitcherStrip)
+ }
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsControlsHaveIconsInEveryStyleAndRespectUncasedLocales() {
+ reset()
+ val keys = listOf(ToolbarKey.valueOf("AUTO_CAP"), ToolbarKey.valueOf("FORCE_AUTO_CAPS"))
+ for (style in listOf(KeyboardTheme.STYLE_HOLO, KeyboardTheme.STYLE_MATERIAL, KeyboardTheme.STYLE_ROUNDED)) {
+ latinIME.prefs().edit { putString(Settings.PREF_ICON_STYLE, style) }
+ KeyboardIconsSet.instance.loadIcons(latinIME)
+ keys.forEach { assertNotNull(createToolbarKey(latinIME, it).drawable) }
+ }
+ latinIME.prefs().edit { putBoolean(Settings.PREF_FORCE_AUTO_CAPS, true) }
+ latinIME.settings.loadSettings(latinIME, Locale.KOREAN, settingsValues.mInputAttributes, ScriptUtils.SCRIPT_LATIN)
+ assertFalse(settingsValues.mAutoCap)
+ keys.forEach {
+ val button = createToolbarKey(latinIME, it)
+ assertFalse(button.isActivated)
+ assertEquals("Saved on; this language has no uppercase letters", ViewCompat.getStateDescription(button))
+ }
+ assertTrue(latinIME.prefs().getBoolean(Settings.PREF_AUTO_CAP, true))
+ assertTrue(latinIME.prefs().getBoolean(Settings.PREF_FORCE_AUTO_CAPS, false))
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsControlsUpgradeWithoutChangingToolbarOrderOrEnablingNewItems() {
+ reset()
+ val keys = listOf(ToolbarKey.valueOf("AUTO_CAP"), ToolbarKey.valueOf("FORCE_AUTO_CAPS"))
+ assertTrue(keys.none { it in getEnabledToolbarKeys(latinIME.prefs()) || it in getPinnedToolbarKeys(latinIME.prefs()) })
+ val enabled = listOf(ToolbarKey.PASTE, ToolbarKey.AUTOCORRECT)
+ val oldKeys = enabled + ToolbarKey.entries.filter { it !in keys && it !in enabled }
+ val original = oldKeys.joinToString(Separators.ENTRY) { it.name + Separators.KV + (it in enabled) }
+ val prefs = listOf(Settings.PREF_TOOLBAR_KEYS, Settings.PREF_PINNED_TOOLBAR_KEYS, Settings.PREF_CLIPBOARD_TOOLBAR_KEYS)
+ latinIME.prefs().edit { prefs.forEach { putString(it, original) } }
+ upgradeToolbarPrefs(latinIME.prefs())
+ assertEquals(enabled, getEnabledToolbarKeys(latinIME.prefs()))
+ assertEquals(enabled, getPinnedToolbarKeys(latinIME.prefs()))
+ assertEquals(enabled, getEnabledClipboardToolbarKeys(latinIME.prefs()))
+ for (pref in prefs) {
+ val updated = latinIME.prefs().getString(pref, "")!!
+ assertTrue(updated.startsWith(original))
+ keys.forEach { assertTrue(updated.contains(it.name + Separators.KV + false)) }
+ }
+ }
+
+ @Test @Config(sdk = [32, 35])
+ fun capsControlHighlightUsesEffectiveEnabledColors() {
+ reset()
+ latinIME.prefs().edit { putBoolean(Settings.PREF_FORCE_AUTO_CAPS, true) }
+ val values = settingsValues
+ val colorsField = values.javaClass.getDeclaredField("mColors").apply { isAccessible = true }
+ val originalColors = colorsField.get(values)
+ val colors = Mockito.mock(helium314.keyboard.latin.common.Colors::class.java)
+ val buttons = listOf("AUTO_CAP", "FORCE_AUTO_CAPS").map { createToolbarKey(latinIME, ToolbarKey.valueOf(it)) }
+ colorsField.set(values, colors)
+ try {
+ for (button in buttons) {
+ setToolbarButtonActivatedState(button)
+ Mockito.verify(colors).setColor(button.background, helium314.keyboard.latin.common.ColorType.TOOL_BAR_KEY_ENABLED_BACKGROUND)
+ Mockito.clearInvocations(colors)
+ }
+ } finally {
+ colorsField.set(values, originalColors)
+ }
+ }
+
@Test fun `english space-separated typing keeps composing word`() {
reset()
chainInput("hello")
@@ -1265,6 +1535,7 @@ class InputLogicTest {
text = ""
batchEdit = 0
currentInputType = InputType.TYPE_CLASS_TEXT
+ editorInfoOverride = null
lastAddedWord = ""
lastNgramContext = ""
addedWords.clear()
@@ -1506,6 +1777,7 @@ class InputLogicTest {
}
private var currentInputType = InputType.TYPE_CLASS_TEXT
+private var editorInfoOverride: EditorInfo? = null
private var currentScript = ScriptUtils.SCRIPT_LATIN
private val messages = mutableListOf() // for latinIME / ShadowInputMethodService
private val delayedMessages = mutableListOf() // for latinIME / ShadowInputMethodService
@@ -1705,7 +1977,7 @@ private val ic = object : InputConnection {
@Implements(InputMethodService::class)
class ShadowInputMethodService {
@Implementation
- fun getCurrentInputEditorInfo() = EditorInfo().apply {
+ fun getCurrentInputEditorInfo() = editorInfoOverride ?: EditorInfo().apply {
inputType = currentInputType
// anything else?
}
diff --git a/docs/FEATURES.md b/docs/FEATURES.md
index 779455ee9..7b69e6cfe 100644
--- a/docs/FEATURES.md
+++ b/docs/FEATURES.md
@@ -645,6 +645,25 @@ LeanType provides an extensible keycode architecture that allows customizing key
> [!TIP]
> All negative keycodes listed below are actively recognized and validated by `KeyCode.checkAndConvertCode()`. Positive integer codes correspond to standard Unicode codepoints (or ASCII values like `10` for Enter, `32` for Space, `9` for Tab).
+### Capitalization controls
+
+**Auto-capitalization** (`{"label":"auto_cap"}`, fixed code `-10079`) and
+**Force auto-capitalization** (`{"label":"force_auto_caps"}`, fixed code `-10080`)
+toggle the existing saved Text correction settings. Enable these optional controls
+in the toolbar, pin them, or use them in custom JSON keys and popups. New entries
+start disabled; existing toolbar order and custom keycodes are preserved.
+Keywords follow customized toolbar codes; an explicit numeric `code` keeps the
+fixed action. The force keyword is plural: `force_auto_caps`.
+
+These settings request sentence capitalization, not manual Shift or Caps Lock.
+Toggling preserves manual Shift/Caps Lock, selection, and the current composing
+word. Highlights reflect effective enablement, not whether the next character
+will be uppercase. Turning Auto-capitalization off also suppresses Force
+auto-capitalization without clearing its saved choice. Languages without
+uppercase letters suppress both effects. Accessibility state descriptions
+distinguish off, on, and saved-on-but-inactive states. Existing field-capitalization
+and correction rules are unchanged.
+
---
### 1. ✏️ Text Editing & Deletion
@@ -752,6 +771,8 @@ LeanType provides an extensible keycode architecture that allows customizing key
| **`-233`** | `VOICE_INPUT` | Initiates voice typing (Whisper on-device or system voice IME). |
| **`-244`** | `TOGGLE_INCOGNITO_MODE`| Toggles incognito private mode (disables dictionary learning). |
| **`-245`** | `TOGGLE_AUTOCORRECT` | Instantly toggles auto-correction on or off. |
+| **`-10079`** | `TOGGLE_AUTO_CAP` | Toggles the saved Auto-capitalization setting. |
+| **`-10080`** | `TOGGLE_FORCE_AUTO_CAPS` | Toggles the saved Force auto-capitalization setting. |
| **`-301`** | `SETTINGS` | Opens LeanType Settings. |
| **`-10043`** | `TIMESTAMP` | Inserts the current localized date and time stamp at cursor. |
| **`-10051`** | `INLINE_EMOJI_SEARCH_DONE` | Confirms and closes inline emoji search. |
@@ -830,4 +851,3 @@ LeanType provides an extensible keycode architecture that allows customizing key
| **`-10008`** | `NOT_SPECIFIED` | Structural dummy spacer key (disables key interaction, renders blank). |
-