Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions app/src/keyboards/java/be/scri/helpers/KeyHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class KeyHandler(
true
}
else -> {
handleDefaultKey(code)
handleDefaultKey(code, language)
true
}
}
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
55 changes: 15 additions & 40 deletions app/src/keyboards/java/be/scri/helpers/SpaceKeyProcessor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,33 +64,24 @@ 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()

Expand Down Expand Up @@ -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(" .")
}
17 changes: 3 additions & 14 deletions app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
40 changes: 39 additions & 1 deletion app/src/main/java/be/scri/helpers/PreferencesHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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)
}

/**
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/be/scri/ui/screens/LanguageSettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,6 +69,13 @@ fun LanguageSettingsScreen(
)
}

val autoSpaceAfterPunctuationState =
remember {
mutableStateOf(
PreferencesHelper.getAutoSpaceAfterPunctuationPreference(context, language),
)
}

val emojiSuggestionsState =
remember {
mutableStateOf(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -288,6 +306,12 @@ private fun getFunctionalityListData(settings: FunctionalitySettings): List<Scri
state = settings.periodOnDoubleTapState,
onToggle = settings.onTogglePeriodOnDoubleTap,
),
ScribeItem.SwitchItem(
title = R.string.i18n_app_settings_keyboard_functionality_auto_space_punctuation,
desc = R.string.i18n_app_settings_keyboard_functionality_auto_space_punctuation_description,
state = settings.autoSpaceAfterPunctuationState,
onToggle = settings.onToggleAutoSpaceAfterPunctuation,
),
ScribeItem.SwitchItem(
title = R.string.i18n_app_settings_keyboard_functionality_auto_suggest_emoji,
desc = R.string.i18n_app_settings_keyboard_functionality_auto_suggest_emoji_description,
Expand Down
8 changes: 5 additions & 3 deletions app/src/main/res/values/string.xml
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@
<string name="i18n.app.keyboard.shift">Shift</string>
<string name="i18n.app.keyboard.suggestion">Suggestion</string>
<string name="i18n.app.keyboard.tutorial.chapters">Tutorial chapters</string>
<string name="i18n.app.keyboard.tutorial.description">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.</string>
<string name="i18n.app.keyboard.tutorial.description">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.</string>
<string name="i18n.app.keyboard.tutorial.finish_tutorial">Finish tutorial</string>
<string name="i18n.app.keyboard.tutorial.next">Next</string>
<string name="i18n.app.keyboard.tutorial.non_scribe_keyboard">Non-Scribe Keyboard</string>
<string name="i18n.app.keyboard.tutorial.not_quite">Not quite! Try writing {expected_word}.</string>
<string name="i18n.app.keyboard.tutorial.noun_annotation">Noun annotation</string>
<string name="i18n.app.keyboard.tutorial.noun_annotation.instruction_1">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}.</string>
<string name="i18n.app.keyboard.tutorial.noun_annotation.instruction_2">Now write the word "{father_word}" and then press space. The gender tag will be "{father_tag}", for {father_gender}.</string>
<string name="i18n.app.keyboard.tutorial.noun_annotation.instruction_1">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}.</string>
<string name="i18n.app.keyboard.tutorial.noun_annotation.instruction_2">Now write the word "{father_word}" and then press space. The gender tag will be "{father_tag}" for {father_gender}.</string>
<string name="i18n.app.keyboard.tutorial.noun_annotation.invalid_language">Noun annotation is not available for {language} as nouns don\'t have genders. Please proceed to the next tutorial.</string>
<string name="i18n.app.keyboard.tutorial.noun_plurals">Noun plurals</string>
<string name="i18n.app.keyboard.tutorial.noun_plurals.instruction">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.</string>
Expand All @@ -174,6 +174,8 @@
<string name="i18n.app.settings.keyboard.functionality.default_emoji_tone_description">Set a default skin tone for emoji autosuggestions and completions.</string>
<string name="i18n.app.settings.keyboard.functionality.delete_word_by_word">Word for word deletion on long press</string>
<string name="i18n.app.settings.keyboard.functionality.delete_word_by_word_description">Delete text word by word when the delete key is pressed and held.</string>
<string name="i18n.app.settings.keyboard.functionality.auto_space_punctuation">Auto-space after punctuation</string>
<string name="i18n.app.settings.keyboard.functionality.auto_space_punctuation_description">Automatically insert a space after typing punctuation marks.</string>
<string name="i18n.app.settings.keyboard.functionality.double_space_period">Double space periods</string>
<string name="i18n.app.settings.keyboard.functionality.double_space_period_description">Automatically insert a period when the space key is pressed twice.</string>
<string name="i18n.app.settings.keyboard.functionality.hold_for_alt_chars">Hold for alternate characters</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@
<string name="drag_keyboard">Drag keyboard</string>
<string name="clipboard">Clipboard</string>
<string name="floating_keyboard">Floating keyboard</string>
<string name="i18n.app.settings.keyboard.functionality.auto_space_punctuation">Auto-space after punctuation</string>
<string name="i18n.app.settings.keyboard.functionality.auto_space_punctuation_description">Automatically insert a space after typing punctuation marks.</string>
</resources>

Original file line number Diff line number Diff line change
@@ -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<GeneralKeyboardIME>(relaxed = true)
private val suggestionHandler = mockk<SuggestionHandler>(relaxed = true)
private val inputConnection = mockk<InputConnection>(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() }
}
}
Loading