Skip to content

Commit d57c20c

Browse files
authored
Implement smart delete (#55)
* ci: ๐ŸŽก ignore project coverage check * feat: ๐ŸŽธ replace delete text with icon * feat: ๐ŸŽธ add onLongPress action to toggle smart delete mode * feat: ๐ŸŽธ add smart delete to settings * feat: ๐ŸŽธ implement smart delete for deleting a notation * fix: ๐Ÿ› re-implement to resolve batchEdit crash * test: ๐Ÿ’ fix SettingsScreenUiTest * fix: ๐Ÿ› remove unused long press action to pass unit tests * ci: ๐ŸŽก remove text during rebase * fix: ๐Ÿ› resolve smart delete state management from dataStore โœ… Closes: \ * refactor: ๐Ÿ’ก default true for smart delete feature * chore: ๐Ÿค– add logs * refactor: ๐Ÿ’ก extract constants
1 parent 5201743 commit d57c20c

File tree

13 files changed

+189
-23
lines changed

13 files changed

+189
-23
lines changed

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/data/AppSettings.ktโ€Ž

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import kotlinx.serialization.Serializable
55
@Serializable
66
data class AppSettings(
77
val themeOption: ThemeOption = ThemeOption.System,
8+
val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW,
9+
val smartDelete: Boolean = true,
810
val addSpaceAfterNotation: Boolean = true,
9-
val vibrateOnTap: Boolean = true,
10-
val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW
11+
val vibrateOnTap: Boolean = true
1112
)
1213

1314
enum class ThemeOption { System, Light, Dark }

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/data/SettingsRepository.ktโ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class SettingsRepository @Inject constructor(
2222
dataStore.updateData { it.copy(wideNotationOption = wideNotationOption) }
2323
}
2424

25+
suspend fun updateSmartDelete(smartDelete: Boolean) {
26+
dataStore.updateData { it.copy(smartDelete = smartDelete) }
27+
}
28+
2529
suspend fun updateAddSpaceBetweenNotation(addSpaceBetweenNotation: Boolean) {
2630
dataStore.updateData { it.copy(addSpaceAfterNotation = addSpaceBetweenNotation) }
2731
}

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/keyboard/HushKeyboardView.ktโ€Ž

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.rickyhu.hushkeyboard.keyboard
33
import android.content.Context
44
import android.os.Build
55
import android.os.VibratorManager
6+
import android.util.Log
67
import androidx.annotation.RequiresApi
78
import androidx.annotation.VisibleForTesting
89
import androidx.compose.foundation.background
@@ -32,11 +33,15 @@ import com.rickyhu.hushkeyboard.service.HushIMEService
3233
import com.rickyhu.hushkeyboard.theme.DarkBackground
3334
import com.rickyhu.hushkeyboard.theme.LightBackground
3435
import com.rickyhu.hushkeyboard.utils.deleteText
36+
import com.rickyhu.hushkeyboard.utils.inputNewline
3537
import com.rickyhu.hushkeyboard.utils.inputText
3638
import com.rickyhu.hushkeyboard.utils.maybeVibrate
39+
import com.rickyhu.hushkeyboard.utils.smartDelete
3740
import com.rickyhu.hushkeyboard.utils.toInputConnection
3841
import splitties.systemservices.inputMethodManager
3942

43+
private const val TAG = "HushKeyboardView"
44+
4045
class HushKeyboardView(context: Context) : AbstractComposeView(context) {
4146

4247
@RequiresApi(Build.VERSION_CODES.S)
@@ -80,6 +85,7 @@ fun HushKeyboardContent(state: KeyboardState) {
8085
addSpaceAfterNotation = state.addSpaceAfterNotation,
8186
wideNotationOption = state.wideNotationOption,
8287
onTextInput = {
88+
Log.d(TAG, "Notation key tapped")
8389
context.toInputConnection().inputText(it)
8490
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
8591
}
@@ -99,17 +105,21 @@ fun HushKeyboardContent(state: KeyboardState) {
99105
ControlKeyButtonRow(
100106
turns = keyConfigState.turns,
101107
isDarkTheme = isDarkTheme,
108+
smartDelete = state.smartDelete,
102109
inputMethodButtonAction = {
110+
Log.d(TAG, "Input method picker tapped")
103111
inputMethodManager.showInputMethodPicker()
104112
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
105113
},
106114
rotateDirectionButtonAction = {
115+
Log.d(TAG, "Rotate direction button tapped")
107116
keyConfigState = keyConfigState.copy(
108117
isCounterClockwise = !keyConfigState.isCounterClockwise
109118
)
110119
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
111120
},
112121
turnDegreeButtonAction = {
122+
Log.d(TAG, "Turn degree button tapped")
113123
keyConfigState = when (keyConfigState.turns) {
114124
Turns.Single -> keyConfigState.copy(turns = Turns.Double)
115125
Turns.Double -> keyConfigState.copy(turns = Turns.Triple)
@@ -118,17 +128,28 @@ fun HushKeyboardContent(state: KeyboardState) {
118128
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
119129
},
120130
wideTurnButtonAction = {
131+
Log.d(TAG, "Wide turn button tapped")
121132
keyConfigState = keyConfigState.copy(
122133
isWideTurn = !keyConfigState.isWideTurn
123134
)
124135
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
125136
},
126-
deleteButtonAction = {
127-
context.toInputConnection().deleteText()
128-
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
137+
deleteButtonAction = if (state.smartDelete) {
138+
{
139+
Log.d(TAG, "Delete button tapped")
140+
context.toInputConnection().smartDelete()
141+
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
142+
}
143+
} else {
144+
{
145+
Log.d(TAG, "Smart delete button tapped")
146+
context.toInputConnection().deleteText()
147+
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
148+
}
129149
},
130150
newLineButtonAction = {
131-
context.toInputConnection().inputText("\n")
151+
Log.d(TAG, "New line button tapped")
152+
context.toInputConnection().inputNewline()
132153
if (state.vibrateOnTap) vibratorManager?.maybeVibrate()
133154
}
134155
)
@@ -142,9 +163,10 @@ fun HushKeyboardPreview() {
142163
HushKeyboardContent(
143164
state = KeyboardState(
144165
themeOption = ThemeOption.System,
166+
wideNotationOption = WideNotationOption.WideWithW,
167+
smartDelete = true,
145168
addSpaceAfterNotation = true,
146-
vibrateOnTap = true,
147-
wideNotationOption = WideNotationOption.WideWithW
169+
vibrateOnTap = true
148170
)
149171
)
150172
}

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/keyboard/KeyboardViewModel.ktโ€Ž

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,18 @@ class KeyboardViewModel @Inject constructor(
2222
).map { settings ->
2323
KeyboardState(
2424
themeOption = settings.themeOption,
25+
wideNotationOption = settings.wideNotationOption,
26+
smartDelete = settings.smartDelete,
2527
addSpaceAfterNotation = settings.addSpaceAfterNotation,
26-
vibrateOnTap = settings.vibrateOnTap,
27-
wideNotationOption = settings.wideNotationOption
28+
vibrateOnTap = settings.vibrateOnTap
2829
)
2930
}
3031
}
3132

3233
data class KeyboardState(
3334
val themeOption: ThemeOption = ThemeOption.System,
35+
val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW,
36+
val smartDelete: Boolean = true,
3437
val addSpaceAfterNotation: Boolean = true,
35-
val vibrateOnTap: Boolean = true,
36-
val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW
38+
val vibrateOnTap: Boolean = true
3739
)

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/keyboard/ui/rows/ControlKeyButtonRow.ktโ€Ž

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ fun ControlKeyButtonRow(
2626
modifier: Modifier = Modifier,
2727
turns: Turns,
2828
isDarkTheme: Boolean,
29+
smartDelete: Boolean,
2930
inputMethodButtonAction: () -> Unit,
3031
rotateDirectionButtonAction: () -> Unit,
3132
turnDegreeButtonAction: () -> Unit,
@@ -94,16 +95,20 @@ fun ControlKeyButtonRow(
9495
)
9596
}
9697
)
98+
9799
ControlKeyButton(
98100
modifier = controlKeyModifier.testTag("DeleteButton"),
99101
onClick = deleteButtonAction,
100102
isDarkTheme = isDarkTheme,
101103
content = {
102-
Text(
103-
"โŒซ",
104-
color = keyColor,
105-
fontSize = 18.sp,
106-
textAlign = TextAlign.Center
104+
Icon(
105+
painter = if (smartDelete) {
106+
painterResource(R.drawable.ic_backspace_filled)
107+
} else {
108+
painterResource(R.drawable.ic_backspace_outlined)
109+
},
110+
tint = keyColor,
111+
contentDescription = "Delete"
107112
)
108113
}
109114
)
@@ -129,6 +134,7 @@ private fun ControlKeyButtonRowPreview() {
129134
ControlKeyButtonRow(
130135
turns = Turns.Single,
131136
isDarkTheme = false,
137+
smartDelete = true,
132138
inputMethodButtonAction = {},
133139
rotateDirectionButtonAction = {},
134140
turnDegreeButtonAction = {},

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/model/Notation.ktโ€Ž

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,9 @@ enum class Notation(val value: String) {
1212
S("S"),
1313
X("x"),
1414
Y("y"),
15-
Z("z")
15+
Z("z");
16+
17+
companion object {
18+
fun getCharList(): List<Char> = Notation.entries.map { it.value.single() }
19+
}
1620
}

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsScreen.ktโ€Ž

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.rickyhu.hushkeyboard.data.ThemeOption
2020
import com.rickyhu.hushkeyboard.data.WideNotationOption
2121
import com.rickyhu.hushkeyboard.settings.ui.AddSpaceBetweenNotationSwitchItem
2222
import com.rickyhu.hushkeyboard.settings.ui.AppVersionItem
23+
import com.rickyhu.hushkeyboard.settings.ui.SmartDeleteSwitchItem
2324
import com.rickyhu.hushkeyboard.settings.ui.ThemeOptionDropdownItem
2425
import com.rickyhu.hushkeyboard.settings.ui.VibrateOnTapSwitchItem
2526
import com.rickyhu.hushkeyboard.settings.ui.WideNotationOptionDropdownItem
@@ -35,6 +36,7 @@ fun SettingsScreen(
3536
state,
3637
onThemeSelected = viewModel::updateThemeOption,
3738
onWideNotationOptionSelected = viewModel::updateWideNotationOption,
39+
onSmartDeleteChanged = viewModel::updateSmartDelete,
3840
onAddSpaceBetweenNotationChanged = viewModel::updateAddSpaceBetweenNotation,
3941
onVibrateOnTapChanged = viewModel::updateVibrateOnTap
4042
)
@@ -47,6 +49,7 @@ fun SettingsContent(
4749
state: SettingsState,
4850
onThemeSelected: (themeOption: ThemeOption) -> Unit,
4951
onWideNotationOptionSelected: (wideNotationOption: WideNotationOption) -> Unit,
52+
onSmartDeleteChanged: (smartDelete: Boolean) -> Unit,
5053
onAddSpaceBetweenNotationChanged: (addSpaceAfterNotation: Boolean) -> Unit,
5154
onVibrateOnTapChanged: (vibrateOnTap: Boolean) -> Unit
5255
) {
@@ -66,6 +69,10 @@ fun SettingsContent(
6669
currentOption = state.wideNotationOption,
6770
onOptionSelected = onWideNotationOptionSelected
6871
)
72+
SmartDeleteSwitchItem(
73+
value = state.smartDelete,
74+
onValueChanged = onSmartDeleteChanged
75+
)
6976
AddSpaceBetweenNotationSwitchItem(
7077
value = state.addSpaceAfterNotation,
7178
onValueChanged = onAddSpaceBetweenNotationChanged

โ€Žapp/src/main/java/com/rickyhu/hushkeyboard/settings/SettingsViewModel.ktโ€Ž

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class SettingsViewModel @Inject constructor(
2626
SettingsState(
2727
themeOption = settings.themeOption,
2828
addSpaceAfterNotation = settings.addSpaceAfterNotation,
29+
smartDelete = settings.smartDelete,
2930
vibrateOnTap = settings.vibrateOnTap,
3031
wideNotationOption = settings.wideNotationOption
3132
)
@@ -43,6 +44,12 @@ class SettingsViewModel @Inject constructor(
4344
}
4445
}
4546

47+
fun updateSmartDelete(smartDelete: Boolean) {
48+
viewModelScope.launch {
49+
settingsRepository.updateSmartDelete(smartDelete)
50+
}
51+
}
52+
4653
fun updateAddSpaceBetweenNotation(addSpaceBetweenNotation: Boolean) {
4754
viewModelScope.launch {
4855
settingsRepository.updateAddSpaceBetweenNotation(addSpaceBetweenNotation)
@@ -58,7 +65,8 @@ class SettingsViewModel @Inject constructor(
5865

5966
data class SettingsState(
6067
val themeOption: ThemeOption = ThemeOption.System,
68+
val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW,
69+
val smartDelete: Boolean = true,
6170
val addSpaceAfterNotation: Boolean = true,
62-
val vibrateOnTap: Boolean = true,
63-
val wideNotationOption: WideNotationOption = WideNotationOption.WideWithW
71+
val vibrateOnTap: Boolean = true
6472
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.rickyhu.hushkeyboard.settings.ui
2+
3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.material3.Icon
5+
import androidx.compose.material3.ListItem
6+
import androidx.compose.material3.Switch
7+
import androidx.compose.material3.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.platform.testTag
11+
import androidx.compose.ui.res.painterResource
12+
import androidx.compose.ui.tooling.preview.Preview
13+
import com.rickyhu.hushkeyboard.R
14+
import com.rickyhu.hushkeyboard.theme.HushKeyboardTheme
15+
16+
@Composable
17+
fun SmartDeleteSwitchItem(
18+
value: Boolean,
19+
onValueChanged: (Boolean) -> Unit = {}
20+
) {
21+
ListItem(
22+
modifier = Modifier.clickable { onValueChanged(!value) },
23+
headlineContent = { Text("Smart Delete") },
24+
leadingContent = {
25+
Icon(
26+
painter = painterResource(R.drawable.ic_backspace_filled),
27+
contentDescription = "Delete"
28+
)
29+
},
30+
trailingContent = {
31+
Switch(
32+
checked = value,
33+
onCheckedChange = onValueChanged,
34+
modifier = Modifier.testTag("SmartDeleteSwitchItem")
35+
)
36+
}
37+
)
38+
}
39+
40+
@Preview(showBackground = true)
41+
@Composable
42+
fun SmartDeleteSwitchItemPreview() {
43+
HushKeyboardTheme {
44+
AddSpaceBetweenNotationSwitchItem(
45+
value = true
46+
)
47+
}
48+
}
Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,73 @@
11
package com.rickyhu.hushkeyboard.utils
22

33
import android.content.Context
4+
import android.util.Log
45
import android.view.inputmethod.InputConnection
6+
import com.rickyhu.hushkeyboard.model.Notation
57
import com.rickyhu.hushkeyboard.service.HushIMEService
68

7-
private const val CURSOR_POSITION = 1
9+
private const val TAG = "InputConnection"
10+
private const val NEWLINE = '\n'
11+
private const val END_CURSOR_POSITION = 1
12+
private const val SCAN_WINDOW_SIZE = 50
13+
14+
val notationCharList = Notation.getCharList() + listOf(NEWLINE)
815

916
fun Context.toInputConnection(): InputConnection = (this as HushIMEService).currentInputConnection
1017

1118
fun InputConnection.inputText(text: String) {
12-
commitText(text, CURSOR_POSITION)
19+
commitText(text, END_CURSOR_POSITION)
20+
Log.d(TAG, "inputText $text")
21+
}
22+
23+
fun InputConnection.inputNewline() {
24+
inputText(NEWLINE.toString())
1325
}
1426

1527
fun InputConnection.deleteText() {
1628
val selectedText = getSelectedText(0)
1729

1830
if (selectedText.isNullOrEmpty()) {
1931
deleteSurroundingText(1, 0)
32+
Log.d(TAG, "deleteText")
2033
} else {
21-
commitText("", CURSOR_POSITION)
34+
commitText("", END_CURSOR_POSITION)
35+
Log.d(TAG, "delete selected text: $selectedText")
36+
}
37+
}
38+
39+
fun InputConnection.smartDelete() {
40+
Log.d(TAG, "smartDelete")
41+
42+
val selectedText = getSelectedText(0)
43+
if (!selectedText.isNullOrEmpty()) {
44+
commitText("", END_CURSOR_POSITION)
45+
Log.d(TAG, "delete selected text: $selectedText")
46+
return
47+
}
48+
49+
beginBatchEdit()
50+
try {
51+
val textBeforeCursor = getTextBeforeCursor(SCAN_WINDOW_SIZE, 0)
52+
53+
if (textBeforeCursor.isNullOrEmpty()) {
54+
deleteSurroundingText(1, 0)
55+
return
56+
}
57+
58+
var charsToDelete = 0
59+
for (i in textBeforeCursor.indices.reversed()) {
60+
val char = textBeforeCursor[i]
61+
charsToDelete++
62+
if (char.uppercaseChar() in notationCharList) {
63+
break
64+
}
65+
}
66+
67+
if (charsToDelete > 0) {
68+
deleteSurroundingText(charsToDelete, 0)
69+
}
70+
} finally {
71+
endBatchEdit()
2272
}
2373
}

0 commit comments

Comments
ย (0)