diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/data/AppSettings.kt b/app/src/main/java/com/rickyhu/hushkeyboard/data/AppSettings.kt index 5446521..c1e3627 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/data/AppSettings.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/data/AppSettings.kt @@ -5,9 +5,10 @@ import kotlinx.serialization.Serializable @Serializable data class AppSettings( val themeOption: ThemeOption = ThemeOption.System, + val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW, + val smartDelete: Boolean = true, val addSpaceAfterNotation: Boolean = true, - val vibrateOnTap: Boolean = true, - val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW + val vibrateOnTap: Boolean = true ) enum class ThemeOption { System, Light, Dark } diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/data/SettingsRepository.kt b/app/src/main/java/com/rickyhu/hushkeyboard/data/SettingsRepository.kt index f627508..d0489a9 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/data/SettingsRepository.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/data/SettingsRepository.kt @@ -22,6 +22,10 @@ class SettingsRepository @Inject constructor( dataStore.updateData { it.copy(wideNotationOption = wideNotationOption) } } + suspend fun updateSmartDelete(smartDelete: Boolean) { + dataStore.updateData { it.copy(smartDelete = smartDelete) } + } + suspend fun updateAddSpaceBetweenNotation(addSpaceBetweenNotation: Boolean) { dataStore.updateData { it.copy(addSpaceAfterNotation = addSpaceBetweenNotation) } } diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/HushKeyboardView.kt b/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/HushKeyboardView.kt index 81e9758..bb9fa7d 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/HushKeyboardView.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/HushKeyboardView.kt @@ -3,6 +3,7 @@ package com.rickyhu.hushkeyboard.keyboard import android.content.Context import android.os.Build import android.os.VibratorManager +import android.util.Log import androidx.annotation.RequiresApi import androidx.annotation.VisibleForTesting import androidx.compose.foundation.background @@ -32,11 +33,15 @@ import com.rickyhu.hushkeyboard.service.HushIMEService import com.rickyhu.hushkeyboard.theme.DarkBackground import com.rickyhu.hushkeyboard.theme.LightBackground import com.rickyhu.hushkeyboard.utils.deleteText +import com.rickyhu.hushkeyboard.utils.inputNewline import com.rickyhu.hushkeyboard.utils.inputText import com.rickyhu.hushkeyboard.utils.maybeVibrate +import com.rickyhu.hushkeyboard.utils.smartDelete import com.rickyhu.hushkeyboard.utils.toInputConnection import splitties.systemservices.inputMethodManager +private const val TAG = "HushKeyboardView" + class HushKeyboardView(context: Context) : AbstractComposeView(context) { @RequiresApi(Build.VERSION_CODES.S) @@ -80,6 +85,7 @@ fun HushKeyboardContent(state: KeyboardState) { addSpaceAfterNotation = state.addSpaceAfterNotation, wideNotationOption = state.wideNotationOption, onTextInput = { + Log.d(TAG, "Notation key tapped") context.toInputConnection().inputText(it) if (state.vibrateOnTap) vibratorManager?.maybeVibrate() } @@ -99,17 +105,21 @@ fun HushKeyboardContent(state: KeyboardState) { ControlKeyButtonRow( turns = keyConfigState.turns, isDarkTheme = isDarkTheme, + smartDelete = state.smartDelete, inputMethodButtonAction = { + Log.d(TAG, "Input method picker tapped") inputMethodManager.showInputMethodPicker() if (state.vibrateOnTap) vibratorManager?.maybeVibrate() }, rotateDirectionButtonAction = { + Log.d(TAG, "Rotate direction button tapped") keyConfigState = keyConfigState.copy( isCounterClockwise = !keyConfigState.isCounterClockwise ) if (state.vibrateOnTap) vibratorManager?.maybeVibrate() }, turnDegreeButtonAction = { + Log.d(TAG, "Turn degree button tapped") keyConfigState = when (keyConfigState.turns) { Turns.Single -> keyConfigState.copy(turns = Turns.Double) Turns.Double -> keyConfigState.copy(turns = Turns.Triple) @@ -118,17 +128,28 @@ fun HushKeyboardContent(state: KeyboardState) { if (state.vibrateOnTap) vibratorManager?.maybeVibrate() }, wideTurnButtonAction = { + Log.d(TAG, "Wide turn button tapped") keyConfigState = keyConfigState.copy( isWideTurn = !keyConfigState.isWideTurn ) if (state.vibrateOnTap) vibratorManager?.maybeVibrate() }, - deleteButtonAction = { - context.toInputConnection().deleteText() - if (state.vibrateOnTap) vibratorManager?.maybeVibrate() + deleteButtonAction = if (state.smartDelete) { + { + Log.d(TAG, "Delete button tapped") + context.toInputConnection().smartDelete() + if (state.vibrateOnTap) vibratorManager?.maybeVibrate() + } + } else { + { + Log.d(TAG, "Smart delete button tapped") + context.toInputConnection().deleteText() + if (state.vibrateOnTap) vibratorManager?.maybeVibrate() + } }, newLineButtonAction = { - context.toInputConnection().inputText("\n") + Log.d(TAG, "New line button tapped") + context.toInputConnection().inputNewline() if (state.vibrateOnTap) vibratorManager?.maybeVibrate() } ) @@ -142,9 +163,10 @@ fun HushKeyboardPreview() { HushKeyboardContent( state = KeyboardState( themeOption = ThemeOption.System, + wideNotationOption = WideNotationOption.WideWithW, + smartDelete = true, addSpaceAfterNotation = true, - vibrateOnTap = true, - wideNotationOption = WideNotationOption.WideWithW + vibrateOnTap = true ) ) } diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/KeyboardViewModel.kt b/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/KeyboardViewModel.kt index 962f311..14b3b5c 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/KeyboardViewModel.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/KeyboardViewModel.kt @@ -22,16 +22,18 @@ class KeyboardViewModel @Inject constructor( ).map { settings -> KeyboardState( themeOption = settings.themeOption, + wideNotationOption = settings.wideNotationOption, + smartDelete = settings.smartDelete, addSpaceAfterNotation = settings.addSpaceAfterNotation, - vibrateOnTap = settings.vibrateOnTap, - wideNotationOption = settings.wideNotationOption + vibrateOnTap = settings.vibrateOnTap ) } } data class KeyboardState( val themeOption: ThemeOption = ThemeOption.System, + val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW, + val smartDelete: Boolean = true, val addSpaceAfterNotation: Boolean = true, - val vibrateOnTap: Boolean = true, - val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW + val vibrateOnTap: Boolean = true ) diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/ui/rows/ControlKeyButtonRow.kt b/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/ui/rows/ControlKeyButtonRow.kt index e89900f..4becd37 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/ui/rows/ControlKeyButtonRow.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/keyboard/ui/rows/ControlKeyButtonRow.kt @@ -26,6 +26,7 @@ fun ControlKeyButtonRow( modifier: Modifier = Modifier, turns: Turns, isDarkTheme: Boolean, + smartDelete: Boolean, inputMethodButtonAction: () -> Unit, rotateDirectionButtonAction: () -> Unit, turnDegreeButtonAction: () -> Unit, @@ -94,16 +95,20 @@ fun ControlKeyButtonRow( ) } ) + ControlKeyButton( modifier = controlKeyModifier.testTag("DeleteButton"), onClick = deleteButtonAction, isDarkTheme = isDarkTheme, content = { - Text( - "⌫", - color = keyColor, - fontSize = 18.sp, - textAlign = TextAlign.Center + Icon( + painter = if (smartDelete) { + painterResource(R.drawable.ic_backspace_filled) + } else { + painterResource(R.drawable.ic_backspace_outlined) + }, + tint = keyColor, + contentDescription = "Delete" ) } ) @@ -129,6 +134,7 @@ private fun ControlKeyButtonRowPreview() { ControlKeyButtonRow( turns = Turns.Single, isDarkTheme = false, + smartDelete = true, inputMethodButtonAction = {}, rotateDirectionButtonAction = {}, turnDegreeButtonAction = {}, diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/model/Notation.kt b/app/src/main/java/com/rickyhu/hushkeyboard/model/Notation.kt index 9f5f7ac..30ac0d1 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/model/Notation.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/model/Notation.kt @@ -12,5 +12,9 @@ enum class Notation(val value: String) { S("S"), X("x"), Y("y"), - Z("z") + Z("z"); + + companion object { + fun getCharList(): List = Notation.entries.map { it.value.single() } + } } diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsScreen.kt b/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsScreen.kt index 10ca98b..89a0c33 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsScreen.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsScreen.kt @@ -20,6 +20,7 @@ import com.rickyhu.hushkeyboard.data.ThemeOption import com.rickyhu.hushkeyboard.data.WideNotationOption import com.rickyhu.hushkeyboard.settings.ui.AddSpaceBetweenNotationSwitchItem import com.rickyhu.hushkeyboard.settings.ui.AppVersionItem +import com.rickyhu.hushkeyboard.settings.ui.SmartDeleteSwitchItem import com.rickyhu.hushkeyboard.settings.ui.ThemeOptionDropdownItem import com.rickyhu.hushkeyboard.settings.ui.VibrateOnTapSwitchItem import com.rickyhu.hushkeyboard.settings.ui.WideNotationOptionDropdownItem @@ -35,6 +36,7 @@ fun SettingsScreen( state, onThemeSelected = viewModel::updateThemeOption, onWideNotationOptionSelected = viewModel::updateWideNotationOption, + onSmartDeleteChanged = viewModel::updateSmartDelete, onAddSpaceBetweenNotationChanged = viewModel::updateAddSpaceBetweenNotation, onVibrateOnTapChanged = viewModel::updateVibrateOnTap ) @@ -47,6 +49,7 @@ fun SettingsContent( state: SettingsState, onThemeSelected: (themeOption: ThemeOption) -> Unit, onWideNotationOptionSelected: (wideNotationOption: WideNotationOption) -> Unit, + onSmartDeleteChanged: (smartDelete: Boolean) -> Unit, onAddSpaceBetweenNotationChanged: (addSpaceAfterNotation: Boolean) -> Unit, onVibrateOnTapChanged: (vibrateOnTap: Boolean) -> Unit ) { @@ -66,6 +69,10 @@ fun SettingsContent( currentOption = state.wideNotationOption, onOptionSelected = onWideNotationOptionSelected ) + SmartDeleteSwitchItem( + value = state.smartDelete, + onValueChanged = onSmartDeleteChanged + ) AddSpaceBetweenNotationSwitchItem( value = state.addSpaceAfterNotation, onValueChanged = onAddSpaceBetweenNotationChanged diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsViewModel.kt b/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsViewModel.kt index 1aae40b..9fb0d4b 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsViewModel.kt @@ -26,6 +26,7 @@ class SettingsViewModel @Inject constructor( SettingsState( themeOption = settings.themeOption, addSpaceAfterNotation = settings.addSpaceAfterNotation, + smartDelete = settings.smartDelete, vibrateOnTap = settings.vibrateOnTap, wideNotationOption = settings.wideNotationOption ) @@ -43,6 +44,12 @@ class SettingsViewModel @Inject constructor( } } + fun updateSmartDelete(smartDelete: Boolean) { + viewModelScope.launch { + settingsRepository.updateSmartDelete(smartDelete) + } + } + fun updateAddSpaceBetweenNotation(addSpaceBetweenNotation: Boolean) { viewModelScope.launch { settingsRepository.updateAddSpaceBetweenNotation(addSpaceBetweenNotation) @@ -58,7 +65,8 @@ class SettingsViewModel @Inject constructor( data class SettingsState( val themeOption: ThemeOption = ThemeOption.System, + val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW, + val smartDelete: Boolean = true, val addSpaceAfterNotation: Boolean = true, - val vibrateOnTap: Boolean = true, - val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW + val vibrateOnTap: Boolean = true ) diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/settings/ui/SmartDeleteSwitchItem.kt b/app/src/main/java/com/rickyhu/hushkeyboard/settings/ui/SmartDeleteSwitchItem.kt new file mode 100644 index 0000000..dd3f19f --- /dev/null +++ b/app/src/main/java/com/rickyhu/hushkeyboard/settings/ui/SmartDeleteSwitchItem.kt @@ -0,0 +1,48 @@ +package com.rickyhu.hushkeyboard.settings.ui + +import androidx.compose.foundation.clickable +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import com.rickyhu.hushkeyboard.R +import com.rickyhu.hushkeyboard.theme.HushKeyboardTheme + +@Composable +fun SmartDeleteSwitchItem( + value: Boolean, + onValueChanged: (Boolean) -> Unit = {} +) { + ListItem( + modifier = Modifier.clickable { onValueChanged(!value) }, + headlineContent = { Text("Smart Delete") }, + leadingContent = { + Icon( + painter = painterResource(R.drawable.ic_backspace_filled), + contentDescription = "Delete" + ) + }, + trailingContent = { + Switch( + checked = value, + onCheckedChange = onValueChanged, + modifier = Modifier.testTag("SmartDeleteSwitchItem") + ) + } + ) +} + +@Preview(showBackground = true) +@Composable +fun SmartDeleteSwitchItemPreview() { + HushKeyboardTheme { + AddSpaceBetweenNotationSwitchItem( + value = true + ) + } +} diff --git a/app/src/main/java/com/rickyhu/hushkeyboard/utils/InputConnection.kt b/app/src/main/java/com/rickyhu/hushkeyboard/utils/InputConnection.kt index 6300864..7a9d1c7 100644 --- a/app/src/main/java/com/rickyhu/hushkeyboard/utils/InputConnection.kt +++ b/app/src/main/java/com/rickyhu/hushkeyboard/utils/InputConnection.kt @@ -1,15 +1,27 @@ package com.rickyhu.hushkeyboard.utils import android.content.Context +import android.util.Log import android.view.inputmethod.InputConnection +import com.rickyhu.hushkeyboard.model.Notation import com.rickyhu.hushkeyboard.service.HushIMEService -private const val CURSOR_POSITION = 1 +private const val TAG = "InputConnection" +private const val NEWLINE = '\n' +private const val END_CURSOR_POSITION = 1 +private const val SCAN_WINDOW_SIZE = 50 + +val notationCharList = Notation.getCharList() + listOf(NEWLINE) fun Context.toInputConnection(): InputConnection = (this as HushIMEService).currentInputConnection fun InputConnection.inputText(text: String) { - commitText(text, CURSOR_POSITION) + commitText(text, END_CURSOR_POSITION) + Log.d(TAG, "inputText $text") +} + +fun InputConnection.inputNewline() { + inputText(NEWLINE.toString()) } fun InputConnection.deleteText() { @@ -17,7 +29,45 @@ fun InputConnection.deleteText() { if (selectedText.isNullOrEmpty()) { deleteSurroundingText(1, 0) + Log.d(TAG, "deleteText") } else { - commitText("", CURSOR_POSITION) + commitText("", END_CURSOR_POSITION) + Log.d(TAG, "delete selected text: $selectedText") + } +} + +fun InputConnection.smartDelete() { + Log.d(TAG, "smartDelete") + + val selectedText = getSelectedText(0) + if (!selectedText.isNullOrEmpty()) { + commitText("", END_CURSOR_POSITION) + Log.d(TAG, "delete selected text: $selectedText") + return + } + + beginBatchEdit() + try { + val textBeforeCursor = getTextBeforeCursor(SCAN_WINDOW_SIZE, 0) + + if (textBeforeCursor.isNullOrEmpty()) { + deleteSurroundingText(1, 0) + return + } + + var charsToDelete = 0 + for (i in textBeforeCursor.indices.reversed()) { + val char = textBeforeCursor[i] + charsToDelete++ + if (char.uppercaseChar() in notationCharList) { + break + } + } + + if (charsToDelete > 0) { + deleteSurroundingText(charsToDelete, 0) + } + } finally { + endBatchEdit() } } diff --git a/app/src/main/res/drawable/ic_backspace_filled.xml b/app/src/main/res/drawable/ic_backspace_filled.xml new file mode 100644 index 0000000..4069f83 --- /dev/null +++ b/app/src/main/res/drawable/ic_backspace_filled.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_backspace_outlined.xml b/app/src/main/res/drawable/ic_backspace_outlined.xml new file mode 100644 index 0000000..a014f24 --- /dev/null +++ b/app/src/main/res/drawable/ic_backspace_outlined.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/test/java/com/rickyhu/hushkeyboard/ui/SettingsScreenUiTest.kt b/app/src/test/java/com/rickyhu/hushkeyboard/ui/SettingsScreenUiTest.kt index 7ee57c2..6c8628d 100644 --- a/app/src/test/java/com/rickyhu/hushkeyboard/ui/SettingsScreenUiTest.kt +++ b/app/src/test/java/com/rickyhu/hushkeyboard/ui/SettingsScreenUiTest.kt @@ -27,6 +27,7 @@ class SettingsScreenUiTest { private var addSpaceAfterNotationSwitchValue = true private var vibrateOnTapSwitchValue = true + private var smartDeleteValue = true @Before fun setUp() { @@ -35,6 +36,9 @@ class SettingsScreenUiTest { state = SettingsState(), onThemeSelected = {}, onWideNotationOptionSelected = {}, + onSmartDeleteChanged = { value -> + smartDeleteValue = value + }, onAddSpaceBetweenNotationChanged = { value -> addSpaceAfterNotationSwitchValue = value },