Skip to content

Commit 41940fa

Browse files
authored
Merge pull request #666 from 4di-hello/feature/backspace-deletes-words
Add option to delete whole words when holding the delete key
2 parents 09911b6 + 4e2acf3 commit 41940fa

9 files changed

Lines changed: 187 additions & 0 deletions

File tree

‎app/src/main/java/helium314/keyboard/latin/LatinIME.kt‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,10 +1771,21 @@ class LatinIME : InputMethodService(),
17711771
}
17721772
}
17731773

1774+
/** Haptic and audio feedback when holding delete removed a word */
1775+
fun feedbackForDeletedWord() {
1776+
try {
1777+
hapticAndAudioFeedback(KeyCode.DELETE, 0, HapticEvent.KEY_PRESS)
1778+
} catch (e: Exception) {
1779+
Log.w(TAG, "no feedback for deleted word", e)
1780+
}
1781+
}
1782+
17741783
fun hapticAndAudioFeedback(code: Int, repeatCount: Int, hapticEvent: HapticEvent) {
17751784
val keyboardView = keyboardSwitcher.mainKeyboardView
17761785
if (keyboardView != null && keyboardView.isInDraggingFinger()) return
17771786

1787+
// holding delete removes whole words: feedback comes per word, see feedbackForDeletedWord
1788+
if (repeatCount > 0 && code == KeyCode.DELETE && inputLogic.isBackspaceWordModeActive(mSettings.current)) return
17781789
if (repeatCount > 0) {
17791790
when (code) {
17801791
KeyCode.DELETE, KeyCode.ARROW_LEFT, KeyCode.ARROW_UP, KeyCode.WORD_LEFT, KeyCode.PAGE_UP -> if (!inputLogic.connection.canDeleteCharacters()) return

‎app/src/main/java/helium314/keyboard/latin/inputlogic/InputLogic.kt‎

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@ class InputLogic(
9292
private var mCapsLockFromRecapitalize = false
9393

9494
private var mDeleteCount = 0
95+
// for "backspace repeat deletes words": repeat ticks in the current hold, and words deleted so far
96+
private var mBackspaceRepeatTicks = 0
97+
private var mBackspaceWordsDeleted = 0
98+
/** time (ms after the first key repeat) when the next word is deleted */
99+
private var mBackspaceNextWordTime = 0f
100+
/** word deletion not possible in the current hold (no text available), normal deletion is used */
101+
private var mBackspaceWordModeFailed = false
95102
private var mLastKeyTime = 0L
96103
private var mEnteredText: String? = null
97104
private var mIsAutoCorrectionIndicatorOn = false
@@ -1360,6 +1367,65 @@ class InputLogic(
13601367
}
13611368
}
13621369

1370+
/**
1371+
* Whether holding delete currently removes whole words. Not for languages without spaces, where
1372+
* a "word" may be a whole sentence. Also used to give feedback per word instead of per key repeat.
1373+
*/
1374+
fun isBackspaceWordModeActive(settingsValues: SettingsValues): Boolean =
1375+
settingsValues.mBackspaceRepeatDeletesWords
1376+
&& settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
1377+
&& !mBackspaceWordModeFailed
1378+
1379+
/**
1380+
* When the option is enabled and the delete key is held down, switch from deleting characters
1381+
* to deleting whole words after a short while. Words are deleted with increasing speed.
1382+
* Returns true if the event was consumed (a word was deleted, or this repeat tick is skipped).
1383+
*/
1384+
private fun handleBackspaceRepeatWordDeletion(event: Event, settingsValues: SettingsValues): Boolean {
1385+
if (!event.isKeyRepeat || mDeleteCount <= 1) {
1386+
// new press of the delete key
1387+
mBackspaceRepeatTicks = 0
1388+
mBackspaceWordsDeleted = 0
1389+
mBackspaceNextWordTime = 0f
1390+
mBackspaceWordModeFailed = false
1391+
}
1392+
if (!event.isKeyRepeat) return false
1393+
mBackspaceRepeatTicks++
1394+
if (!isBackspaceWordModeActive(settingsValues)) return false
1395+
// key repeats come in fixed intervals, so we can count time in repeats
1396+
val time = (mBackspaceRepeatTicks - 1) * KEY_REPEAT_INTERVAL_MS
1397+
if (time < mBackspaceNextWordTime) return true // wait between words
1398+
1399+
if (mWordComposer.isComposingWord()) {
1400+
mConnection.finishComposingText()
1401+
resetComposingState(true)
1402+
}
1403+
// with very short intervals, more than one word may be due per key repeat
1404+
var deletedWords = 0
1405+
while (time >= mBackspaceNextWordTime && deletedWords < 2) {
1406+
val textBefore = mConnection.getTextBeforeCursor(BACKSPACE_WORD_LOOKBACK, 0)?.toString()
1407+
if (textBefore == null) { // fall back to normal deletion
1408+
mBackspaceWordModeFailed = true
1409+
return deletedWords > 0
1410+
}
1411+
if (textBefore.isEmpty()) return true
1412+
val length = textBefore.length - getStartOfPreviousWord(textBefore)
1413+
if (InputTypeUtils.isWebEditor(getCurrentInputEditorInfo())) {
1414+
repeat(length) { sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL) }
1415+
} else {
1416+
mConnection.deleteTextBeforeCursor(length)
1417+
}
1418+
StatsUtils.onBackspaceWordDelete(length)
1419+
mBackspaceNextWordTime += getBackspaceWordInterval(mBackspaceWordsDeleted)
1420+
mBackspaceWordsDeleted++
1421+
deletedWords++
1422+
}
1423+
mSuggestionStripViewAccessor.setNeutralSuggestionStrip()
1424+
// one feedback per deleted word, key repeat feedback is suppressed in word mode
1425+
mLatinIME.feedbackForDeletedWord()
1426+
return true
1427+
}
1428+
13631429
private fun handleBackspaceEvent(event: Event, inputTransaction: InputTransaction) {
13641430
val currentKeyboardScript = inputTransaction.settingsValues.mCurrentKeyboardScript
13651431
mSpaceState = SpaceState.NONE
@@ -1420,6 +1486,8 @@ class InputLogic(
14201486
}
14211487
inputTransaction.requireShiftUpdate(shiftUpdateKind)
14221488

1489+
if (handleBackspaceRepeatWordDeletion(event, inputTransaction.settingsValues)) return
1490+
14231491
if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
14241492
unlearnWord(mWordComposer.getTypedWord(), inputTransaction.settingsValues, Constants.EVENT_BACKSPACE)
14251493
resetEntireInputState(mConnection.expectedSelectionStart, mConnection.expectedSelectionEnd, true)
@@ -2964,10 +3032,42 @@ class InputLogic(
29643032
companion object {
29653033
private const val TAG = "InputLogic"
29663034
private const val INLINE_EMOJI_SEARCH_MARKER = ':'
3035+
private const val KEY_REPEAT_INTERVAL_MS = 50
3036+
/** interval before the second word, it gets shorter with each word until the minimum */
3037+
private const val BACKSPACE_WORD_START_INTERVAL = 350f
3038+
private const val BACKSPACE_WORD_ACCELERATION = 0.8f
3039+
private const val BACKSPACE_WORD_MIN_INTERVAL = 80f
3040+
3041+
/** Time until the next word is deleted, after [wordsDeleted] words were deleted already */
3042+
fun getBackspaceWordInterval(wordsDeleted: Int): Float {
3043+
var interval = BACKSPACE_WORD_START_INTERVAL
3044+
repeat(wordsDeleted.coerceAtMost(30)) { interval *= BACKSPACE_WORD_ACCELERATION }
3045+
return interval.coerceAtLeast(BACKSPACE_WORD_MIN_INTERVAL)
3046+
}
3047+
private const val BACKSPACE_WORD_LOOKBACK = 200
29673048
private val THAI_LOCALE = Locale.forLanguageTag("th")
29683049
private val THAI_WORD_BREAK_ITERATOR = ThreadLocal.withInitial { BreakIterator.getWordInstance(THAI_LOCALE) }
29693050
private val CURSOR_PATTERN = java.util.regex.Pattern.compile("%cursor(\\d+)%")
29703051

3052+
/**
3053+
* Index where the word before the end of [text] starts, including whitespace and punctuation
3054+
* after it, but keeping the whitespace before it (so "one two " -> "one ").
3055+
*/
3056+
internal fun getStartOfPreviousWord(text: String): Int {
3057+
var i = text.length
3058+
fun skipWhile(predicate: (Int) -> Boolean) {
3059+
while (i > 0) {
3060+
val cp = Character.codePointBefore(text, i)
3061+
if (!predicate(cp)) break
3062+
i -= Character.charCount(cp)
3063+
}
3064+
}
3065+
skipWhile { Character.isWhitespace(it) }
3066+
skipWhile { !Character.isLetterOrDigit(it) && !Character.isWhitespace(it) }
3067+
skipWhile { Character.isLetterOrDigit(it) }
3068+
return i
3069+
}
3070+
29713071
fun isSpaceStrippingPunctuation(codePoint: Int): Boolean {
29723072
return codePoint == '.'.code
29733073
|| codePoint == ','.code

‎app/src/main/java/helium314/keyboard/latin/settings/Defaults.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ object Defaults {
9494
const val PREF_AUTO_CORRECT_THRESHOLD = 0.185f
9595
const val PREF_AUTOCORRECT_SHORTCUTS = true
9696
const val PREF_BACKSPACE_REVERTS_AUTOCORRECT = true
97+
const val PREF_BACKSPACE_REPEAT_DELETES_WORDS = false
9798
const val PREF_CENTER_SUGGESTION_TEXT_TO_ENTER = false
9899
const val PREF_CENTER_TYPED_WORD = false
99100
const val PREF_SHOW_SUGGESTIONS = true

‎app/src/main/java/helium314/keyboard/latin/settings/Settings.kt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ class Settings private constructor() : SharedPreferences.OnSharedPreferenceChang
343343
const val PREF_AUTO_CORRECT_THRESHOLD = "auto_correct_threshold"
344344
const val PREF_AUTOCORRECT_SHORTCUTS = "autocorrect_shortcuts"
345345
const val PREF_BACKSPACE_REVERTS_AUTOCORRECT = "backspace_reverts_autocorrect"
346+
const val PREF_BACKSPACE_REPEAT_DELETES_WORDS = "backspace_repeat_deletes_words"
346347
const val PREF_CENTER_SUGGESTION_TEXT_TO_ENTER = "center_suggestion_text_to_enter"
347348
const val PREF_CENTER_TYPED_WORD = "center_typed_word"
348349
const val PREF_SHOW_SUGGESTIONS = "show_suggestions"

‎app/src/main/java/helium314/keyboard/latin/settings/SettingsValues.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ open class SettingsValues(
220220
val mRememberFloatingKeyboard: Boolean
221221
val mPersistTextEditMode: Boolean
222222
val mBackspaceRevertsAutocorrect: Boolean
223+
val mBackspaceRepeatDeletesWords: Boolean
223224
val mDisableMultiWordSuggestions: Boolean
224225
val mPrioritizePersonalSuggestions: Boolean
225226
val mSuggestionBalance: Int
@@ -361,6 +362,7 @@ open class SettingsValues(
361362
mRememberFloatingKeyboard = prefs.getBoolean(Settings.PREF_REMEMBER_FLOATING_KEYBOARD, Defaults.PREF_REMEMBER_FLOATING_KEYBOARD)
362363
mPersistTextEditMode = prefs.getBoolean(Settings.PREF_PERSIST_TEXT_EDIT_MODE, Defaults.PREF_PERSIST_TEXT_EDIT_MODE)
363364
mBackspaceRevertsAutocorrect = prefs.getBoolean(Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT, Defaults.PREF_BACKSPACE_REVERTS_AUTOCORRECT)
365+
mBackspaceRepeatDeletesWords = prefs.getBoolean(Settings.PREF_BACKSPACE_REPEAT_DELETES_WORDS, Defaults.PREF_BACKSPACE_REPEAT_DELETES_WORDS)
364366
mDisableMultiWordSuggestions = prefs.getBoolean(Settings.PREF_DISABLE_MULTI_WORD_SUGGESTIONS, Defaults.PREF_DISABLE_MULTI_WORD_SUGGESTIONS)
365367
mBigramPredictionEnabled = prefs.getBoolean(Settings.PREF_BIGRAM_PREDICTIONS, Defaults.PREF_BIGRAM_PREDICTIONS)
366368
mPrioritizePersonalSuggestions = prefs.getBoolean(Settings.PREF_PRIORITIZE_PERSONAL_SUGGESTIONS, Defaults.PREF_PRIORITIZE_PERSONAL_SUGGESTIONS)

‎app/src/main/java/helium314/keyboard/settings/screens/TextCorrectionScreen.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ fun TextCorrectionScreen(
5959
if (autocorrectEnabled) Settings.PREF_AUTO_CORRECT_AGGRESSIVENESS else null,
6060
if (autocorrectEnabled) Settings.PREF_AUTO_CORRECT_TRIGGER else null,
6161
if (autocorrectEnabled) Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT else null,
62+
Settings.PREF_BACKSPACE_REPEAT_DELETES_WORDS,
6263
if (autocorrectEnabled) Settings.PREF_EXPAND_FINE_TUNE_AUTOCORRECT else null,
6364
if (autocorrectEnabled && fineTuneAutocorrectExpanded) Settings.PREF_AUTO_CORRECT_THRESHOLD else null,
6465
if (autocorrectEnabled && fineTuneAutocorrectExpanded) Settings.PREF_MORE_AUTO_CORRECTION else null,
@@ -215,6 +216,11 @@ fun createCorrectionSettings(context: Context) = listOf(
215216
Setting(context, Settings.PREF_BACKSPACE_REVERTS_AUTOCORRECT, R.string.backspace_reverts_autocorrect) {
216217
SwitchPreference(it, Defaults.PREF_BACKSPACE_REVERTS_AUTOCORRECT)
217218
},
219+
Setting(context, Settings.PREF_BACKSPACE_REPEAT_DELETES_WORDS,
220+
R.string.backspace_repeat_deletes_words, R.string.backspace_repeat_deletes_words_summary
221+
) {
222+
SwitchPreference(it, Defaults.PREF_BACKSPACE_REPEAT_DELETES_WORDS)
223+
},
218224
Setting(context, Settings.PREF_AUTO_CAP,
219225
R.string.auto_cap, R.string.auto_cap_summary
220226
) {

‎app/src/main/res/values-de/strings.xml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,8 @@
408408
<string name="subtype_dag">Dagbani</string>
409409
<string name="subtype_st">Sesotho</string>
410410
<string name="backspace_reverts_autocorrect">Rücktaste macht Autokorrektur rückgängig</string>
411+
<string name="backspace_repeat_deletes_words">Gedrückte Löschtaste löscht ganze Wörter</string>
412+
<string name="backspace_repeat_deletes_words_summary">Nach kurzem Halten der Löschtaste wortweise löschen, zunehmend schneller</string>
411413
<string name="autospace_after_suggestion">Automatische Leerzeichen nach Auswählen eines Vorschlags</string>
412414
<string name="prefs_emoji_key_fit">Skaliere den Emoji-Knopf mit der Schriftgröße</string>
413415
<string name="backup_restored">Backup wiederhergestellt</string>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@
184184
<string name="large_screen_layout_note">Settings on this screen apply to large-screen layout</string>
185185
<!-- Option to undo auto correction with backspace -->
186186
<string name="backspace_reverts_autocorrect">Backspace reverts autocorrect</string>
187+
<!-- Option to delete whole words when holding the delete key -->
188+
<string name="backspace_repeat_deletes_words">Holding delete removes whole words</string>
189+
<!-- Description for "backspace_repeat_deletes_words" option -->
190+
<string name="backspace_repeat_deletes_words_summary">After holding the delete key for a moment, delete word by word with increasing speed</string>
187191
<!-- Option to disable auto correction. -->
188192
<string name="auto_correction_threshold_mode_off">Off</string> <!-- todo: use it or remove it -->
189193
<!-- Option to suggest auto correction suggestions modestly. Auto-corrects only to a word which has small edit distance from typed word. -->

‎app/src/test/java/helium314/keyboard/latin/InputLogicTest.kt‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,66 @@ class InputLogicTest {
15991599
assertEquals("", composingText)
16001600
}
16011601

1602+
@Test fun `held backspace deletes whole words when enabled`() {
1603+
reset()
1604+
latinIME.prefs().edit { putBoolean(Settings.PREF_BACKSPACE_REPEAT_DELETES_WORDS, true) }
1605+
setText("eins zwei drei vier fuenf sechs sieben")
1606+
functionalKeyPress(KeyCode.DELETE) // a single press deletes one character
1607+
assertEquals("eins zwei drei vier fuenf sechs siebe", text)
1608+
// the first key repeat deletes the rest of the word
1609+
functionalKeyPress(KeyCode.DELETE, isKeyRepeat = true)
1610+
assertEquals("eins zwei drei vier fuenf sechs ", text)
1611+
// some repeats pass without deleting before the next word
1612+
for (i in 1..6) {
1613+
functionalKeyPress(KeyCode.DELETE, isKeyRepeat = true)
1614+
}
1615+
assertEquals("eins zwei drei vier fuenf sechs ", text)
1616+
functionalKeyPress(KeyCode.DELETE, isKeyRepeat = true)
1617+
assertEquals("eins zwei drei vier fuenf ", text)
1618+
// keep holding: everything gets deleted word by word
1619+
for (i in 1..30) {
1620+
functionalKeyPress(KeyCode.DELETE, isKeyRepeat = true)
1621+
}
1622+
assertEquals("", text)
1623+
}
1624+
1625+
@Test fun `held backspace word deletion accelerates`() {
1626+
reset()
1627+
latinIME.prefs().edit { putBoolean(Settings.PREF_BACKSPACE_REPEAT_DELETES_WORDS, true) }
1628+
setText("a b c d e f g h")
1629+
functionalKeyPress(KeyCode.DELETE) // deletes "h"
1630+
// key repeats every 50 ms: repeat 1 at 0 ms ... repeat 22 at 1050 ms
1631+
for (i in 1..22) functionalKeyPress(KeyCode.DELETE, isKeyRepeat = true)
1632+
// recommended: words at 0, 350, 630, 854, 1033 ms
1633+
assertEquals("a b ", text)
1634+
}
1635+
1636+
@Test fun `backspace word intervals`() {
1637+
assertEquals(350f, InputLogic.getBackspaceWordInterval(0), 0.01f)
1638+
assertEquals(280f, InputLogic.getBackspaceWordInterval(1), 0.01f)
1639+
assertEquals(80f, InputLogic.getBackspaceWordInterval(20), 0.01f) // minimum
1640+
}
1641+
1642+
@Test fun `start of previous word keeps whitespace before the word`() {
1643+
fun remaining(text: String) = text.substring(0, InputLogic.getStartOfPreviousWord(text))
1644+
assertEquals("Hallo, ", remaining("Hallo, Welt! "))
1645+
assertEquals("", remaining("Hallo, "))
1646+
assertEquals("", remaining("Hallo"))
1647+
assertEquals("eins ", remaining("eins zwei"))
1648+
assertEquals("", remaining(" "))
1649+
}
1650+
1651+
@Test fun `held backspace deletes characters only when word deletion is disabled`() {
1652+
reset()
1653+
latinIME.prefs().edit { putBoolean(Settings.PREF_BACKSPACE_REPEAT_DELETES_WORDS, false) }
1654+
setText("eins zwei drei vier")
1655+
functionalKeyPress(KeyCode.DELETE)
1656+
for (i in 1..15) {
1657+
functionalKeyPress(KeyCode.DELETE, isKeyRepeat = true)
1658+
}
1659+
assertEquals("ein", text) // with word deletion, "eins" would be gone completely
1660+
}
1661+
16021662
@Test fun timestamp() {
16031663
reset()
16041664
chainInput("hello")

0 commit comments

Comments
 (0)