@@ -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
0 commit comments