From e6f365cdae9a4f2cc925119229f3f6d61c0c8032 Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Sat, 21 Dec 2024 14:20:53 +0100 Subject: [PATCH 1/9] FEATURE: Abbreviations --- .../com/dessalines/thumbkey/IMEService.kt | 115 ++++++++++- .../com/dessalines/thumbkey/MainActivity.kt | 8 + .../thumbkey/db/AbbreviationRepository.kt | 53 +++++ .../java/com/dessalines/thumbkey/db/AppDb.kt | 52 ++++- .../thumbkey/keyboards/CommonKeys.kt | 15 +- .../ui/components/keyboard/KeyboardKey.kt | 18 +- .../ui/components/settings/SettingsScreen.kt | 11 + .../ui/screens/AbbreviationsScreen.kt | 195 ++++++++++++++++++ .../dessalines/thumbkey/utils/Abbreviation.kt | 3 + .../thumbkey/utils/AbbreviationManager.kt | 56 +++++ .../com/dessalines/thumbkey/utils/Types.kt | 4 + .../com/dessalines/thumbkey/utils/Utils.kt | 6 + app/src/main/res/values/strings.xml | 9 + 13 files changed, 526 insertions(+), 19 deletions(-) create mode 100644 app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt create mode 100644 app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt create mode 100644 app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt create mode 100644 app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt diff --git a/app/src/main/java/com/dessalines/thumbkey/IMEService.kt b/app/src/main/java/com/dessalines/thumbkey/IMEService.kt index 360563e4d..5738c9b0b 100644 --- a/app/src/main/java/com/dessalines/thumbkey/IMEService.kt +++ b/app/src/main/java/com/dessalines/thumbkey/IMEService.kt @@ -1,6 +1,9 @@ package com.dessalines.thumbkey import android.inputmethodservice.InputMethodService +import android.view.inputmethod.InputConnection +import com.dessalines.thumbkey.utils.AbbreviationManager +import com.dessalines.thumbkey.utils.KeyAction import android.util.Log import android.view.View import android.view.inputmethod.CursorAnchorInfo @@ -18,11 +21,104 @@ import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner import com.dessalines.thumbkey.utils.TAG +private const val IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1 + class IMEService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { + + private lateinit var abbreviationManager: AbbreviationManager + private var currentInputConnection: InputConnection? = null + + private fun getCurrentText(): String { + val ic = currentInputConnection ?: return "" + val beforeCursor = ic.getTextBeforeCursor(1000, 0) ?: return "" + return beforeCursor.toString() + } + + fun handleKeyAction(action: KeyAction) { + when (action) { + is KeyAction.CommitText -> { + if (action.text == " ") { + // Check for abbreviation when space is pressed + handleAbbreviationExpansion() + } else { + currentInputConnection?.commitText(action.text, 1) + } + } + is KeyAction.DeleteKeyAction -> { + currentInputConnection?.deleteSurroundingText(1, 0) + } + is KeyAction.DeleteWordBeforeCursor -> { + val text = getCurrentText() + val lastWord = text.split(" ").last() + currentInputConnection?.deleteSurroundingText(lastWord.length, 0) + } + is KeyAction.DeleteWordAfterCursor -> { + val afterCursor = currentInputConnection?.getTextAfterCursor(1000, 0) ?: return + val nextWord = afterCursor.split(" ").firstOrNull() ?: return + currentInputConnection?.deleteSurroundingText(0, nextWord.length) + } + is KeyAction.ReplaceLastText -> { + currentInputConnection?.deleteSurroundingText(action.trimCount, 0) + currentInputConnection?.commitText(action.text, 1) + } + is KeyAction.SendEvent -> { + currentInputConnection?.sendKeyEvent(action.event) + } + is KeyAction.ToggleShiftMode, + is KeyAction.ToggleNumericMode, + is KeyAction.ToggleEmojiMode, + is KeyAction.ToggleCapsLock, + is KeyAction.SwitchLanguage, + is KeyAction.SwitchIME, + is KeyAction.SwitchIMEVoice, + is KeyAction.GotoSettings -> { + // These actions are handled by the keyboard UI + } + is KeyAction.IMECompleteAction -> { + // A lot of apps like discord and slack use weird IME actions, + // so its best to only check the none case + when (val imeAction = getImeActionCode()) { + EditorInfo.IME_ACTION_NONE -> { + currentInputConnection?.commitText("\n", 1) + } + IME_ACTION_CUSTOM_LABEL -> { + currentInputConnection?.performEditorAction(currentInputEditorInfo.actionId) + } + else -> { + currentInputConnection?.performEditorAction(imeAction) + } + } + } + else -> { + // Handle any other actions + } + } + } + + private fun handleAbbreviationExpansion() { + val currentText = getCurrentText() + + val (shouldExpand, expandedText) = abbreviationManager.checkAndExpand(currentText) + + if (shouldExpand) { + val ic = currentInputConnection ?: run { + return + } + // Delete the abbreviation + val lastWord = currentText.split(Regex("[ \n]")).last() + ic.deleteSurroundingText(lastWord.length, 0) + + // Insert the expansion and a space + ic.commitText(expandedText + " ", 1) + } else { + currentInputConnection?.commitText(" ", 1) + } + } + private fun setupView(): View { val settingsRepo = (application as ThumbkeyApplication).appSettingsRepository @@ -49,6 +145,7 @@ class IMEService : restarting: Boolean, ) { super.onStartInput(attribute, restarting) + currentInputConnection = getCurrentInputConnection() val view = this.setupView() this.setInputView(view) } @@ -62,6 +159,11 @@ class IMEService : override fun onCreate() { super.onCreate() + try { + abbreviationManager = AbbreviationManager(applicationContext) + } catch (e: Exception) { + Log.e(TAG, "Error creating AbbreviationManager: ${e.message}", e) + } savedStateRegistryController.performRestore(null) handleLifecycleEvent(Lifecycle.Event.ON_RESUME) } @@ -80,7 +182,6 @@ class IMEService : ignoreCursorMove = false false } else { - Log.d(TAG, "cursor moved") cursorAnchorInfo.selectionStart != selectionStart || cursorAnchorInfo.selectionEnd != selectionEnd } @@ -109,4 +210,16 @@ class IMEService : private val savedStateRegistryController = SavedStateRegistryController.create(this) override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry + + // IME Action Methods + private fun getImeActionCode(): Int { + val ei = currentInputEditorInfo + + return if ((ei.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { + EditorInfo.IME_ACTION_NONE + } else { + // Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId" + ei.imeOptions and EditorInfo.IME_MASK_ACTION + } + } } diff --git a/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt b/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt index d5b892d06..a70859a70 100644 --- a/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt +++ b/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt @@ -26,6 +26,7 @@ import com.dessalines.thumbkey.ui.components.settings.SettingsScreen import com.dessalines.thumbkey.ui.components.settings.about.AboutScreen import com.dessalines.thumbkey.ui.components.settings.backupandrestore.BackupAndRestoreScreen import com.dessalines.thumbkey.ui.components.settings.behavior.BehaviorScreen +import com.dessalines.thumbkey.ui.screens.AbbreviationsScreen import com.dessalines.thumbkey.ui.components.settings.lookandfeel.LookAndFeelScreen import com.dessalines.thumbkey.ui.components.setup.SetupScreen import com.dessalines.thumbkey.ui.theme.ThumbkeyTheme @@ -154,6 +155,13 @@ class MainActivity : AppCompatActivity() { appSettingsViewModel = appSettingsViewModel, ) } + composable( + route = "abbreviations", + ) { + AbbreviationsScreen( + navController = navController, + ) + } } } } diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt b/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt new file mode 100644 index 000000000..2037bbb8e --- /dev/null +++ b/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt @@ -0,0 +1,53 @@ +package com.dessalines.thumbkey.db + +import androidx.annotation.WorkerThread +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch + +class AbbreviationRepository(private val abbreviationDao: AbbreviationDao) { + val allAbbreviations: LiveData> = abbreviationDao.getAllAbbreviations() + + @WorkerThread + suspend fun insertOrUpdate(abbr: String, expansion: String) { + abbreviationDao.insertOrUpdate(abbr, expansion) + } + + @WorkerThread + suspend fun delete(abbr: String) { + abbreviationDao.delete(abbr) + } + + @WorkerThread + fun getAbbreviation(abbr: String): Abbreviation? { + return abbreviationDao.getAbbreviation(abbr) + } +} + +class AbbreviationViewModel(private val repository: AbbreviationRepository) : ViewModel() { + val allAbbreviations: LiveData> = repository.allAbbreviations + + fun insertOrUpdate(abbr: String, expansion: String) = viewModelScope.launch { + repository.insertOrUpdate(abbr, expansion) + } + + fun delete(abbr: String) = viewModelScope.launch { + repository.delete(abbr) + } + + fun getAbbreviation(abbr: String): Abbreviation? { + return repository.getAbbreviation(abbr) + } +} + +class AbbreviationViewModelFactory(private val repository: AbbreviationRepository) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(AbbreviationViewModel::class.java)) { + @Suppress("UNCHECKED_CAST") + return AbbreviationViewModel(repository) as T + } + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt index 3d1d80882..c8a7813e2 100644 --- a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt +++ b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt @@ -344,6 +344,24 @@ data class BehaviorUpdate( val ghostKeysEnabled: Int, ) +@Dao +interface AbbreviationDao { + @Query("SELECT * FROM Abbreviation") + fun getAllAbbreviations(): LiveData> + + @Query("SELECT * FROM Abbreviation WHERE lower(abbreviation) = lower(:abbr) LIMIT 1") + fun getAbbreviation(abbr: String): Abbreviation? + + @Query("SELECT * FROM Abbreviation WHERE lower(abbreviation) = lower(:abbr) LIMIT 1") + suspend fun getAbbreviationAsync(abbr: String): Abbreviation? + + @Query("INSERT OR REPLACE INTO Abbreviation (abbreviation, expansion) VALUES (:abbr, :expansion)") + suspend fun insertOrUpdate(abbr: String, expansion: String) + + @Query("DELETE FROM Abbreviation WHERE abbreviation = :abbr") + suspend fun delete(abbr: String) +} + @Dao interface AppSettingsDao { @Query("SELECT * FROM AppSettings limit 1") @@ -582,13 +600,41 @@ val MIGRATION_15_16 = } } +val MIGRATION_16_17 = + object : Migration(16, 17) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + """ + CREATE TABLE IF NOT EXISTS Abbreviation ( + abbreviation TEXT PRIMARY KEY NOT NULL, + expansion TEXT NOT NULL + ) + """ + ) + // Add default abbreviation + db.execSQL( + """ + INSERT INTO Abbreviation (abbreviation, expansion) + VALUES ('gm', 'Guten Morgen ') + """ + ) + } + } + +@Entity +data class Abbreviation( + @PrimaryKey val abbreviation: String, + @ColumnInfo(name = "expansion") val expansion: String, +) + @Database( - version = 16, - entities = [AppSettings::class], + version = 17, + entities = [AppSettings::class, Abbreviation::class], exportSchema = true, ) abstract class AppDB : RoomDatabase() { abstract fun appSettingsDao(): AppSettingsDao + abstract fun abbreviationDao(): AbbreviationDao companion object { @Volatile @@ -621,7 +667,9 @@ abstract class AppDB : RoomDatabase() { MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16, + MIGRATION_16_17, ) + .fallbackToDestructiveMigration() // Necessary because it can't insert data on creation .addCallback( object : Callback() { diff --git a/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt b/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt index fd26d7378..6141986e5 100644 --- a/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt +++ b/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt @@ -168,7 +168,10 @@ val BACKSPACE_WIDE_KEY_ITEM = BACKSPACE_KEY_ITEM.copy(widthMultiplier = 3) val SPACEBAR_KEY_ITEM = KeyItemC( - center = KeyC(" "), + center = KeyC( + action = KeyAction.CommitText(" "), + display = KeyDisplay.TextDisplay(" ") + ), swipeType = FOUR_WAY_CROSS, slideType = SlideType.MOVE_CURSOR, left = @@ -210,7 +213,10 @@ val SPACEBAR_DOUBLE_KEY_ITEM = SPACEBAR_KEY_ITEM.copy(widthMultiplier = 2) val SPACEBAR_PROGRAMMING_KEY_ITEM = KeyItemC( - center = KeyC(" "), + center = KeyC( + action = KeyAction.CommitText(" "), + display = KeyDisplay.TextDisplay(" ") + ), swipeType = FOUR_WAY_CROSS, slideType = SlideType.MOVE_CURSOR, left = @@ -276,7 +282,10 @@ val RETURN_KEY_ITEM = val SPACEBAR_TYPESPLIT_MIDDLE_KEY_ITEM = KeyItemC( - center = KeyC(" "), + center = KeyC( + action = KeyAction.CommitText(" "), + display = KeyDisplay.TextDisplay(" ") + ), swipeType = FOUR_WAY_CROSS, slideType = SlideType.MOVE_CURSOR, left = diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt index bff5358cf..28f02c37e 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt @@ -1,5 +1,7 @@ package com.dessalines.thumbkey.ui.components.keyboard + import android.content.Context +import android.util.Log import android.media.AudioManager import android.view.HapticFeedbackConstants import android.view.KeyEvent @@ -81,6 +83,8 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeMark import kotlin.time.TimeSource +private const val TAG = "KeyboardKey" + @OptIn(ExperimentalFoundationApi::class) @Composable fun KeyboardKey( @@ -225,19 +229,7 @@ fun KeyboardKey( // Set the correct action val action = tapActions[tapCount % tapActions.size] - performKeyAction( - action = action, - ime = ime, - autoCapitalize = autoCapitalize, - keyboardSettings = keyboardSettings, - onToggleShiftMode = onToggleShiftMode, - onToggleNumericMode = onToggleNumericMode, - onToggleEmojiMode = onToggleEmojiMode, - onToggleCapsLock = onToggleCapsLock, - onAutoCapitalize = onAutoCapitalize, - onSwitchLanguage = onSwitchLanguage, - onChangePosition = onChangePosition, - ) + ime.handleKeyAction(action) doneKeyAction(scope, action, isDragged, releasedKey, animationHelperSpeed) }, onLongClick = { diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/settings/SettingsScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/settings/SettingsScreen.kt index c2206c89b..41db28689 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/settings/SettingsScreen.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/settings/SettingsScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.outlined.InstallMobile import androidx.compose.material.icons.outlined.KeyboardAlt import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Restore +import androidx.compose.material.icons.outlined.ShortText import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -153,6 +154,16 @@ fun SettingsScreen( }, onClick = { navController.navigate("behavior") }, ) + Preference( + title = { Text(stringResource(R.string.abbreviations)) }, + icon = { + Icon( + imageVector = Icons.Outlined.ShortText, + contentDescription = null, + ) + }, + onClick = { navController.navigate("abbreviations") }, + ) Preference( title = { Text(stringResource(R.string.backup_and_restore)) }, icon = { diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt new file mode 100644 index 000000000..45be65d91 --- /dev/null +++ b/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt @@ -0,0 +1,195 @@ +package com.dessalines.thumbkey.ui.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import com.dessalines.thumbkey.R +import com.dessalines.thumbkey.db.* +import com.dessalines.thumbkey.utils.SimpleTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AbbreviationsScreen( + navController: NavController, + abbreviationViewModel: AbbreviationViewModel = viewModel( + factory = AbbreviationViewModelFactory( + AbbreviationRepository( + AppDB.getDatabase(LocalContext.current).abbreviationDao() + ) + ) + ) +) { + var showAddDialog by remember { mutableStateOf(false) } + var abbreviationToEdit by remember { mutableStateOf(null) } + + Scaffold( + topBar = { + SimpleTopAppBar( + text = stringResource(R.string.abbreviations), + navController = navController + ) + }, + floatingActionButton = { + FloatingActionButton(onClick = { showAddDialog = true }) { + Icon(Icons.Default.Add, contentDescription = "Add abbreviation") + } + } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.abbreviations_description), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + + val abbreviations by abbreviationViewModel.allAbbreviations.observeAsState(initial = emptyList()) + LazyColumn { + items(abbreviations) { abbreviation -> + AbbreviationItem( + abbreviation = abbreviation, + onEdit = { abbreviationToEdit = it }, + onDelete = { abbreviationViewModel.delete(it.abbreviation) } + ) + } + } + } + + if (showAddDialog) { + AddEditAbbreviationDialog( + abbreviation = null, + onDismiss = { showAddDialog = false }, + onSave = { abbr, expansion -> + abbreviationViewModel.insertOrUpdate(abbr, expansion) + showAddDialog = false + } + ) + } + + abbreviationToEdit?.let { abbreviation -> + AddEditAbbreviationDialog( + abbreviation = abbreviation, + onDismiss = { abbreviationToEdit = null }, + onSave = { abbr, expansion -> + abbreviationViewModel.insertOrUpdate(abbr, expansion) + abbreviationToEdit = null + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AbbreviationItem( + abbreviation: Abbreviation, + onEdit: (Abbreviation) -> Unit, + onDelete: (Abbreviation) -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + onClick = { onEdit(abbreviation) } + ) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = abbreviation.abbreviation, + style = MaterialTheme.typography.titleMedium + ) + Text( + text = abbreviation.expansion, + style = MaterialTheme.typography.bodyMedium + ) + } + IconButton(onClick = { onDelete(abbreviation) }) { + Icon(Icons.Default.Delete, contentDescription = "Delete") + } + } + } +} + +@Composable +fun AddEditAbbreviationDialog( + abbreviation: Abbreviation?, + onDismiss: () -> Unit, + onSave: (String, String) -> Unit +) { + var abbr by remember { mutableStateOf(abbreviation?.abbreviation ?: "") } + var expansion by remember { mutableStateOf(abbreviation?.expansion ?: "") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Text( + text = if (abbreviation == null) + stringResource(R.string.add_abbreviation) + else + stringResource(R.string.edit_abbreviation) + ) + }, + text = { + Column { + OutlinedTextField( + value = abbr, + onValueChange = { abbr = it }, + label = { Text(stringResource(R.string.abbreviation)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = expansion, + onValueChange = { expansion = it }, + label = { Text(stringResource(R.string.expansion)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + } + }, + confirmButton = { + TextButton( + onClick = { + if (abbr.isNotBlank() && expansion.isNotBlank()) { + onSave(abbr, expansion) + } + } + ) { + Text(stringResource(R.string.save)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt new file mode 100644 index 000000000..51286be9a --- /dev/null +++ b/app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt @@ -0,0 +1,3 @@ +package com.dessalines.thumbkey.utils + +// This file is kept for future use diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt b/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt new file mode 100644 index 000000000..0e7cbf9b8 --- /dev/null +++ b/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt @@ -0,0 +1,56 @@ +package com.dessalines.thumbkey.utils + +import android.content.Context +import android.util.Log +import com.dessalines.thumbkey.db.AbbreviationDao +import com.dessalines.thumbkey.db.AppDB + +private const val ABBR_TAG = "AbbreviationManager" + +class AbbreviationManager(private val context: Context) { + init { + Log.d(ABBR_TAG, "Initializing AbbreviationManager") + } + + private val abbreviationDao: AbbreviationDao = run { + Log.d(ABBR_TAG, "Creating AbbreviationDao") + AppDB.getDatabase(context).abbreviationDao().also { + Log.d(ABBR_TAG, "AbbreviationDao created successfully") + } + } + + @Synchronized + fun checkAndExpand(text: String): Pair { + Log.d(ABBR_TAG, "checkAndExpand: input text = '$text'") + if (text.isEmpty()) { + Log.d(ABBR_TAG, "checkAndExpand: text is empty") + return Pair(false, text) + } + + // Get the last word from the text, handling both spaces and newlines + val words = text.split(Regex("[ \n]")) + if (words.isEmpty()) { + Log.d(ABBR_TAG, "checkAndExpand: no words found") + return Pair(false, text) + } + + val lastWord = words.last() + Log.d(ABBR_TAG, "checkAndExpand: lastWord = '$lastWord'") + if (lastWord.isEmpty()) { + Log.d(ABBR_TAG, "checkAndExpand: lastWord is empty") + return Pair(false, text) + } + + // Check if the last word is an abbreviation (case-insensitive) + val abbreviation = abbreviationDao.getAbbreviation(lastWord.lowercase()) + Log.d(ABBR_TAG, "checkAndExpand: found abbreviation = ${abbreviation?.expansion}") + if (abbreviation != null) { + // Return just the expansion + Log.d(ABBR_TAG, "checkAndExpand: expanding to '${abbreviation.expansion}'") + return Pair(true, abbreviation.expansion) + } + + Log.d(ABBR_TAG, "checkAndExpand: no expansion found") + return Pair(false, text) + } +} diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Types.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Types.kt index 3f770a1f2..a90af6557 100644 --- a/app/src/main/java/com/dessalines/thumbkey/utils/Types.kt +++ b/app/src/main/java/com/dessalines/thumbkey/utils/Types.kt @@ -197,6 +197,10 @@ sealed class KeyAction { data object SwitchIME : KeyAction() data object SwitchIMEVoice : KeyAction() + + class ExpandAbbreviation( + val text: String, + ) : KeyAction() } enum class CursorAccelerationMode( diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt index f75652dd3..3f8300e85 100644 --- a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt +++ b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt @@ -334,6 +334,7 @@ fun performKeyAction( onSwitchLanguage: () -> Unit, onChangePosition: ((old: KeyboardPosition) -> KeyboardPosition) -> Unit, ) { + Log.d(TAG, "performKeyAction: action = $action") when (action) { is KeyAction.CommitText -> { val text = action.text @@ -1016,6 +1017,11 @@ fun performKeyAction( } } + is KeyAction.ExpandAbbreviation -> { + val text = action.text + ime.currentInputConnection.commitText(text, 1) + } + is KeyAction.ToggleCurrentWordCapitalization -> { val maxLength = 100 val wordBorderCharacters = ".,;:!?\"'()-—[]{}<>/\\|#$%^_+=~`" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9dc8153f0..9c0b2a336 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,4 +93,13 @@ Database Restored. Database Backed up. Backup and restore + + + Abbreviations + Add and manage text abbreviations that will be automatically expanded when you type them followed by a space. + Add Abbreviation + Edit Abbreviation + Abbreviation + Expansion + Save From 57c16ff9e79c01b93168351c3a56f95656e2e866 Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Sun, 22 Dec 2024 08:52:40 +0100 Subject: [PATCH 2/9] FIX: digits keyboard switch --- .gitignore | 1 + .../thumbkey/ComposeKeyboardView.kt | 13 +- .../com/dessalines/thumbkey/IMEService.kt | 135 ++---------------- .../ui/components/keyboard/KeyboardKey.kt | 16 ++- .../ui/components/keyboard/KeyboardScreen.kt | 114 ++++++++++----- .../com/dessalines/thumbkey/utils/Utils.kt | 53 +++++-- 6 files changed, 163 insertions(+), 169 deletions(-) diff --git a/.gitignore b/.gitignore index bca7b392f..3ac5078c5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ app/schemas .classpath .kotlin build.sh +/original_files* diff --git a/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt b/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt index be75f6ebb..9ca5106f2 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt @@ -2,11 +2,14 @@ package com.dessalines.thumbkey import android.annotation.SuppressLint import android.content.Context +import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection @@ -14,7 +17,9 @@ import androidx.lifecycle.lifecycleScope import com.dessalines.thumbkey.db.AppSettingsRepository import com.dessalines.thumbkey.ui.components.keyboard.KeyboardScreen import com.dessalines.thumbkey.ui.theme.ThumbkeyTheme +import com.dessalines.thumbkey.utils.KeyboardMode import com.dessalines.thumbkey.utils.KeyboardPosition +import com.dessalines.thumbkey.utils.TAG import com.dessalines.thumbkey.utils.keyboardLayoutsSetFromDbIndexString import kotlinx.coroutines.launch @@ -23,12 +28,12 @@ class ComposeKeyboardView( context: Context, private val settingsRepo: AppSettingsRepository, ) : AbstractComposeView(context) { + private val ime = context as IMEService + @Composable override fun Content() { val settingsState = settingsRepo.appSettings.observeAsState() val settings by settingsState - val ctx = context as IMEService - ThumbkeyTheme( settings = settings, ) { @@ -36,7 +41,7 @@ class ComposeKeyboardView( KeyboardScreen( settings = settings, onSwitchLanguage = { - ctx.lifecycleScope.launch { + ime.lifecycleScope.launch { // Cycle to the next keyboard val state = settingsState.value state?.let { s -> @@ -61,7 +66,7 @@ class ComposeKeyboardView( } }, onChangePosition = { f -> - ctx.lifecycleScope.launch { + ime.lifecycleScope.launch { val state = settingsState.value state?.let { s -> val nextPosition = f(KeyboardPosition.entries[s.position]).ordinal diff --git a/app/src/main/java/com/dessalines/thumbkey/IMEService.kt b/app/src/main/java/com/dessalines/thumbkey/IMEService.kt index 5738c9b0b..eaffae492 100644 --- a/app/src/main/java/com/dessalines/thumbkey/IMEService.kt +++ b/app/src/main/java/com/dessalines/thumbkey/IMEService.kt @@ -1,8 +1,10 @@ package com.dessalines.thumbkey +import android.content.Context +import android.content.Intent import android.inputmethodservice.InputMethodService import android.view.inputmethod.InputConnection -import com.dessalines.thumbkey.utils.AbbreviationManager +import android.view.inputmethod.InputMethodManager import com.dessalines.thumbkey.utils.KeyAction import android.util.Log import android.view.View @@ -19,7 +21,9 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.dessalines.thumbkey.utils.KeyboardMode import com.dessalines.thumbkey.utils.TAG +import com.dessalines.thumbkey.utils.getKeyboardMode private const val IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1 @@ -29,96 +33,6 @@ class IMEService : ViewModelStoreOwner, SavedStateRegistryOwner { - private lateinit var abbreviationManager: AbbreviationManager - private var currentInputConnection: InputConnection? = null - - private fun getCurrentText(): String { - val ic = currentInputConnection ?: return "" - val beforeCursor = ic.getTextBeforeCursor(1000, 0) ?: return "" - return beforeCursor.toString() - } - - fun handleKeyAction(action: KeyAction) { - when (action) { - is KeyAction.CommitText -> { - if (action.text == " ") { - // Check for abbreviation when space is pressed - handleAbbreviationExpansion() - } else { - currentInputConnection?.commitText(action.text, 1) - } - } - is KeyAction.DeleteKeyAction -> { - currentInputConnection?.deleteSurroundingText(1, 0) - } - is KeyAction.DeleteWordBeforeCursor -> { - val text = getCurrentText() - val lastWord = text.split(" ").last() - currentInputConnection?.deleteSurroundingText(lastWord.length, 0) - } - is KeyAction.DeleteWordAfterCursor -> { - val afterCursor = currentInputConnection?.getTextAfterCursor(1000, 0) ?: return - val nextWord = afterCursor.split(" ").firstOrNull() ?: return - currentInputConnection?.deleteSurroundingText(0, nextWord.length) - } - is KeyAction.ReplaceLastText -> { - currentInputConnection?.deleteSurroundingText(action.trimCount, 0) - currentInputConnection?.commitText(action.text, 1) - } - is KeyAction.SendEvent -> { - currentInputConnection?.sendKeyEvent(action.event) - } - is KeyAction.ToggleShiftMode, - is KeyAction.ToggleNumericMode, - is KeyAction.ToggleEmojiMode, - is KeyAction.ToggleCapsLock, - is KeyAction.SwitchLanguage, - is KeyAction.SwitchIME, - is KeyAction.SwitchIMEVoice, - is KeyAction.GotoSettings -> { - // These actions are handled by the keyboard UI - } - is KeyAction.IMECompleteAction -> { - // A lot of apps like discord and slack use weird IME actions, - // so its best to only check the none case - when (val imeAction = getImeActionCode()) { - EditorInfo.IME_ACTION_NONE -> { - currentInputConnection?.commitText("\n", 1) - } - IME_ACTION_CUSTOM_LABEL -> { - currentInputConnection?.performEditorAction(currentInputEditorInfo.actionId) - } - else -> { - currentInputConnection?.performEditorAction(imeAction) - } - } - } - else -> { - // Handle any other actions - } - } - } - - private fun handleAbbreviationExpansion() { - val currentText = getCurrentText() - - val (shouldExpand, expandedText) = abbreviationManager.checkAndExpand(currentText) - - if (shouldExpand) { - val ic = currentInputConnection ?: run { - return - } - // Delete the abbreviation - val lastWord = currentText.split(Regex("[ \n]")).last() - ic.deleteSurroundingText(lastWord.length, 0) - - // Insert the expansion and a space - ic.commitText(expandedText + " ", 1) - } else { - currentInputConnection?.commitText(" ", 1) - } - } - private fun setupView(): View { val settingsRepo = (application as ThumbkeyApplication).appSettingsRepository @@ -136,19 +50,6 @@ class IMEService : return view } - /** - * This is called every time the keyboard is brought up. - * You can't use onCreate, because that can't pick up new numeric inputs - */ - override fun onStartInput( - attribute: EditorInfo?, - restarting: Boolean, - ) { - super.onStartInput(attribute, restarting) - currentInputConnection = getCurrentInputConnection() - val view = this.setupView() - this.setInputView(view) - } // Lifecycle Methods private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) @@ -159,13 +60,18 @@ class IMEService : override fun onCreate() { super.onCreate() - try { - abbreviationManager = AbbreviationManager(applicationContext) - } catch (e: Exception) { - Log.e(TAG, "Error creating AbbreviationManager: ${e.message}", e) - } savedStateRegistryController.performRestore(null) handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + + } + + override fun onStartInput( + attribute: EditorInfo?, + restarting: Boolean, + ) { + super.onStartInput(attribute, restarting) + val view = this.setupView() + this.setInputView(view) } override fun onDestroy() { @@ -211,15 +117,4 @@ class IMEService : override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry - // IME Action Methods - private fun getImeActionCode(): Int { - val ei = currentInputEditorInfo - - return if ((ei.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { - EditorInfo.IME_ACTION_NONE - } else { - // Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId" - ei.imeOptions and EditorInfo.IME_MASK_ACTION - } - } } diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt index 28f02c37e..122bff214 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt @@ -218,6 +218,7 @@ fun KeyboardKey( onClick = { // Set the last key info, and the tap count val cAction = key.center.action + Log.d(TAG, "KeyboardKey: Center action: $cAction") lastAction.value?.let { (lastAction, time) -> if (time.elapsedNow() < 1.seconds && lastAction == cAction && !ime.didCursorMove()) { tapCount += 1 @@ -229,7 +230,20 @@ fun KeyboardKey( // Set the correct action val action = tapActions[tapCount % tapActions.size] - ime.handleKeyAction(action) + Log.d(TAG, "KeyboardKey: Executing action: $action") + performKeyAction( + action = action, + ime = ime, + autoCapitalize = autoCapitalize, + keyboardSettings = keyboardSettings, + onToggleShiftMode = onToggleShiftMode, + onToggleNumericMode = onToggleNumericMode, + onToggleEmojiMode = onToggleEmojiMode, + onToggleCapsLock = onToggleCapsLock, + onAutoCapitalize = onAutoCapitalize, + onSwitchLanguage = onSwitchLanguage, + onChangePosition = onChangePosition, + ) doneKeyAction(scope, action, isDragged, releasedKey, animationHelperSpeed) }, onLongClick = { diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt index 361aa76ec..deab25a37 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -71,6 +72,8 @@ import com.dessalines.thumbkey.utils.KeyboardMode import com.dessalines.thumbkey.utils.KeyboardPosition import com.dessalines.thumbkey.utils.TAG import com.dessalines.thumbkey.utils.getKeyboardMode +import com.dessalines.thumbkey.utils.AbbreviationManager +import com.dessalines.thumbkey.utils.performKeyAction import com.dessalines.thumbkey.utils.keyboardPositionToAlignment import com.dessalines.thumbkey.utils.toBool import kotlin.time.TimeMark @@ -82,14 +85,14 @@ fun KeyboardScreen( onChangePosition: ((old: KeyboardPosition) -> KeyboardPosition) -> Unit, ) { val ctx = LocalContext.current as IMEService + val abbreviationManager = remember { AbbreviationManager(ctx.applicationContext) } var mode by remember { - val startMode = - getKeyboardMode( - ime = ctx, - autoCapitalize = settings?.autoCapitalize?.toBool() ?: false, - ) - + val startMode = getKeyboardMode( + ime = ctx, + autoCapitalize = settings?.autoCapitalize?.toBool() ?: false, + ) + Log.d(TAG, "KeyboardScreen: Initial keyboard mode: $startMode") mutableStateOf(startMode) } @@ -226,9 +229,38 @@ fun KeyboardScreen( if (soundOnTap) { audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK, .1f) } - ctx.currentInputConnection.commitText( - it.emoji, - 1, + performKeyAction( + action = KeyAction.CommitText(it.emoji), + ime = ctx, + autoCapitalize = autoCapitalize, + keyboardSettings = keyboardDefinition.settings, + onToggleShiftMode = { enable -> + mode = if (enable) KeyboardMode.SHIFTED else { + capsLock = false + KeyboardMode.MAIN + } + }, + onToggleNumericMode = { enable -> + mode = if (enable) KeyboardMode.NUMERIC else { + capsLock = false + KeyboardMode.MAIN + } + }, + onToggleEmojiMode = { enable -> + mode = if (enable) KeyboardMode.EMOJI else KeyboardMode.MAIN + }, + onToggleCapsLock = { capsLock = !capsLock }, + onAutoCapitalize = { enable -> + if (mode !== KeyboardMode.NUMERIC) { + if (enable) { + mode = KeyboardMode.SHIFTED + } else if (!capsLock) { + mode = KeyboardMode.MAIN + } + } + }, + onSwitchLanguage = onSwitchLanguage, + onChangePosition = onChangePosition, ) } emojiPicker @@ -272,32 +304,38 @@ fun KeyboardScreen( slideCursorMovementMode = slideCursorMovementMode, slideSpacebarDeadzoneEnabled = slideSpacebarDeadzoneEnabled, slideBackspaceDeadzoneEnabled = slideBackspaceDeadzoneEnabled, - onToggleShiftMode = { enable -> - mode = - if (enable) { - KeyboardMode.SHIFTED - } else { - capsLock = false - KeyboardMode.MAIN - } - }, - onToggleNumericMode = { enable -> - mode = - if (enable) { - KeyboardMode.NUMERIC - } else { - capsLock = false - KeyboardMode.MAIN - } - }, - onToggleEmojiMode = { enable -> - mode = - if (enable) { - KeyboardMode.EMOJI - } else { - KeyboardMode.MAIN - } - }, + onToggleShiftMode = { enable -> + Log.d(TAG, "KeyboardScreen: Toggling shift mode, enable: $enable") + mode = + if (enable) { + KeyboardMode.SHIFTED + } else { + capsLock = false + KeyboardMode.MAIN + } + Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") + }, + onToggleNumericMode = { enable -> + Log.d(TAG, "KeyboardScreen: Toggling numeric mode, enable: $enable") + mode = + if (enable) { + KeyboardMode.NUMERIC + } else { + capsLock = false + KeyboardMode.MAIN + } + Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") + }, + onToggleEmojiMode = { enable -> + Log.d(TAG, "KeyboardScreen: Toggling emoji mode, enable: $enable") + mode = + if (enable) { + KeyboardMode.EMOJI + } else { + KeyboardMode.MAIN + } + Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") + }, onToggleCapsLock = { capsLock = !capsLock }, @@ -409,6 +447,7 @@ fun KeyboardScreen( slideSpacebarDeadzoneEnabled = slideSpacebarDeadzoneEnabled, slideBackspaceDeadzoneEnabled = slideBackspaceDeadzoneEnabled, onToggleShiftMode = { enable -> + Log.d(TAG, "KeyboardScreen (main): Toggling shift mode, enable: $enable") mode = if (enable) { KeyboardMode.SHIFTED @@ -416,8 +455,10 @@ fun KeyboardScreen( capsLock = false KeyboardMode.MAIN } + Log.d(TAG, "KeyboardScreen (main): New keyboard mode: $mode") }, onToggleNumericMode = { enable -> + Log.d(TAG, "KeyboardScreen (main): Toggling numeric mode, enable: $enable") mode = if (enable) { KeyboardMode.NUMERIC @@ -425,14 +466,17 @@ fun KeyboardScreen( capsLock = false KeyboardMode.MAIN } + Log.d(TAG, "KeyboardScreen (main): New keyboard mode: $mode") }, onToggleEmojiMode = { enable -> + Log.d(TAG, "KeyboardScreen (main): Toggling emoji mode, enable: $enable") mode = if (enable) { KeyboardMode.EMOJI } else { KeyboardMode.MAIN } + Log.d(TAG, "KeyboardScreen (main): New keyboard mode: $mode") }, onToggleCapsLock = { capsLock = !capsLock diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt index 3f8300e85..58ce0f09d 100644 --- a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt +++ b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt @@ -340,10 +340,29 @@ fun performKeyAction( val text = action.text Log.d(TAG, "committing key text: $text") ime.ignoreNextCursorMove() - ime.currentInputConnection.commitText( - text, - 1, - ) + + if (text == " ") { + val currentText = ime.currentInputConnection.getTextBeforeCursor(1000, 0)?.toString() + if (currentText != null) { + val abbreviationManager = AbbreviationManager(ime.applicationContext) + val (shouldExpand, expandedText) = abbreviationManager.checkAndExpand(currentText) + + if (shouldExpand) { + // Delete the abbreviation + val lastWord = currentText.split(Regex("[ \n]")).last() + ime.currentInputConnection.deleteSurroundingText(lastWord.length, 0) + + // Insert the expansion and a space + ime.currentInputConnection.commitText(expandedText + " ", 1) + } else { + ime.currentInputConnection.commitText(" ", 1) + } + } else { + ime.currentInputConnection.commitText(" ", 1) + } + } else { + ime.currentInputConnection.commitText(text, 1) + } if (autoCapitalize) { autoCapitalize( @@ -1060,7 +1079,7 @@ fun performKeyAction( * Returns the current IME action, or IME_FLAG_NO_ENTER_ACTION if there is none. */ fun getImeActionCode(ime: IMEService): Int { - val ei = ime.currentInputEditorInfo + val ei = ime.currentInputEditorInfo ?: return EditorInfo.IME_ACTION_NONE return if ((ei.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { EditorInfo.IME_ACTION_NONE @@ -1080,17 +1099,27 @@ fun getKeyboardMode( autoCapitalize: Boolean, ): KeyboardMode { val inputType = ime.currentInputEditorInfo.inputType and (InputType.TYPE_MASK_CLASS) + Log.d(TAG, "getKeyboardMode: Input type: $inputType") + Log.d(TAG, "getKeyboardMode: Auto capitalize: $autoCapitalize") return if (listOf( InputType.TYPE_CLASS_NUMBER, InputType.TYPE_CLASS_PHONE, ).contains(inputType) ) { + Log.d(TAG, "getKeyboardMode: Setting NUMERIC mode due to number/phone input type") KeyboardMode.NUMERIC } else { - if (autoCapitalize && !isUriOrEmailOrPasswordField(ime) && autoCapitalizeCheck(ime)) { + val isUriOrEmail = isUriOrEmailOrPasswordField(ime) + val shouldAutoCapitalize = autoCapitalizeCheck(ime) + Log.d(TAG, "getKeyboardMode: Is URI/Email/Password field: $isUriOrEmail") + Log.d(TAG, "getKeyboardMode: Should auto capitalize: $shouldAutoCapitalize") + + if (autoCapitalize && !isUriOrEmail && shouldAutoCapitalize) { + Log.d(TAG, "getKeyboardMode: Setting SHIFTED mode due to auto capitalize") KeyboardMode.SHIFTED } else { + Log.d(TAG, "getKeyboardMode: Setting MAIN mode") KeyboardMode.MAIN } } @@ -1113,13 +1142,18 @@ private fun autoCapitalize( } } -fun autoCapitalizeCheck(ime: IMEService): Boolean = ime.currentInputConnection.getCursorCapsMode(ime.currentInputEditorInfo.inputType) > 0 +fun autoCapitalizeCheck(ime: IMEService): Boolean { + val connection = ime.currentInputConnection ?: return false + val editorInfo = ime.currentInputEditorInfo ?: return false + return connection.getCursorCapsMode(editorInfo.inputType) > 0 +} /** * Avoid capitalizing or switching to shifted mode in certain edit boxes */ fun isUriOrEmailOrPasswordField(ime: IMEService): Boolean { - val inputType = ime.currentInputEditorInfo.inputType and (InputType.TYPE_MASK_VARIATION) + val editorInfo = ime.currentInputEditorInfo ?: return false + val inputType = editorInfo.inputType and (InputType.TYPE_MASK_VARIATION) return listOf( InputType.TYPE_TEXT_VARIATION_URI, InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS, @@ -1133,7 +1167,8 @@ fun isUriOrEmailOrPasswordField(ime: IMEService): Boolean { } fun isPasswordField(ime: IMEService): Boolean { - val inputType = ime.currentInputEditorInfo.inputType and (InputType.TYPE_MASK_VARIATION) + val editorInfo = ime.currentInputEditorInfo ?: return false + val inputType = editorInfo.inputType and (InputType.TYPE_MASK_VARIATION) return listOf( InputType.TYPE_TEXT_VARIATION_PASSWORD, InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD, From e1f2729915582e4ef4e245783e691ed670d8ec37 Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Sun, 22 Dec 2024 09:05:09 +0100 Subject: [PATCH 3/9] FIX: removed code not needed for abbreviations --- .../thumbkey/ComposeKeyboardView.kt | 13 +++---- .../com/dessalines/thumbkey/IMEService.kt | 34 +++++++------------ .../thumbkey/keyboards/CommonKeys.kt | 15 ++------ .../ui/components/keyboard/KeyboardKey.kt | 6 ---- 4 files changed, 20 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt b/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt index 9ca5106f2..be75f6ebb 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ComposeKeyboardView.kt @@ -2,14 +2,11 @@ package com.dessalines.thumbkey import android.annotation.SuppressLint import android.content.Context -import android.util.Log import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection @@ -17,9 +14,7 @@ import androidx.lifecycle.lifecycleScope import com.dessalines.thumbkey.db.AppSettingsRepository import com.dessalines.thumbkey.ui.components.keyboard.KeyboardScreen import com.dessalines.thumbkey.ui.theme.ThumbkeyTheme -import com.dessalines.thumbkey.utils.KeyboardMode import com.dessalines.thumbkey.utils.KeyboardPosition -import com.dessalines.thumbkey.utils.TAG import com.dessalines.thumbkey.utils.keyboardLayoutsSetFromDbIndexString import kotlinx.coroutines.launch @@ -28,12 +23,12 @@ class ComposeKeyboardView( context: Context, private val settingsRepo: AppSettingsRepository, ) : AbstractComposeView(context) { - private val ime = context as IMEService - @Composable override fun Content() { val settingsState = settingsRepo.appSettings.observeAsState() val settings by settingsState + val ctx = context as IMEService + ThumbkeyTheme( settings = settings, ) { @@ -41,7 +36,7 @@ class ComposeKeyboardView( KeyboardScreen( settings = settings, onSwitchLanguage = { - ime.lifecycleScope.launch { + ctx.lifecycleScope.launch { // Cycle to the next keyboard val state = settingsState.value state?.let { s -> @@ -66,7 +61,7 @@ class ComposeKeyboardView( } }, onChangePosition = { f -> - ime.lifecycleScope.launch { + ctx.lifecycleScope.launch { val state = settingsState.value state?.let { s -> val nextPosition = f(KeyboardPosition.entries[s.position]).ordinal diff --git a/app/src/main/java/com/dessalines/thumbkey/IMEService.kt b/app/src/main/java/com/dessalines/thumbkey/IMEService.kt index eaffae492..360563e4d 100644 --- a/app/src/main/java/com/dessalines/thumbkey/IMEService.kt +++ b/app/src/main/java/com/dessalines/thumbkey/IMEService.kt @@ -1,11 +1,6 @@ package com.dessalines.thumbkey -import android.content.Context -import android.content.Intent import android.inputmethodservice.InputMethodService -import android.view.inputmethod.InputConnection -import android.view.inputmethod.InputMethodManager -import com.dessalines.thumbkey.utils.KeyAction import android.util.Log import android.view.View import android.view.inputmethod.CursorAnchorInfo @@ -21,18 +16,13 @@ import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner -import com.dessalines.thumbkey.utils.KeyboardMode import com.dessalines.thumbkey.utils.TAG -import com.dessalines.thumbkey.utils.getKeyboardMode - -private const val IME_ACTION_CUSTOM_LABEL = EditorInfo.IME_MASK_ACTION + 1 class IMEService : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { - private fun setupView(): View { val settingsRepo = (application as ThumbkeyApplication).appSettingsRepository @@ -50,6 +40,18 @@ class IMEService : return view } + /** + * This is called every time the keyboard is brought up. + * You can't use onCreate, because that can't pick up new numeric inputs + */ + override fun onStartInput( + attribute: EditorInfo?, + restarting: Boolean, + ) { + super.onStartInput(attribute, restarting) + val view = this.setupView() + this.setInputView(view) + } // Lifecycle Methods private var lifecycleRegistry: LifecycleRegistry = LifecycleRegistry(this) @@ -62,16 +64,6 @@ class IMEService : super.onCreate() savedStateRegistryController.performRestore(null) handleLifecycleEvent(Lifecycle.Event.ON_RESUME) - - } - - override fun onStartInput( - attribute: EditorInfo?, - restarting: Boolean, - ) { - super.onStartInput(attribute, restarting) - val view = this.setupView() - this.setInputView(view) } override fun onDestroy() { @@ -88,6 +80,7 @@ class IMEService : ignoreCursorMove = false false } else { + Log.d(TAG, "cursor moved") cursorAnchorInfo.selectionStart != selectionStart || cursorAnchorInfo.selectionEnd != selectionEnd } @@ -116,5 +109,4 @@ class IMEService : private val savedStateRegistryController = SavedStateRegistryController.create(this) override val savedStateRegistry: SavedStateRegistry = savedStateRegistryController.savedStateRegistry - } diff --git a/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt b/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt index 6141986e5..fd26d7378 100644 --- a/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt +++ b/app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt @@ -168,10 +168,7 @@ val BACKSPACE_WIDE_KEY_ITEM = BACKSPACE_KEY_ITEM.copy(widthMultiplier = 3) val SPACEBAR_KEY_ITEM = KeyItemC( - center = KeyC( - action = KeyAction.CommitText(" "), - display = KeyDisplay.TextDisplay(" ") - ), + center = KeyC(" "), swipeType = FOUR_WAY_CROSS, slideType = SlideType.MOVE_CURSOR, left = @@ -213,10 +210,7 @@ val SPACEBAR_DOUBLE_KEY_ITEM = SPACEBAR_KEY_ITEM.copy(widthMultiplier = 2) val SPACEBAR_PROGRAMMING_KEY_ITEM = KeyItemC( - center = KeyC( - action = KeyAction.CommitText(" "), - display = KeyDisplay.TextDisplay(" ") - ), + center = KeyC(" "), swipeType = FOUR_WAY_CROSS, slideType = SlideType.MOVE_CURSOR, left = @@ -282,10 +276,7 @@ val RETURN_KEY_ITEM = val SPACEBAR_TYPESPLIT_MIDDLE_KEY_ITEM = KeyItemC( - center = KeyC( - action = KeyAction.CommitText(" "), - display = KeyDisplay.TextDisplay(" ") - ), + center = KeyC(" "), swipeType = FOUR_WAY_CROSS, slideType = SlideType.MOVE_CURSOR, left = diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt index 122bff214..bff5358cf 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt @@ -1,7 +1,5 @@ package com.dessalines.thumbkey.ui.components.keyboard - import android.content.Context -import android.util.Log import android.media.AudioManager import android.view.HapticFeedbackConstants import android.view.KeyEvent @@ -83,8 +81,6 @@ import kotlin.time.Duration.Companion.seconds import kotlin.time.TimeMark import kotlin.time.TimeSource -private const val TAG = "KeyboardKey" - @OptIn(ExperimentalFoundationApi::class) @Composable fun KeyboardKey( @@ -218,7 +214,6 @@ fun KeyboardKey( onClick = { // Set the last key info, and the tap count val cAction = key.center.action - Log.d(TAG, "KeyboardKey: Center action: $cAction") lastAction.value?.let { (lastAction, time) -> if (time.elapsedNow() < 1.seconds && lastAction == cAction && !ime.didCursorMove()) { tapCount += 1 @@ -230,7 +225,6 @@ fun KeyboardKey( // Set the correct action val action = tapActions[tapCount % tapActions.size] - Log.d(TAG, "KeyboardKey: Executing action: $action") performKeyAction( action = action, ime = ime, From 80b34226b917e5e271069b1d13beae56f76d91ed Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Wed, 25 Dec 2024 08:42:48 +0100 Subject: [PATCH 4/9] FIX: remove default abbreviation --- app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt index c8a7813e2..30b5bd04a 100644 --- a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt +++ b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt @@ -611,13 +611,6 @@ val MIGRATION_16_17 = ) """ ) - // Add default abbreviation - db.execSQL( - """ - INSERT INTO Abbreviation (abbreviation, expansion) - VALUES ('gm', 'Guten Morgen ') - """ - ) } } From 339eeb231aff877c7cccfdbc5c0b3804300c7d07 Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Wed, 25 Dec 2024 09:02:15 +0100 Subject: [PATCH 5/9] FIX: remove unneded changes to .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3ac5078c5..bca7b392f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,3 @@ app/schemas .classpath .kotlin build.sh -/original_files* From 3f7ae1e5405fc5386ecc71f0857661ce8ada528b Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Wed, 25 Dec 2024 09:04:51 +0100 Subject: [PATCH 6/9] FIX: remove logs --- .../thumbkey/ui/components/keyboard/KeyboardScreen.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt index deab25a37..f7b7fbb13 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt @@ -447,7 +447,6 @@ fun KeyboardScreen( slideSpacebarDeadzoneEnabled = slideSpacebarDeadzoneEnabled, slideBackspaceDeadzoneEnabled = slideBackspaceDeadzoneEnabled, onToggleShiftMode = { enable -> - Log.d(TAG, "KeyboardScreen (main): Toggling shift mode, enable: $enable") mode = if (enable) { KeyboardMode.SHIFTED @@ -455,10 +454,8 @@ fun KeyboardScreen( capsLock = false KeyboardMode.MAIN } - Log.d(TAG, "KeyboardScreen (main): New keyboard mode: $mode") }, onToggleNumericMode = { enable -> - Log.d(TAG, "KeyboardScreen (main): Toggling numeric mode, enable: $enable") mode = if (enable) { KeyboardMode.NUMERIC @@ -466,17 +463,14 @@ fun KeyboardScreen( capsLock = false KeyboardMode.MAIN } - Log.d(TAG, "KeyboardScreen (main): New keyboard mode: $mode") }, onToggleEmojiMode = { enable -> - Log.d(TAG, "KeyboardScreen (main): Toggling emoji mode, enable: $enable") mode = if (enable) { KeyboardMode.EMOJI } else { KeyboardMode.MAIN } - Log.d(TAG, "KeyboardScreen (main): New keyboard mode: $mode") }, onToggleCapsLock = { capsLock = !capsLock From 99f1f71f7b2045265c8e0345b02e80d5c2e2b217 Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Wed, 25 Dec 2024 09:06:37 +0100 Subject: [PATCH 7/9] FIX: remove logs --- app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt index 58ce0f09d..15016c175 100644 --- a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt +++ b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt @@ -334,7 +334,6 @@ fun performKeyAction( onSwitchLanguage: () -> Unit, onChangePosition: ((old: KeyboardPosition) -> KeyboardPosition) -> Unit, ) { - Log.d(TAG, "performKeyAction: action = $action") when (action) { is KeyAction.CommitText -> { val text = action.text @@ -1099,8 +1098,6 @@ fun getKeyboardMode( autoCapitalize: Boolean, ): KeyboardMode { val inputType = ime.currentInputEditorInfo.inputType and (InputType.TYPE_MASK_CLASS) - Log.d(TAG, "getKeyboardMode: Input type: $inputType") - Log.d(TAG, "getKeyboardMode: Auto capitalize: $autoCapitalize") return if (listOf( InputType.TYPE_CLASS_NUMBER, From e9321717367a266525e11ca0fd4a573775d74976 Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Thu, 26 Dec 2024 09:48:19 +0100 Subject: [PATCH 8/9] FIX: AbbreviationsScreen.kt scroll further down after last entry to be able to delete entry FIX: Editing abbreviation entries does not create new entries but actually edits them --- .../thumbkey/db/AbbreviationRepository.kt | 20 +++++---- .../java/com/dessalines/thumbkey/db/AppDb.kt | 42 ++++++++++++++++--- .../ui/screens/AbbreviationsScreen.kt | 8 ++-- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt b/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt index 2037bbb8e..188335ca6 100644 --- a/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt +++ b/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt @@ -11,13 +11,17 @@ class AbbreviationRepository(private val abbreviationDao: AbbreviationDao) { val allAbbreviations: LiveData> = abbreviationDao.getAllAbbreviations() @WorkerThread - suspend fun insertOrUpdate(abbr: String, expansion: String) { - abbreviationDao.insertOrUpdate(abbr, expansion) + suspend fun insertOrUpdate(abbr: String, expansion: String, id: Int? = null) { + if (id != null) { + abbreviationDao.update(id, abbr, expansion) + } else { + abbreviationDao.insert(abbr, expansion) + } } @WorkerThread - suspend fun delete(abbr: String) { - abbreviationDao.delete(abbr) + suspend fun delete(id: Int) { + abbreviationDao.deleteById(id) } @WorkerThread @@ -29,12 +33,12 @@ class AbbreviationRepository(private val abbreviationDao: AbbreviationDao) { class AbbreviationViewModel(private val repository: AbbreviationRepository) : ViewModel() { val allAbbreviations: LiveData> = repository.allAbbreviations - fun insertOrUpdate(abbr: String, expansion: String) = viewModelScope.launch { - repository.insertOrUpdate(abbr, expansion) + fun insertOrUpdate(abbr: String, expansion: String, id: Int? = null) = viewModelScope.launch { + repository.insertOrUpdate(abbr, expansion, id) } - fun delete(abbr: String) = viewModelScope.launch { - repository.delete(abbr) + fun delete(id: Int) = viewModelScope.launch { + repository.delete(id) } fun getAbbreviation(abbr: String): Abbreviation? { diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt index 30b5bd04a..e8f75b8f0 100644 --- a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt +++ b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt @@ -355,11 +355,14 @@ interface AbbreviationDao { @Query("SELECT * FROM Abbreviation WHERE lower(abbreviation) = lower(:abbr) LIMIT 1") suspend fun getAbbreviationAsync(abbr: String): Abbreviation? - @Query("INSERT OR REPLACE INTO Abbreviation (abbreviation, expansion) VALUES (:abbr, :expansion)") - suspend fun insertOrUpdate(abbr: String, expansion: String) + @Query("UPDATE Abbreviation SET abbreviation = :newAbbr, expansion = :expansion WHERE id = :id") + suspend fun update(id: Int, newAbbr: String, expansion: String) - @Query("DELETE FROM Abbreviation WHERE abbreviation = :abbr") - suspend fun delete(abbr: String) + @Query("INSERT INTO Abbreviation (abbreviation, expansion) VALUES (:abbr, :expansion)") + suspend fun insert(abbr: String, expansion: String) + + @Query("DELETE FROM Abbreviation WHERE id = :id") + suspend fun deleteById(id: Int) } @Dao @@ -616,12 +619,38 @@ val MIGRATION_16_17 = @Entity data class Abbreviation( - @PrimaryKey val abbreviation: String, + @PrimaryKey(autoGenerate = true) val id: Int = 0, + @ColumnInfo(name = "abbreviation") val abbreviation: String, @ColumnInfo(name = "expansion") val expansion: String, ) +val MIGRATION_17_18 = object : Migration(17, 18) { + override fun migrate(db: SupportSQLiteDatabase) { + // Create a temporary table with the new schema + db.execSQL(""" + CREATE TABLE IF NOT EXISTS Abbreviation_temp ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + abbreviation TEXT NOT NULL, + expansion TEXT NOT NULL + ) + """) + + // Copy data from the old table to the new table + db.execSQL(""" + INSERT INTO Abbreviation_temp (abbreviation, expansion) + SELECT abbreviation, expansion FROM Abbreviation + """) + + // Drop the old table + db.execSQL("DROP TABLE Abbreviation") + + // Rename the temporary table to the original name + db.execSQL("ALTER TABLE Abbreviation_temp RENAME TO Abbreviation") + } +} + @Database( - version = 17, + version = 18, entities = [AppSettings::class, Abbreviation::class], exportSchema = true, ) @@ -661,6 +690,7 @@ abstract class AppDB : RoomDatabase() { MIGRATION_14_15, MIGRATION_15_16, MIGRATION_16_17, + MIGRATION_17_18, ) .fallbackToDestructiveMigration() // Necessary because it can't insert data on creation diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt index 45be65d91..76c6f211a 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt @@ -65,12 +65,14 @@ fun AbbreviationsScreen( ) val abbreviations by abbreviationViewModel.allAbbreviations.observeAsState(initial = emptyList()) - LazyColumn { + LazyColumn( + contentPadding = PaddingValues(bottom = 80.dp) // Add padding for FAB + ) { items(abbreviations) { abbreviation -> AbbreviationItem( abbreviation = abbreviation, onEdit = { abbreviationToEdit = it }, - onDelete = { abbreviationViewModel.delete(it.abbreviation) } + onDelete = { abbreviationViewModel.delete(it.id) } ) } } @@ -92,7 +94,7 @@ fun AbbreviationsScreen( abbreviation = abbreviation, onDismiss = { abbreviationToEdit = null }, onSave = { abbr, expansion -> - abbreviationViewModel.insertOrUpdate(abbr, expansion) + abbreviationViewModel.insertOrUpdate(abbr, expansion, abbreviation.id) abbreviationToEdit = null } ) From 82c7315a164b83610822d1c648a77f8fa52b1eba Mon Sep 17 00:00:00 2001 From: Benjamin Kobjolke Date: Thu, 26 Dec 2024 10:19:55 +0100 Subject: [PATCH 9/9] FIX: linting --- .../com/dessalines/thumbkey/MainActivity.kt | 2 +- .../thumbkey/db/AbbreviationRepository.kt | 39 +++--- .../java/com/dessalines/thumbkey/db/AppDb.kt | 54 +++++---- .../ui/components/keyboard/KeyboardScreen.kt | 100 ++++++++-------- .../ui/screens/AbbreviationsScreen.kt | 113 +++++++++++------- .../dessalines/thumbkey/utils/Abbreviation.kt | 3 - .../thumbkey/utils/AbbreviationManager.kt | 17 +-- .../com/dessalines/thumbkey/utils/Utils.kt | 8 +- 8 files changed, 197 insertions(+), 139 deletions(-) delete mode 100644 app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt diff --git a/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt b/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt index a70859a70..ce8856c6a 100644 --- a/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt +++ b/app/src/main/java/com/dessalines/thumbkey/MainActivity.kt @@ -26,9 +26,9 @@ import com.dessalines.thumbkey.ui.components.settings.SettingsScreen import com.dessalines.thumbkey.ui.components.settings.about.AboutScreen import com.dessalines.thumbkey.ui.components.settings.backupandrestore.BackupAndRestoreScreen import com.dessalines.thumbkey.ui.components.settings.behavior.BehaviorScreen -import com.dessalines.thumbkey.ui.screens.AbbreviationsScreen import com.dessalines.thumbkey.ui.components.settings.lookandfeel.LookAndFeelScreen import com.dessalines.thumbkey.ui.components.setup.SetupScreen +import com.dessalines.thumbkey.ui.screens.AbbreviationsScreen import com.dessalines.thumbkey.ui.theme.ThumbkeyTheme import com.dessalines.thumbkey.utils.ANIMATION_SPEED import com.dessalines.thumbkey.utils.getImeNames diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt b/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt index 188335ca6..da5c25804 100644 --- a/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt +++ b/app/src/main/java/com/dessalines/thumbkey/db/AbbreviationRepository.kt @@ -7,11 +7,17 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch -class AbbreviationRepository(private val abbreviationDao: AbbreviationDao) { +class AbbreviationRepository( + private val abbreviationDao: AbbreviationDao, +) { val allAbbreviations: LiveData> = abbreviationDao.getAllAbbreviations() @WorkerThread - suspend fun insertOrUpdate(abbr: String, expansion: String, id: Int? = null) { + suspend fun insertOrUpdate( + abbr: String, + expansion: String, + id: Int? = null, + ) { if (id != null) { abbreviationDao.update(id, abbr, expansion) } else { @@ -25,28 +31,33 @@ class AbbreviationRepository(private val abbreviationDao: AbbreviationDao) { } @WorkerThread - fun getAbbreviation(abbr: String): Abbreviation? { - return abbreviationDao.getAbbreviation(abbr) - } + fun getAbbreviation(abbr: String): Abbreviation? = abbreviationDao.getAbbreviation(abbr) } -class AbbreviationViewModel(private val repository: AbbreviationRepository) : ViewModel() { +class AbbreviationViewModel( + private val repository: AbbreviationRepository, +) : ViewModel() { val allAbbreviations: LiveData> = repository.allAbbreviations - fun insertOrUpdate(abbr: String, expansion: String, id: Int? = null) = viewModelScope.launch { + fun insertOrUpdate( + abbr: String, + expansion: String, + id: Int? = null, + ) = viewModelScope.launch { repository.insertOrUpdate(abbr, expansion, id) } - fun delete(id: Int) = viewModelScope.launch { - repository.delete(id) - } + fun delete(id: Int) = + viewModelScope.launch { + repository.delete(id) + } - fun getAbbreviation(abbr: String): Abbreviation? { - return repository.getAbbreviation(abbr) - } + fun getAbbreviation(abbr: String): Abbreviation? = repository.getAbbreviation(abbr) } -class AbbreviationViewModelFactory(private val repository: AbbreviationRepository) : ViewModelProvider.Factory { +class AbbreviationViewModelFactory( + private val repository: AbbreviationRepository, +) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { if (modelClass.isAssignableFrom(AbbreviationViewModel::class.java)) { @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt index e8f75b8f0..8af8a982c 100644 --- a/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt +++ b/app/src/main/java/com/dessalines/thumbkey/db/AppDb.kt @@ -356,10 +356,17 @@ interface AbbreviationDao { suspend fun getAbbreviationAsync(abbr: String): Abbreviation? @Query("UPDATE Abbreviation SET abbreviation = :newAbbr, expansion = :expansion WHERE id = :id") - suspend fun update(id: Int, newAbbr: String, expansion: String) + suspend fun update( + id: Int, + newAbbr: String, + expansion: String, + ) @Query("INSERT INTO Abbreviation (abbreviation, expansion) VALUES (:abbr, :expansion)") - suspend fun insert(abbr: String, expansion: String) + suspend fun insert( + abbr: String, + expansion: String, + ) @Query("DELETE FROM Abbreviation WHERE id = :id") suspend fun deleteById(id: Int) @@ -612,7 +619,7 @@ val MIGRATION_16_17 = abbreviation TEXT PRIMARY KEY NOT NULL, expansion TEXT NOT NULL ) - """ + """, ) } } @@ -624,30 +631,35 @@ data class Abbreviation( @ColumnInfo(name = "expansion") val expansion: String, ) -val MIGRATION_17_18 = object : Migration(17, 18) { - override fun migrate(db: SupportSQLiteDatabase) { - // Create a temporary table with the new schema - db.execSQL(""" +val MIGRATION_17_18 = + object : Migration(17, 18) { + override fun migrate(db: SupportSQLiteDatabase) { + // Create a temporary table with the new schema + db.execSQL( + """ CREATE TABLE IF NOT EXISTS Abbreviation_temp ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, abbreviation TEXT NOT NULL, expansion TEXT NOT NULL ) - """) - - // Copy data from the old table to the new table - db.execSQL(""" + """, + ) + + // Copy data from the old table to the new table + db.execSQL( + """ INSERT INTO Abbreviation_temp (abbreviation, expansion) SELECT abbreviation, expansion FROM Abbreviation - """) - - // Drop the old table - db.execSQL("DROP TABLE Abbreviation") - - // Rename the temporary table to the original name - db.execSQL("ALTER TABLE Abbreviation_temp RENAME TO Abbreviation") + """, + ) + + // Drop the old table + db.execSQL("DROP TABLE Abbreviation") + + // Rename the temporary table to the original name + db.execSQL("ALTER TABLE Abbreviation_temp RENAME TO Abbreviation") + } } -} @Database( version = 18, @@ -656,6 +668,7 @@ val MIGRATION_17_18 = object : Migration(17, 18) { ) abstract class AppDB : RoomDatabase() { abstract fun appSettingsDao(): AppSettingsDao + abstract fun abbreviationDao(): AbbreviationDao companion object { @@ -691,8 +704,7 @@ abstract class AppDB : RoomDatabase() { MIGRATION_15_16, MIGRATION_16_17, MIGRATION_17_18, - ) - .fallbackToDestructiveMigration() + ).fallbackToDestructiveMigration() // Necessary because it can't insert data on creation .addCallback( object : Callback() { diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt index f7b7fbb13..19c43faab 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -65,6 +64,7 @@ import com.dessalines.thumbkey.keyboards.EMOJI_BACK_KEY_ITEM import com.dessalines.thumbkey.keyboards.KB_EN_THUMBKEY_MAIN import com.dessalines.thumbkey.keyboards.NUMERIC_KEY_ITEM import com.dessalines.thumbkey.keyboards.RETURN_KEY_ITEM +import com.dessalines.thumbkey.utils.AbbreviationManager import com.dessalines.thumbkey.utils.CircularDragAction import com.dessalines.thumbkey.utils.KeyAction import com.dessalines.thumbkey.utils.KeyboardLayout @@ -72,9 +72,8 @@ import com.dessalines.thumbkey.utils.KeyboardMode import com.dessalines.thumbkey.utils.KeyboardPosition import com.dessalines.thumbkey.utils.TAG import com.dessalines.thumbkey.utils.getKeyboardMode -import com.dessalines.thumbkey.utils.AbbreviationManager -import com.dessalines.thumbkey.utils.performKeyAction import com.dessalines.thumbkey.utils.keyboardPositionToAlignment +import com.dessalines.thumbkey.utils.performKeyAction import com.dessalines.thumbkey.utils.toBool import kotlin.time.TimeMark @@ -88,10 +87,11 @@ fun KeyboardScreen( val abbreviationManager = remember { AbbreviationManager(ctx.applicationContext) } var mode by remember { - val startMode = getKeyboardMode( - ime = ctx, - autoCapitalize = settings?.autoCapitalize?.toBool() ?: false, - ) + val startMode = + getKeyboardMode( + ime = ctx, + autoCapitalize = settings?.autoCapitalize?.toBool() ?: false, + ) Log.d(TAG, "KeyboardScreen: Initial keyboard mode: $startMode") mutableStateOf(startMode) } @@ -235,16 +235,22 @@ fun KeyboardScreen( autoCapitalize = autoCapitalize, keyboardSettings = keyboardDefinition.settings, onToggleShiftMode = { enable -> - mode = if (enable) KeyboardMode.SHIFTED else { - capsLock = false - KeyboardMode.MAIN - } + mode = + if (enable) { + KeyboardMode.SHIFTED + } else { + capsLock = false + KeyboardMode.MAIN + } }, onToggleNumericMode = { enable -> - mode = if (enable) KeyboardMode.NUMERIC else { - capsLock = false - KeyboardMode.MAIN - } + mode = + if (enable) { + KeyboardMode.NUMERIC + } else { + capsLock = false + KeyboardMode.MAIN + } }, onToggleEmojiMode = { enable -> mode = if (enable) KeyboardMode.EMOJI else KeyboardMode.MAIN @@ -304,38 +310,38 @@ fun KeyboardScreen( slideCursorMovementMode = slideCursorMovementMode, slideSpacebarDeadzoneEnabled = slideSpacebarDeadzoneEnabled, slideBackspaceDeadzoneEnabled = slideBackspaceDeadzoneEnabled, - onToggleShiftMode = { enable -> - Log.d(TAG, "KeyboardScreen: Toggling shift mode, enable: $enable") - mode = - if (enable) { - KeyboardMode.SHIFTED - } else { - capsLock = false - KeyboardMode.MAIN - } - Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") - }, - onToggleNumericMode = { enable -> - Log.d(TAG, "KeyboardScreen: Toggling numeric mode, enable: $enable") - mode = - if (enable) { - KeyboardMode.NUMERIC - } else { - capsLock = false - KeyboardMode.MAIN - } - Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") - }, - onToggleEmojiMode = { enable -> - Log.d(TAG, "KeyboardScreen: Toggling emoji mode, enable: $enable") - mode = - if (enable) { - KeyboardMode.EMOJI - } else { - KeyboardMode.MAIN - } - Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") - }, + onToggleShiftMode = { enable -> + Log.d(TAG, "KeyboardScreen: Toggling shift mode, enable: $enable") + mode = + if (enable) { + KeyboardMode.SHIFTED + } else { + capsLock = false + KeyboardMode.MAIN + } + Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") + }, + onToggleNumericMode = { enable -> + Log.d(TAG, "KeyboardScreen: Toggling numeric mode, enable: $enable") + mode = + if (enable) { + KeyboardMode.NUMERIC + } else { + capsLock = false + KeyboardMode.MAIN + } + Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") + }, + onToggleEmojiMode = { enable -> + Log.d(TAG, "KeyboardScreen: Toggling emoji mode, enable: $enable") + mode = + if (enable) { + KeyboardMode.EMOJI + } else { + KeyboardMode.MAIN + } + Log.d(TAG, "KeyboardScreen: New keyboard mode: $mode") + }, onToggleCapsLock = { capsLock = !capsLock }, diff --git a/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt b/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt index 76c6f211a..77c7e4778 100644 --- a/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt +++ b/app/src/main/java/com/dessalines/thumbkey/ui/screens/AbbreviationsScreen.kt @@ -1,18 +1,36 @@ package com.dessalines.thumbkey.ui.screens -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.* +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -21,20 +39,26 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import com.dessalines.thumbkey.R -import com.dessalines.thumbkey.db.* +import com.dessalines.thumbkey.db.Abbreviation +import com.dessalines.thumbkey.db.AbbreviationRepository +import com.dessalines.thumbkey.db.AbbreviationViewModel +import com.dessalines.thumbkey.db.AbbreviationViewModelFactory +import com.dessalines.thumbkey.db.AppDB import com.dessalines.thumbkey.utils.SimpleTopAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable fun AbbreviationsScreen( navController: NavController, - abbreviationViewModel: AbbreviationViewModel = viewModel( - factory = AbbreviationViewModelFactory( - AbbreviationRepository( - AppDB.getDatabase(LocalContext.current).abbreviationDao() - ) - ) - ) + abbreviationViewModel: AbbreviationViewModel = + viewModel( + factory = + AbbreviationViewModelFactory( + AbbreviationRepository( + AppDB.getDatabase(LocalContext.current).abbreviationDao(), + ), + ), + ), ) { var showAddDialog by remember { mutableStateOf(false) } var abbreviationToEdit by remember { mutableStateOf(null) } @@ -43,36 +67,37 @@ fun AbbreviationsScreen( topBar = { SimpleTopAppBar( text = stringResource(R.string.abbreviations), - navController = navController + navController = navController, ) }, floatingActionButton = { FloatingActionButton(onClick = { showAddDialog = true }) { Icon(Icons.Default.Add, contentDescription = "Add abbreviation") } - } + }, ) { padding -> Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - .padding(16.dp) + modifier = + Modifier + .fillMaxSize() + .padding(padding) + .padding(16.dp), ) { Text( text = stringResource(R.string.abbreviations_description), style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(bottom = 16.dp) + modifier = Modifier.padding(bottom = 16.dp), ) val abbreviations by abbreviationViewModel.allAbbreviations.observeAsState(initial = emptyList()) LazyColumn( - contentPadding = PaddingValues(bottom = 80.dp) // Add padding for FAB + contentPadding = PaddingValues(bottom = 80.dp), // Add padding for FAB ) { items(abbreviations) { abbreviation -> AbbreviationItem( abbreviation = abbreviation, onEdit = { abbreviationToEdit = it }, - onDelete = { abbreviationViewModel.delete(it.id) } + onDelete = { abbreviationViewModel.delete(it.id) }, ) } } @@ -85,7 +110,7 @@ fun AbbreviationsScreen( onSave = { abbr, expansion -> abbreviationViewModel.insertOrUpdate(abbr, expansion) showAddDialog = false - } + }, ) } @@ -96,7 +121,7 @@ fun AbbreviationsScreen( onSave = { abbr, expansion -> abbreviationViewModel.insertOrUpdate(abbr, expansion, abbreviation.id) abbreviationToEdit = null - } + }, ) } } @@ -107,29 +132,31 @@ fun AbbreviationsScreen( fun AbbreviationItem( abbreviation: Abbreviation, onEdit: (Abbreviation) -> Unit, - onDelete: (Abbreviation) -> Unit + onDelete: (Abbreviation) -> Unit, ) { Card( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - onClick = { onEdit(abbreviation) } + modifier = + Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + onClick = { onEdit(abbreviation) }, ) { Row( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), + modifier = + Modifier + .padding(16.dp) + .fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { Column { Text( text = abbreviation.abbreviation, - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, ) Text( text = abbreviation.expansion, - style = MaterialTheme.typography.bodyMedium + style = MaterialTheme.typography.bodyMedium, ) } IconButton(onClick = { onDelete(abbreviation) }) { @@ -143,7 +170,7 @@ fun AbbreviationItem( fun AddEditAbbreviationDialog( abbreviation: Abbreviation?, onDismiss: () -> Unit, - onSave: (String, String) -> Unit + onSave: (String, String) -> Unit, ) { var abbr by remember { mutableStateOf(abbreviation?.abbreviation ?: "") } var expansion by remember { mutableStateOf(abbreviation?.expansion ?: "") } @@ -152,10 +179,12 @@ fun AddEditAbbreviationDialog( onDismissRequest = onDismiss, title = { Text( - text = if (abbreviation == null) - stringResource(R.string.add_abbreviation) - else - stringResource(R.string.edit_abbreviation) + text = + if (abbreviation == null) { + stringResource(R.string.add_abbreviation) + } else { + stringResource(R.string.edit_abbreviation) + }, ) }, text = { @@ -165,7 +194,7 @@ fun AddEditAbbreviationDialog( onValueChange = { abbr = it }, label = { Text(stringResource(R.string.abbreviation)) }, singleLine = true, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( @@ -173,7 +202,7 @@ fun AddEditAbbreviationDialog( onValueChange = { expansion = it }, label = { Text(stringResource(R.string.expansion)) }, singleLine = true, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), ) } }, @@ -183,7 +212,7 @@ fun AddEditAbbreviationDialog( if (abbr.isNotBlank() && expansion.isNotBlank()) { onSave(abbr, expansion) } - } + }, ) { Text(stringResource(R.string.save)) } @@ -192,6 +221,6 @@ fun AddEditAbbreviationDialog( TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } - } + }, ) } diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt deleted file mode 100644 index 51286be9a..000000000 --- a/app/src/main/java/com/dessalines/thumbkey/utils/Abbreviation.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.dessalines.thumbkey.utils - -// This file is kept for future use diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt b/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt index 0e7cbf9b8..4d3346498 100644 --- a/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt +++ b/app/src/main/java/com/dessalines/thumbkey/utils/AbbreviationManager.kt @@ -7,17 +7,20 @@ import com.dessalines.thumbkey.db.AppDB private const val ABBR_TAG = "AbbreviationManager" -class AbbreviationManager(private val context: Context) { +class AbbreviationManager( + private val context: Context, +) { init { Log.d(ABBR_TAG, "Initializing AbbreviationManager") } - private val abbreviationDao: AbbreviationDao = run { - Log.d(ABBR_TAG, "Creating AbbreviationDao") - AppDB.getDatabase(context).abbreviationDao().also { - Log.d(ABBR_TAG, "AbbreviationDao created successfully") + private val abbreviationDao: AbbreviationDao = + run { + Log.d(ABBR_TAG, "Creating AbbreviationDao") + AppDB.getDatabase(context).abbreviationDao().also { + Log.d(ABBR_TAG, "AbbreviationDao created successfully") + } } - } @Synchronized fun checkAndExpand(text: String): Pair { @@ -33,7 +36,7 @@ class AbbreviationManager(private val context: Context) { Log.d(ABBR_TAG, "checkAndExpand: no words found") return Pair(false, text) } - + val lastWord = words.last() Log.d(ABBR_TAG, "checkAndExpand: lastWord = '$lastWord'") if (lastWord.isEmpty()) { diff --git a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt index 15016c175..08dc1ed9e 100644 --- a/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt +++ b/app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt @@ -339,18 +339,18 @@ fun performKeyAction( val text = action.text Log.d(TAG, "committing key text: $text") ime.ignoreNextCursorMove() - + if (text == " ") { val currentText = ime.currentInputConnection.getTextBeforeCursor(1000, 0)?.toString() if (currentText != null) { val abbreviationManager = AbbreviationManager(ime.applicationContext) val (shouldExpand, expandedText) = abbreviationManager.checkAndExpand(currentText) - + if (shouldExpand) { // Delete the abbreviation val lastWord = currentText.split(Regex("[ \n]")).last() ime.currentInputConnection.deleteSurroundingText(lastWord.length, 0) - + // Insert the expansion and a space ime.currentInputConnection.commitText(expandedText + " ", 1) } else { @@ -1111,7 +1111,7 @@ fun getKeyboardMode( val shouldAutoCapitalize = autoCapitalizeCheck(ime) Log.d(TAG, "getKeyboardMode: Is URI/Email/Password field: $isUriOrEmail") Log.d(TAG, "getKeyboardMode: Should auto capitalize: $shouldAutoCapitalize") - + if (autoCapitalize && !isUriOrEmail && shouldAutoCapitalize) { Log.d(TAG, "getKeyboardMode: Setting SHIFTED mode due to auto capitalize") KeyboardMode.SHIFTED