From 075e0f0821c13dad91fb8da273eebf6e06c2789f Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:13:21 +0530 Subject: [PATCH 1/4] feat: implement auto-spacing after punctuation and double-tap space period (#669) - Add auto-space after punctuation (. , ! ?) setting and key handling - Refine double-tap space for period conversion and auto-capitalization - Default auto-spacing and double-tap period preferences to enabled - Add unit tests for SpaceKeyProcessor --- .../java/be/scri/helpers/KeyHandler.kt | 37 +++++++++- .../java/be/scri/helpers/SpaceKeyProcessor.kt | 55 ++++---------- .../be/scri/services/GeneralKeyboardIME.kt | 17 +---- app/src/main/assets/i18n | 2 +- .../java/be/scri/helpers/PreferencesHelper.kt | 40 ++++++++++- .../scri/ui/screens/LanguageSettingsScreen.kt | 24 +++++++ app/src/main/res/values/string.xml | 8 ++- .../be/scri/helpers/SpaceKeyProcessorTest.kt | 71 +++++++++++++++++++ 8 files changed, 193 insertions(+), 61 deletions(-) create mode 100644 app/src/testKeyboards/kotlin/be/scri/helpers/SpaceKeyProcessorTest.kt diff --git a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt index 549c7385..25663f03 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyHandler.kt @@ -130,7 +130,7 @@ class KeyHandler( true } else -> { - handleDefaultKey(code) + handleDefaultKey(code, language) true } } @@ -361,7 +361,10 @@ class KeyHandler( * @param code The key code representing the character to input. */ - private fun handleDefaultKey(code: Int) { + private fun handleDefaultKey( + code: Int, + language: String, + ) { val isCommandBarActive = when (ime.currentState) { ScribeState.TRANSLATE, @@ -371,8 +374,38 @@ class KeyHandler( else -> false // use main input field for IDLE and SELECT_COMMAND } + val charCode = code.toChar() + val isPunctuation = charCode in listOf('.', ',', '!', '?') + val isAutoSpaceEnabled = + !isCommandBarActive && + isPunctuation && + PreferencesHelper.getAutoSpaceAfterPunctuationPreference(ime.applicationContext, language) + + if (isAutoSpaceEnabled) { + val ic = ime.currentInputConnection + val textBefore = ic?.getTextBeforeCursor(2, 0)?.toString() + if (textBefore != null && textBefore.length == 2 && textBefore.endsWith(" ")) { + val charBeforeSpace = textBefore[0] + if (charBeforeSpace in listOf('.', ',', '!', '?')) { + ic.deleteSurroundingText(1, 0) + } + } + } + ime.handleElseCondition(code, ime.keyboardMode, isCommandBarActive) + if (isAutoSpaceEnabled) { + val ic = ime.currentInputConnection + val textBefore = ic?.getTextBeforeCursor(2, 0)?.toString() + if (textBefore != null && textBefore.length == 2) { + val prevChar = textBefore[0] + val typedPunct = textBefore[1] + if (typedPunct in listOf('.', ',', '!', '?') && !prevChar.isWhitespace() && prevChar !in listOf('.', ',', '!', '?')) { + ic.commitText(" ", 1) + } + } + } + if (ime.currentState == ScribeState.IDLE) { val currentWord = ime.getLastWordBeforeCursor() autocompletionHandler.processAutocomplete(currentWord) diff --git a/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt b/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt index 568975a8..677ccf1d 100644 --- a/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt +++ b/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt @@ -17,6 +17,7 @@ class SpaceKeyProcessor( private val ime: GeneralKeyboardIME, private val suggestionHandler: SuggestionHandler, ) { + /** * Handles the "Space" key press. * If not in command bar mode, it implements "period on double tap" logic or commits a normal space. @@ -64,33 +65,23 @@ class SpaceKeyProcessor( val periodOnDoubleTapEnabled = PreferencesHelper.getEnablePeriodOnSpaceBarDoubleTap(context = ime, ime.language) val ic = ime.currentInputConnection ?: return val wordBeforeSpace = ime.getLastWordBeforeCursor() - // Get char before space. - val twoCharsBeforeCursor = ic.getTextBeforeCursor(2, 0)?.toString() - val charBeforeSpace = if (twoCharsBeforeCursor?.length == 2) twoCharsBeforeCursor[0] else null - val isPunctuationBeforeSpace = charBeforeSpace == '.' || charBeforeSpace == '?' || charBeforeSpace == '!' - var shouldEnableAutoCapitalization = false + val textBefore = ic.getTextBeforeCursor(2, 0)?.toString() + val charBeforeSpace = if (textBefore != null && textBefore.length == 2) textBefore[0] else null + val isPunctuationOrSpaceBefore = charBeforeSpace == null || + charBeforeSpace.isWhitespace() || + charBeforeSpace in listOf('.', '?', '!', ',') - if (periodOnDoubleTapEnabled && wasLastKeySpace && ime.hasTextBeforeCursor()) { - val textBeforeTwoChars = ic.getTextBeforeCursor(2, 0)?.toString() + var shouldEnableAutoCapitalization = false - if (meetsTwoCharDoubleSpacePeriodCondition(textBeforeTwoChars)) { - val oneCharBefore = ic.getTextBeforeCursor(1, 0)?.toString() - if (oneCharBefore == " " && !isPunctuationBeforeSpace) { - ime.commitPeriodAfterSpace() - shouldEnableAutoCapitalization = true - } else { - insertSpace() - } - } else { - val textBeforeOneChar = ic.getTextBeforeCursor(1, 0)?.toString() - if (textBeforeOneChar == " " && !isPunctuationBeforeSpace) { - ime.commitPeriodAfterSpace() - shouldEnableAutoCapitalization = true - } else { - insertSpace() - } - } + if (periodOnDoubleTapEnabled && + wasLastKeySpace && + textBefore != null && + textBefore.endsWith(" ") && + !isPunctuationOrSpaceBefore + ) { + ime.commitPeriodAfterSpace() + shouldEnableAutoCapitalization = true } else { insertSpace() @@ -125,20 +116,4 @@ class SpaceKeyProcessor( commandBarState = false, ) } - - /** - * Checks if the text before the cursor meets the specific criteria for inserting a period - * on a double space when the text before is two characters long. - * Criteria: not null, length is 2, starts with a space, and does not end with " .". - * This typically matches patterns like " X" (where X is not '.') or " ". - * - * @param textBefore The two characters of text immediately before the cursor. - * - * @return true if the conditions are met, false otherwise. - */ - private fun meetsTwoCharDoubleSpacePeriodCondition(textBefore: String?): Boolean = - textBefore != null && - textBefore.length == 2 && - textBefore.startsWith(" ") && - !textBefore.endsWith(" .") } diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 16115def..5e4226df 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -504,22 +504,11 @@ abstract class GeneralKeyboardIME( */ override fun hasTextBeforeCursor(): Boolean = hasTextBeforeCursor - /** - * Handles the "period on double tap" feature. If enabled, it replaces the two spaces with a period and a space. - */ override fun commitPeriodAfterSpace() { if (currentState == ScribeState.IDLE || currentState == ScribeState.SELECT_COMMAND) { - val isPeriodOnDoubleTapEnabled = PreferencesHelper.getEnablePeriodOnSpaceBarDoubleTap(this, language) - if (isPeriodOnDoubleTapEnabled) { - currentInputConnection?.apply { - deleteSurroundingText(1, 0) - commitText(". ", 1) - } - } else { - currentInputConnection?.apply { - deleteSurroundingText(1, 0) - commitText(" ", 1) - } + currentInputConnection?.apply { + deleteSurroundingText(1, 0) + commitText(". ", 1) } } } diff --git a/app/src/main/assets/i18n b/app/src/main/assets/i18n index 7a66444d..06f89f1e 160000 --- a/app/src/main/assets/i18n +++ b/app/src/main/assets/i18n @@ -1 +1 @@ -Subproject commit 7a66444d106080e32ad67d31e3ef51a76b1de1fa +Subproject commit 06f89f1e8233a7a6b3ebcd02cbbf517fa8609a21 diff --git a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt index fa2c74a6..7a255b92 100644 --- a/app/src/main/java/be/scri/helpers/PreferencesHelper.kt +++ b/app/src/main/java/be/scri/helpers/PreferencesHelper.kt @@ -15,6 +15,7 @@ import androidx.core.content.edit object PreferencesHelper { const val SCRIBE_PREFS = "app_preferences" private const val PERIOD_ON_DOUBLE_TAP = "period_on_double_tap" + private const val AUTO_SPACE_AFTER_PUNCTUATION = "auto_space_after_punctuation" private const val VIBRATE_ON_KEYPRESS = "vibrate_on_keypress" private const val SOUND_ON_KEYPRESS = "sound_on_keypress" private const val SHOW_POPUP_ON_KEYPRESS = "show_popup_on_keypress" @@ -96,6 +97,27 @@ object PreferencesHelper { } } + /** + * Sets the preference for enabling or disabling auto spacing after punctuation. + * + * @param context The application context. + * @param language The language for which to set the preference. + * @param shouldEnableAutoSpaceAfterPunctuation Whether to enable or disable the feature. + */ + fun setAutoSpaceAfterPunctuationPreference( + context: Context, + language: String, + shouldEnableAutoSpaceAfterPunctuation: Boolean, + ) { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, Context.MODE_PRIVATE) + sharedPref.edit { + putBoolean( + getLanguageSpecificPreferenceKey(AUTO_SPACE_AFTER_PUNCTUATION, language), + shouldEnableAutoSpaceAfterPunctuation, + ) + } + } + /** * Sets the preference for disabling or enabling accent characters for a language. * @@ -348,7 +370,23 @@ object PreferencesHelper { language: String, ): Boolean { val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, MODE_PRIVATE) - return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(PERIOD_ON_DOUBLE_TAP, language), false) + return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(PERIOD_ON_DOUBLE_TAP, language), true) + } + + /** + * Retrieves whether auto spacing after punctuation is enabled for a given language. + * + * @param context The application context. + * @param language The language for which to check the preference. + * + * @return true if auto spacing after punctuation is enabled, false otherwise. + */ + fun getAutoSpaceAfterPunctuationPreference( + context: Context, + language: String, + ): Boolean { + val sharedPref = context.getSharedPreferences(SCRIBE_PREFS, MODE_PRIVATE) + return sharedPref.getBoolean(getLanguageSpecificPreferenceKey(AUTO_SPACE_AFTER_PUNCTUATION, language), true) } /** diff --git a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt index 19a5e7b9..7b67fdf4 100644 --- a/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt +++ b/app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt @@ -30,6 +30,8 @@ import be.scri.ui.models.ScribeItemList private data class FunctionalitySettings( val periodOnDoubleTapState: Boolean, val onTogglePeriodOnDoubleTap: (Boolean) -> Unit, + val autoSpaceAfterPunctuationState: Boolean, + val onToggleAutoSpaceAfterPunctuation: (Boolean) -> Unit, val emojiSuggestionsState: Boolean, val onToggleEmojiSuggestions: (Boolean) -> Unit, val togglePopUpOnKeyPress: Boolean, @@ -67,6 +69,13 @@ fun LanguageSettingsScreen( ) } + val autoSpaceAfterPunctuationState = + remember { + mutableStateOf( + PreferencesHelper.getAutoSpaceAfterPunctuationPreference(context, language), + ) + } + val emojiSuggestionsState = remember { mutableStateOf( @@ -170,6 +179,15 @@ fun LanguageSettingsScreen( isEnabled, ) }, + autoSpaceAfterPunctuationState = autoSpaceAfterPunctuationState.value, + onToggleAutoSpaceAfterPunctuation = { isEnabled -> + autoSpaceAfterPunctuationState.value = isEnabled + PreferencesHelper.setAutoSpaceAfterPunctuationPreference( + context, + language, + isEnabled, + ) + }, emojiSuggestionsState = emojiSuggestionsState.value, onToggleEmojiSuggestions = { isEnabled -> emojiSuggestionsState.value = isEnabled @@ -288,6 +306,12 @@ private fun getFunctionalityListData(settings: FunctionalitySettings): ListShift Suggestion Tutorial chapters - This quick tutorial will show you how to use Scribe to support writing in your second language.\nMake sure you select the desired Scribe keyboard by pressing 🌐 when typing. + This quick tutorial will show you how to use Scribe to support writing in your second language.\n\nMake sure you select the desired Scribe keyboard by pressing 🌐 when typing. Finish tutorial Next Non-Scribe Keyboard Not quite! Try writing {expected_word}. Noun annotation - Write the word "{mother_word}". Notice the word suggestions that appear on the keyboard\'s top bar.\n\nThen, press space. You will see the word\'s gender tag on the keyboard\'s top bar – in this case, "{mother_tag}" for {mother_gender}. - Now write the word "{father_word}" and then press space. The gender tag will be "{father_tag}", for {father_gender}. + Write the word "{mother_word}". Notice the word suggestions that appear on the keyboard\'s top bar.\n\nThen, press space. You will see the word\'s gender tag on the keyboard\'s top bar – in this case "{mother_tag}" for {mother_gender}. + Now write the word "{father_word}" and then press space. The gender tag will be "{father_tag}" for {father_gender}. Noun annotation is not available for {language} as nouns don\'t have genders. Please proceed to the next tutorial. Noun plurals Scribe can easily find the plural of any noun that Wikidata has. Tap the pencil-like Scribe key on the top-left corner of your keyboard, and select {plural}.\n\nThen write the noun you want the plural for, press ▶, and the plural will be returned to you. @@ -174,6 +174,8 @@ Set a default skin tone for emoji autosuggestions and completions. Word for word deletion on long press Delete text word by word when the delete key is pressed and held. + Auto-space after punctuation + Automatically insert a space after typing punctuation marks. Double space periods Automatically insert a period when the space key is pressed twice. Hold for alternate characters diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/SpaceKeyProcessorTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/SpaceKeyProcessorTest.kt new file mode 100644 index 00000000..8af62073 --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/SpaceKeyProcessorTest.kt @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import android.view.inputmethod.InputConnection +import be.scri.services.GeneralKeyboardIME +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class SpaceKeyProcessorTest { + private val ime = mockk(relaxed = true) + private val suggestionHandler = mockk(relaxed = true) + private val inputConnection = mockk(relaxed = true) + private lateinit var spaceKeyProcessor: SpaceKeyProcessor + + @Before + fun setUp() { + mockkObject(PreferencesHelper) + every { PreferencesHelper.getEnablePeriodOnSpaceBarDoubleTap(any(), any()) } returns true + spaceKeyProcessor = SpaceKeyProcessor(ime, suggestionHandler) + every { ime.language } returns "en" + every { ime.currentInputConnection } returns inputConnection + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun processKeycodeSpace_outsideCommandBar_returnsTrue() { + every { ime.currentState } returns be.scri.models.ScribeState.IDLE + + val result = spaceKeyProcessor.processKeycodeSpace(currentWasLastKeySpace = false) + + assertTrue(result) + verify { suggestionHandler.processWordSuggestions(any()) } + } + + @Test + fun processKeycodeSpace_inCommandBar_returnsFalse() { + every { ime.currentState } returns be.scri.models.ScribeState.TRANSLATE + + val result = spaceKeyProcessor.processKeycodeSpace(currentWasLastKeySpace = false) + + assertFalse(result) + verify { suggestionHandler.clearAllSuggestionsAndHideButtonUI() } + } + + @Test + fun processKeycodeSpace_doubleSpaceAfterWord_commitsPeriod() { + every { ime.currentState } returns be.scri.models.ScribeState.IDLE + every { inputConnection.getTextBeforeCursor(2, 0) } returns "s " + + // First press initializes lastSpacePressTime + spaceKeyProcessor.processKeycodeSpace(currentWasLastKeySpace = false) + + // Rapid second press with wasLastKeySpace = true + spaceKeyProcessor.processKeycodeSpace(currentWasLastKeySpace = true) + + verify { ime.commitPeriodAfterSpace() } + } +} From 79b9e043b48a529a39d868069216d5eb2b6970f7 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:20:43 +0530 Subject: [PATCH 2/4] style: fix ktlint formatting in SpaceKeyProcessor.kt --- .../keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt b/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt index 677ccf1d..6033be25 100644 --- a/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt +++ b/app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt @@ -17,7 +17,6 @@ class SpaceKeyProcessor( private val ime: GeneralKeyboardIME, private val suggestionHandler: SuggestionHandler, ) { - /** * Handles the "Space" key press. * If not in command bar mode, it implements "period on double tap" logic or commits a normal space. @@ -68,9 +67,10 @@ class SpaceKeyProcessor( val textBefore = ic.getTextBeforeCursor(2, 0)?.toString() val charBeforeSpace = if (textBefore != null && textBefore.length == 2) textBefore[0] else null - val isPunctuationOrSpaceBefore = charBeforeSpace == null || - charBeforeSpace.isWhitespace() || - charBeforeSpace in listOf('.', '?', '!', ',') + val isPunctuationOrSpaceBefore = + charBeforeSpace == null || + charBeforeSpace.isWhitespace() || + charBeforeSpace in listOf('.', '?', '!', ',') var shouldEnableAutoCapitalization = false From c7b5b6282dc49fe9aa3ff5506b4523068cb31bed Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:25:55 +0530 Subject: [PATCH 3/4] fix(ci): reset i18n submodule pointer to tracked commit --- app/src/main/assets/i18n | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/i18n b/app/src/main/assets/i18n index 06f89f1e..7a66444d 160000 --- a/app/src/main/assets/i18n +++ b/app/src/main/assets/i18n @@ -1 +1 @@ -Subproject commit 06f89f1e8233a7a6b3ebcd02cbbf517fa8609a21 +Subproject commit 7a66444d106080e32ad67d31e3ef51a76b1de1fa From d397528d37d461968dc906836cb9e53eae37d0c6 Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Mon, 10 Aug 2026 15:35:17 +0530 Subject: [PATCH 4/4] fix(strings): add auto-space strings to strings.xml to prevent build overwrite --- app/src/main/res/values/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1c901873..23363b51 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,5 +14,7 @@ Drag keyboard Clipboard Floating keyboard + Auto-space after punctuation + Automatically insert a space after typing punctuation marks.