Skip to content

Commit e11c2fa

Browse files
authored
Add slide selection and deletion (#424)
* Initial support for marking and deleting * Fix issues after rebase, also fix multitap issues * Fix selection using ime * Rename spacebar slide string to slide enable * Set larger initial threshold when sliding to delete * Move start selection logic into function and various fixes * Add slide gesture section to readme
1 parent 0c1d27c commit e11c2fa

7 files changed

Lines changed: 160 additions & 25 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ This project is a follow-up to the now unmaintained (and closed-source) [Message
6464
- Slide right on the `#` key to switch keyboard position.
6565
- Practice typing, and check your words per minute, using [monkeytype.com](https://monkeytype.com)
6666

67+
### Slide gestures
68+
69+
Enabling `Slide gestures` in keyboard settings will enable the following gestures:
70+
71+
- Slide spacebar horizontally to move cursor position left and right.
72+
- Slide upwards while sliding the spacebar to select text.
73+
- Slide backspace to the left to select text to be deleted. Text will be deleted when key is released.
74+
6775
## Thumb-Key Design
6876

6977
### A History of Phone Keyboards

app/src/main/java/com/dessalines/thumbkey/keyboards/CommonKeys.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ val BACKSPACE_KEY_ITEM =
135135
color = ColorVariant.SECONDARY,
136136
),
137137
swipeType = SwipeNWay.TWO_WAY_HORIZONTAL,
138+
slideType = SlideType.DELETE,
138139
swipes = mapOf(
139140
SwipeDirection.LEFT to KeyC(
140141
action = KeyAction.DeleteLastWord,

app/src/main/java/com/dessalines/thumbkey/ui/components/keyboard/KeyboardKey.kt

Lines changed: 121 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,15 @@ import com.dessalines.thumbkey.utils.KeyAction
4747
import com.dessalines.thumbkey.utils.KeyC
4848
import com.dessalines.thumbkey.utils.KeyDisplay
4949
import com.dessalines.thumbkey.utils.KeyItemC
50+
import com.dessalines.thumbkey.utils.Selection
5051
import com.dessalines.thumbkey.utils.SlideType
5152
import com.dessalines.thumbkey.utils.SwipeDirection
5253
import com.dessalines.thumbkey.utils.buildTapActions
5354
import com.dessalines.thumbkey.utils.colorVariantToColor
5455
import com.dessalines.thumbkey.utils.doneKeyAction
5556
import com.dessalines.thumbkey.utils.fontSizeVariantToFontSize
5657
import com.dessalines.thumbkey.utils.performKeyAction
58+
import com.dessalines.thumbkey.utils.startSelection
5759
import com.dessalines.thumbkey.utils.swipeDirection
5860
import kotlin.math.abs
5961

@@ -106,6 +108,8 @@ fun KeyboardKey(
106108
var offsetX by remember { mutableFloatStateOf(0f) }
107109
var offsetY by remember { mutableFloatStateOf(0f) }
108110

111+
var selection by remember { mutableStateOf(Selection()) }
112+
109113
val backgroundColor = if (!(isDragged.value || isPressed)) {
110114
colorVariantToColor(colorVariant = key.backgroundColor)
111115
} else {
@@ -183,18 +187,46 @@ fun KeyboardKey(
183187
offsetX += x
184188
offsetY += y
185189
if (key.slideType == SlideType.MOVE_CURSOR && slideEnabled) {
186-
if (abs(offsetX) > slideSensitivity) {
190+
if (abs(offsetY) > slideSensitivity * 5) {
191+
// If user slides upwards, enable selection
192+
if (!selection.active) {
193+
// Activate selection
194+
selection = startSelection(ime)
195+
}
196+
if (abs(offsetX) > slideSensitivity) {
197+
if (offsetX < 0.00) {
198+
selection.left()
199+
} else {
200+
selection.right()
201+
}
202+
ime.currentInputConnection.setSelection(
203+
selection.start,
204+
selection.end,
205+
)
206+
offsetX = 0f
207+
}
208+
} else if (abs(offsetX) > slideSensitivity) {
209+
// If user slides horizontally only, move cursor
210+
if (selection.active) selection = Selection(0, 0, false)
187211
val direction: Int
188212
var shouldMove = false
189213
if (offsetX < 0.00) {
190214
// move left
191-
if (ime.currentInputConnection.getTextBeforeCursor(1, 0)?.length != 0) {
215+
if (ime.currentInputConnection.getTextBeforeCursor(
216+
1,
217+
0,
218+
)?.length != 0
219+
) {
192220
shouldMove = true
193221
}
194222
direction = KeyEvent.KEYCODE_DPAD_LEFT
195223
} else {
196224
// move right
197-
if (ime.currentInputConnection.getTextAfterCursor(1, 0)?.length != 0) {
225+
if (ime.currentInputConnection.getTextAfterCursor(
226+
1,
227+
0,
228+
)?.length != 0
229+
) {
198230
shouldMove = true
199231
}
200232
direction = KeyEvent.KEYCODE_DPAD_RIGHT
@@ -219,15 +251,37 @@ fun KeyboardKey(
219251
onSwitchPosition = onSwitchPosition,
220252
)
221253
}
254+
// reset offsetX, do not reset offsetY when sliding, it will break selecting
222255
offsetX = 0f
223-
offsetY = 0f
256+
}
257+
} else if (key.slideType == SlideType.DELETE && slideEnabled) {
258+
if (!selection.active) {
259+
// Activate selection, first detection is longer to preserve swipe actions
260+
if (abs(offsetX) > slideSensitivity * 10) {
261+
selection = startSelection(ime)
262+
}
263+
} else {
264+
if (abs(offsetX) > slideSensitivity) {
265+
if (offsetX < 0.00) {
266+
selection.left()
267+
} else {
268+
selection.right()
269+
}
270+
ime.currentInputConnection.setSelection(
271+
selection.start,
272+
selection.end,
273+
)
274+
offsetX = 0f
275+
}
224276
}
225277
}
226278
},
227279
onDragEnd = {
228-
if (key.slideType == SlideType.NONE || !slideEnabled) {
229-
val swipeDirection = swipeDirection(offsetX, offsetY, minSwipeLength, key.swipeType)
230-
val action = key.swipes?.get(swipeDirection)?.action ?: key.center.action
280+
lateinit var action: KeyAction
281+
if (key.slideType == SlideType.NONE || !slideEnabled || (key.slideType == SlideType.DELETE && !selection.active)) {
282+
val swipeDirection =
283+
swipeDirection(offsetX, offsetY, minSwipeLength, key.swipeType)
284+
action = key.swipes?.get(swipeDirection)?.action ?: key.center.action
231285

232286
performKeyAction(
233287
action = action,
@@ -241,28 +295,72 @@ fun KeyboardKey(
241295
onSwitchLanguage = onSwitchLanguage,
242296
onSwitchPosition = onSwitchPosition,
243297
)
244-
tapCount = 0
245-
lastAction.value = action
246-
247-
// Reset the drags
248-
offsetX = 0f
249-
offsetY = 0f
250-
251-
doneKeyAction(scope, action, isDragged, releasedKey, animationHelperSpeed)
252-
} else {
253298
doneKeyAction(
254299
scope,
255-
KeyAction.SendEvent(
256-
KeyEvent(
257-
KeyEvent.ACTION_UP,
258-
KeyEvent.KEYCODE_DPAD_RIGHT,
259-
),
300+
action,
301+
isDragged,
302+
releasedKey,
303+
animationHelperSpeed,
304+
)
305+
} else if (key.slideType == SlideType.DELETE) {
306+
action = KeyAction.SendEvent(
307+
KeyEvent(
308+
KeyEvent.ACTION_DOWN,
309+
KeyEvent
310+
.KEYCODE_DEL,
260311
),
312+
)
313+
// only delete if valid selection
314+
val sel = ime.currentInputConnection.getSelectedText(0)
315+
sel?.let {
316+
if (it.isNotEmpty()) {
317+
performKeyAction(
318+
action = action,
319+
ime = ime,
320+
autoCapitalize = autoCapitalize,
321+
onToggleShiftMode = onToggleShiftMode,
322+
onToggleNumericMode = onToggleNumericMode,
323+
onToggleCapsLock = onToggleCapsLock,
324+
onAutoCapitalize = onAutoCapitalize,
325+
onSwitchLanguage = onSwitchLanguage,
326+
onSwitchPosition = onSwitchPosition,
327+
onToggleEmojiMode = onToggleEmojiMode,
328+
)
329+
}
330+
}
331+
doneKeyAction(
332+
scope,
333+
action,
334+
isDragged,
335+
releasedKey,
336+
animationHelperSpeed,
337+
)
338+
} else {
339+
action = KeyAction.SendEvent(
340+
KeyEvent(
341+
KeyEvent.ACTION_UP,
342+
KeyEvent.KEYCODE_DPAD_RIGHT,
343+
),
344+
)
345+
doneKeyAction(
346+
scope,
347+
action,
261348
isDragged,
262349
releasedKey,
263350
animationHelperSpeed,
264351
)
265352
}
353+
354+
// Set tapCount and lastAction to avoid issues with multitap after slide
355+
tapCount = 0
356+
lastAction.value = action
357+
358+
// Reset the drags
359+
offsetX = 0f
360+
offsetY = 0f
361+
362+
// Reset selection
363+
selection = Selection()
266364
},
267365
)
268366
}
@@ -373,7 +471,8 @@ fun KeyboardKey(
373471
) {
374472
Box(
375473
contentAlignment = Alignment.Center,
376-
modifier = Modifier.fillMaxSize()
474+
modifier = Modifier
475+
.fillMaxSize()
377476
.background(color = MaterialTheme.colorScheme.tertiaryContainer),
378477
) {}
379478
}

app/src/main/java/com/dessalines/thumbkey/ui/components/settings/lookandfeel/LookAndFeelActivity.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -587,11 +587,11 @@ fun LookAndFeelActivity(
587587
icon = {
588588
Icon(
589589
imageVector = Icons.Outlined.SpaceBar,
590-
contentDescription = stringResource(R.string.spacebar_slide),
590+
contentDescription = stringResource(R.string.slide_enable),
591591
)
592592
},
593593
title = {
594-
Text(stringResource(R.string.spacebar_slide))
594+
Text(stringResource(R.string.slide_enable))
595595
},
596596
onCheckedChange = {
597597
updateAppSettings(

app/src/main/java/com/dessalines/thumbkey/utils/Types.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,19 @@ enum class SwipeNWay {
191191
enum class SlideType {
192192
NONE,
193193
MOVE_CURSOR,
194+
DELETE,
195+
}
196+
197+
data class Selection(
198+
var start: Int,
199+
var end: Int,
200+
var active: Boolean,
201+
) {
202+
constructor() : this (0, 0, false)
203+
fun left() {
204+
end -= 1
205+
}
206+
fun right() {
207+
end += 1
208+
}
194209
}

app/src/main/java/com/dessalines/thumbkey/utils/Utils.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,15 @@ fun Context.getVersionCode(): Int = if (Build.VERSION.SDK_INT >= Build.VERSION_C
602602
@Suppress("DEPRECATION")
603603
getPackageInfo().versionCode
604604
}
605+
606+
fun startSelection(ime: IMEService): Selection {
607+
val cursorPosition =
608+
ime.currentInputConnection.getTextBeforeCursor(
609+
1000, // Higher value mens slower execution
610+
0,
611+
)?.length
612+
cursorPosition?.let {
613+
return Selection(it, it, true)
614+
}
615+
return Selection()
616+
}

app/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
<string name="animation_speed">Animation Speed: %1$s</string>
4242
<string name="animation_helper_speed">Animation Helper Speed: %1$s</string>
4343
<string name="slide_sensitivity">Slide sensitivity: %1$s</string>
44-
<string name="spacebar_slide">Spacebar Sliding Cursor</string>
44+
<string name="slide_enable">Slide gestures</string>
4545
<string name="reset_to_defaults">Reset to defaults</string>
4646
<string name="reset_to_defaults_msg">Are you sure you want to reset settings to defaults?</string>
4747
<string name="reset_to_defaults_confirm">Reset</string>

0 commit comments

Comments
 (0)