mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-22 07:09:10 +00:00
Merge 84f73cc3da
into a745c92e05
This commit is contained in:
commit
1e5bf2b238
2 changed files with 100 additions and 94 deletions
|
@ -4,6 +4,7 @@ import android.view.KeyEvent
|
||||||
import android.view.inputmethod.InputMethodSubtype
|
import android.view.inputmethod.InputMethodSubtype
|
||||||
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
import helium314.keyboard.keyboard.internal.keyboard_parser.floris.KeyCode
|
||||||
import helium314.keyboard.latin.LatinIME
|
import helium314.keyboard.latin.LatinIME
|
||||||
|
import helium314.keyboard.latin.RichInputConnection
|
||||||
import helium314.keyboard.latin.RichInputMethodManager
|
import helium314.keyboard.latin.RichInputMethodManager
|
||||||
import helium314.keyboard.latin.common.Constants
|
import helium314.keyboard.latin.common.Constants
|
||||||
import helium314.keyboard.latin.common.InputPointers
|
import helium314.keyboard.latin.common.InputPointers
|
||||||
|
@ -13,9 +14,12 @@ import helium314.keyboard.latin.common.loopOverCodePointsBackwards
|
||||||
import helium314.keyboard.latin.inputlogic.InputLogic
|
import helium314.keyboard.latin.inputlogic.InputLogic
|
||||||
import helium314.keyboard.latin.settings.Settings
|
import helium314.keyboard.latin.settings.Settings
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener {
|
class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inputLogic: InputLogic) : KeyboardActionListener {
|
||||||
|
|
||||||
|
private val connection = inputLogic.mConnection
|
||||||
|
|
||||||
private val keyboardSwitcher = KeyboardSwitcher.getInstance()
|
private val keyboardSwitcher = KeyboardSwitcher.getInstance()
|
||||||
private val settings = Settings.getInstance()
|
private val settings = Settings.getInstance()
|
||||||
private var metaState = 0 // is this enough, or are there threading issues with the different PointerTrackers?
|
private var metaState = 0 // is this enough, or are there threading issues with the different PointerTrackers?
|
||||||
|
@ -70,8 +74,9 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
||||||
keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
|
keyboardSwitcher.onFinishSlidingInput(latinIME.currentAutoCapsState, latinIME.currentRecapitalizeState)
|
||||||
|
|
||||||
override fun onCustomRequest(requestCode: Int): Boolean {
|
override fun onCustomRequest(requestCode: Int): Boolean {
|
||||||
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER)
|
if (requestCode == Constants.CUSTOM_CODE_SHOW_INPUT_METHOD_PICKER) {
|
||||||
return latinIME.showInputPickerDialog()
|
return latinIME.showInputPickerDialog()
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,30 +106,34 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
||||||
|
|
||||||
override fun onMoveDeletePointer(steps: Int) {
|
override fun onMoveDeletePointer(steps: Int) {
|
||||||
inputLogic.finishInput()
|
inputLogic.finishInput()
|
||||||
val end = inputLogic.mConnection.expectedSelectionEnd
|
val end = connection.expectedSelectionEnd
|
||||||
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
|
val actualSteps = actualSteps(steps)
|
||||||
|
val start = connection.expectedSelectionStart + actualSteps
|
||||||
|
if (start > end) return
|
||||||
|
connection.setSelection(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun actualSteps(steps: Int): Int {
|
||||||
|
var actualSteps = 0
|
||||||
|
// corrected steps to avoid splitting chars belonging to the same codepoint
|
||||||
if (steps > 0) {
|
if (steps > 0) {
|
||||||
val text = inputLogic.mConnection.getSelectedText(0)
|
val text = connection.getSelectedText(0) ?: return steps
|
||||||
if (text == null) actualSteps = steps
|
loopOverCodePoints(text) { cp, charCount ->
|
||||||
else loopOverCodePoints(text) {
|
actualSteps += charCount
|
||||||
actualSteps += Character.charCount(it)
|
|
||||||
actualSteps >= steps
|
actualSteps >= steps
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0)
|
val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return steps
|
||||||
if (text == null) actualSteps = steps
|
loopOverCodePointsBackwards(text) { cp, charCount ->
|
||||||
else loopOverCodePointsBackwards(text) {
|
actualSteps -= charCount
|
||||||
actualSteps -= Character.charCount(it)
|
|
||||||
actualSteps <= steps
|
actualSteps <= steps
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val start = inputLogic.mConnection.expectedSelectionStart + actualSteps
|
return actualSteps
|
||||||
if (start > end) return
|
|
||||||
inputLogic.mConnection.setSelection(start, end)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpWithDeletePointerActive() {
|
override fun onUpWithDeletePointerActive() {
|
||||||
if (!inputLogic.mConnection.hasSelection()) return
|
if (!connection.hasSelection()) return
|
||||||
inputLogic.finishInput()
|
inputLogic.finishInput()
|
||||||
onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
|
onCodeInput(KeyCode.DELETE, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
|
||||||
}
|
}
|
||||||
|
@ -143,17 +152,16 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
||||||
val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype
|
val current = RichInputMethodManager.getInstance().currentSubtype.rawSubtype
|
||||||
var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1
|
var wantedIndex = subtypes.indexOf(current) + if (steps > 0) 1 else -1
|
||||||
wantedIndex %= subtypes.size
|
wantedIndex %= subtypes.size
|
||||||
if (wantedIndex < 0)
|
if (wantedIndex < 0) wantedIndex += subtypes.size
|
||||||
wantedIndex += subtypes.size
|
|
||||||
val newSubtype = subtypes[wantedIndex]
|
val newSubtype = subtypes[wantedIndex]
|
||||||
|
|
||||||
// do not switch if we would switch to the initial subtype after cycling all other subtypes
|
// do not switch if we would switch to the initial subtype after cycling all other subtypes
|
||||||
if (initialSubtype == null)
|
if (initialSubtype == null) initialSubtype = current
|
||||||
initialSubtype = current
|
|
||||||
if (initialSubtype == newSubtype) {
|
if (initialSubtype == newSubtype) {
|
||||||
if ((subtypeSwitchCount > 0 && steps > 0) || ((subtypeSwitchCount < 0 && steps < 0)))
|
if (subtypeSwitchCount > 0 && steps > 0 || subtypeSwitchCount < 0 && steps < 0) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount--
|
if (steps > 0) subtypeSwitchCount++ else subtypeSwitchCount--
|
||||||
|
|
||||||
KeyboardSwitcher.getInstance().switchToSubtype(newSubtype)
|
KeyboardSwitcher.getInstance().switchToSubtype(newSubtype)
|
||||||
|
@ -173,17 +181,8 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
||||||
val steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps
|
val steps = if (RichInputMethodManager.getInstance().currentSubtype.isRtlSubtype) -rawSteps else rawSteps
|
||||||
val moveSteps: Int
|
val moveSteps: Int
|
||||||
if (steps < 0) {
|
if (steps < 0) {
|
||||||
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
|
val text = connection.getTextBeforeCursor(-steps * 4, 0) ?: return false
|
||||||
val text = inputLogic.mConnection.getTextBeforeCursor(-steps * 4, 0) ?: return false
|
moveSteps = negativeMoveSteps(text, steps)
|
||||||
loopOverCodePointsBackwards(text) {
|
|
||||||
if (StringUtils.mightBeEmoji(it)) {
|
|
||||||
actualSteps = 0
|
|
||||||
return@loopOverCodePointsBackwards true
|
|
||||||
}
|
|
||||||
actualSteps -= Character.charCount(it)
|
|
||||||
actualSteps <= steps
|
|
||||||
}
|
|
||||||
moveSteps = -text.length.coerceAtMost(abs(actualSteps))
|
|
||||||
if (moveSteps == 0) {
|
if (moveSteps == 0) {
|
||||||
// some apps don't return any text via input connection, and the cursor can't be moved
|
// some apps don't return any text via input connection, and the cursor can't be moved
|
||||||
// we fall back to virtually pressing the left/right key one or more times instead
|
// we fall back to virtually pressing the left/right key one or more times instead
|
||||||
|
@ -193,18 +192,11 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var actualSteps = 0 // corrected steps to avoid splitting chars belonging to the same codepoint
|
val text = connection.getTextAfterCursor(steps * 4, 0) ?: return false
|
||||||
val text = inputLogic.mConnection.getTextAfterCursor(steps * 4, 0) ?: return false
|
moveSteps = positiveMoveSteps(text, steps)
|
||||||
loopOverCodePoints(text) {
|
|
||||||
if (StringUtils.mightBeEmoji(it)) {
|
|
||||||
actualSteps = 0
|
|
||||||
return@loopOverCodePoints true
|
|
||||||
}
|
|
||||||
actualSteps += Character.charCount(it)
|
|
||||||
actualSteps >= steps
|
|
||||||
}
|
|
||||||
moveSteps = text.length.coerceAtMost(actualSteps)
|
|
||||||
if (moveSteps == 0) {
|
if (moveSteps == 0) {
|
||||||
|
// some apps don't return any text via input connection, and the cursor can't be moved
|
||||||
|
// we fall back to virtually pressing the left/right key one or more times instead
|
||||||
repeat(steps) {
|
repeat(steps) {
|
||||||
onCodeInput(KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
|
onCodeInput(KeyCode.ARROW_RIGHT, Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false)
|
||||||
}
|
}
|
||||||
|
@ -214,15 +206,36 @@ class KeyboardActionListenerImpl(private val latinIME: LatinIME, private val inp
|
||||||
if (inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
|
if (inputLogic.moveCursorByAndReturnIfInsideComposingWord(moveSteps)) {
|
||||||
// no need to finish input and restart suggestions if we're still in the word
|
// no need to finish input and restart suggestions if we're still in the word
|
||||||
// this is a noticeable performance improvement
|
// this is a noticeable performance improvement
|
||||||
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
|
val newPosition = connection.expectedSelectionStart + moveSteps
|
||||||
inputLogic.mConnection.setSelection(newPosition, newPosition)
|
connection.setSelection(newPosition, newPosition)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
inputLogic.finishInput()
|
inputLogic.finishInput()
|
||||||
val newPosition = inputLogic.mConnection.expectedSelectionStart + moveSteps
|
val newPosition = connection.expectedSelectionStart + moveSteps
|
||||||
inputLogic.mConnection.setSelection(newPosition, newPosition)
|
connection.setSelection(newPosition, newPosition)
|
||||||
inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript)
|
inputLogic.restartSuggestionsOnWordTouchedByCursor(settings.current, keyboardSwitcher.currentKeyboardScript)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun positiveMoveSteps(text: CharSequence, steps: Int): Int {
|
||||||
|
var actualSteps = 0
|
||||||
|
// corrected steps to avoid splitting chars belonging to the same codepoint
|
||||||
|
loopOverCodePoints(text) { cp, charCount ->
|
||||||
|
if (StringUtils.mightBeEmoji(cp)) return 0
|
||||||
|
actualSteps += charCount
|
||||||
|
actualSteps >= steps
|
||||||
|
}
|
||||||
|
return min(actualSteps, text.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun negativeMoveSteps(text: CharSequence, steps: Int): Int {
|
||||||
|
var actualSteps = 0
|
||||||
|
// corrected steps to avoid splitting chars belonging to the same codepoint
|
||||||
|
loopOverCodePointsBackwards(text) { cp, charCount ->
|
||||||
|
if (StringUtils.mightBeEmoji(cp)) return 0
|
||||||
|
actualSteps -= charCount
|
||||||
|
actualSteps <= steps
|
||||||
|
}
|
||||||
|
return -min(-actualSteps, text.length)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,64 +9,61 @@ import helium314.keyboard.latin.settings.SpacingAndPunctuations
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) {
|
inline fun loopOverCodePoints(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
|
||||||
val text = if (s is String) s else s.toString()
|
val s = text.toString()
|
||||||
var offset = 0
|
var offset = 0
|
||||||
while (offset < text.length) {
|
while (offset < s.length) {
|
||||||
val codepoint = text.codePointAt(offset)
|
val cp = s.codePointAt(offset)
|
||||||
if (run(codepoint)) return
|
val charCount = Character.charCount(cp)
|
||||||
offset += Character.charCount(codepoint)
|
if (loop(cp, charCount)) return
|
||||||
|
offset += charCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) {
|
inline fun loopOverCodePointsBackwards(text: CharSequence, loop: (cp: Int, charCount: Int) -> Boolean) {
|
||||||
val text = if (s is String) s else s.toString()
|
val s = text.toString()
|
||||||
var offset = text.length
|
var offset = s.length
|
||||||
while (offset > 0) {
|
while (offset > 0) {
|
||||||
val codepoint = text.codePointBefore(offset)
|
val cp = s.codePointBefore(offset)
|
||||||
if (run(codepoint)) return
|
val charCount = Character.charCount(cp)
|
||||||
offset -= Character.charCount(codepoint)
|
if (loop(cp, charCount)) return
|
||||||
|
offset -= charCount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
|
fun nonWordCodePointAndNoSpaceBeforeCursor(text: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
|
||||||
var space = false
|
var space = false
|
||||||
var nonWordCodePoint = false
|
var nonWordCodePoint = false
|
||||||
loopOverCodePointsBackwards(s) {
|
loopOverCodePointsBackwards(text) { cp, _ ->
|
||||||
if (!space && Character.isWhitespace(it))
|
if (!space && Character.isWhitespace(cp)) space = true
|
||||||
space = true
|
// treat double quote like a word codepoint for this function (not great, maybe clarify name or extend list of chars?)
|
||||||
// treat double quote like a word codepoint for the purpose of this function (not great, maybe clarify name, or extend list of chars?)
|
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(cp) && cp != '"'.code) {
|
||||||
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it) && it != '"'.code)
|
|
||||||
nonWordCodePoint = true
|
nonWordCodePoint = true
|
||||||
|
}
|
||||||
space && nonWordCodePoint // stop if both are found
|
space && nonWordCodePoint // stop if both are found
|
||||||
}
|
}
|
||||||
return nonWordCodePoint && !space // return true if an non-word codepoint and no space was found
|
return nonWordCodePoint // return true if a non-word codepoint and no space was found
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean {
|
fun hasLetterBeforeLastSpaceBeforeCursor(text: CharSequence): Boolean {
|
||||||
var letter = false
|
loopOverCodePointsBackwards(text) { cp, _ ->
|
||||||
loopOverCodePointsBackwards(s) {
|
if (Character.isWhitespace(cp)) return false
|
||||||
if (Character.isWhitespace(it)) true
|
else if (Character.isLetter(cp)) return true
|
||||||
else if (Character.isLetter(it)) {
|
false // continue
|
||||||
letter = true
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
else false
|
return false
|
||||||
}
|
|
||||||
return letter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** get the complete emoji at end of [s], considering that emojis can be joined with ZWJ resulting in different emojis */
|
/** get the complete emoji at end of [text], considering that emojis can be joined with ZWJ resulting in different emojis */
|
||||||
fun getFullEmojiAtEnd(s: CharSequence): String {
|
fun getFullEmojiAtEnd(text: CharSequence): String {
|
||||||
val text = if (s is String) s else s.toString()
|
val s = text.toString()
|
||||||
var offset = text.length
|
var offset = s.length
|
||||||
while (offset > 0) {
|
while (offset > 0) {
|
||||||
val codepoint = text.codePointBefore(offset)
|
val codepoint = s.codePointBefore(offset)
|
||||||
// stop if codepoint can't be emoji
|
// stop if codepoint can't be emoji
|
||||||
if (!mightBeEmoji(codepoint))
|
if (!mightBeEmoji(codepoint)) return text.substring(offset)
|
||||||
return text.substring(offset)
|
|
||||||
offset -= Character.charCount(codepoint)
|
offset -= Character.charCount(codepoint)
|
||||||
if (offset > 0 && text[offset - 1].code == KeyCode.ZWJ) {
|
if (offset > 0 && s[offset - 1].code == KeyCode.ZWJ) {
|
||||||
// todo: this appends ZWJ in weird cases like text, ZWJ, emoji
|
// todo: this appends ZWJ in weird cases like text, ZWJ, emoji
|
||||||
// and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd)
|
// and detects single ZWJ as emoji (at least irrelevant for current use of getFullEmojiAtEnd)
|
||||||
offset -= 1
|
offset -= 1
|
||||||
|
@ -76,19 +73,17 @@ fun getFullEmojiAtEnd(s: CharSequence): String {
|
||||||
if (codepoint in 0x1F3FB..0x1F3FF) {
|
if (codepoint in 0x1F3FB..0x1F3FF) {
|
||||||
// Skin tones are not added with ZWJ, but just appended. This is not nice as they can be emojis on their own,
|
// Skin tones are not added with ZWJ, but just appended. This is not nice as they can be emojis on their own,
|
||||||
// but that's how it is done. Assume that an emoji before the skin tone will get merged (usually correct in practice)
|
// but that's how it is done. Assume that an emoji before the skin tone will get merged (usually correct in practice)
|
||||||
val codepointBefore = text.codePointBefore(offset)
|
val codepointBefore = s.codePointBefore(offset)
|
||||||
if (isEmoji(codepointBefore)) {
|
if (isEmoji(codepointBefore)) {
|
||||||
offset -= Character.charCount(codepointBefore)
|
offset -= Character.charCount(codepointBefore)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// check the whole text after offset
|
// check the whole text after offset
|
||||||
val textToCheck = text.substring(offset)
|
val textToCheck = s.substring(offset)
|
||||||
if (isEmoji(textToCheck)) {
|
if (isEmoji(textToCheck)) return textToCheck
|
||||||
return textToCheck
|
|
||||||
}
|
}
|
||||||
}
|
return s.substring(offset)
|
||||||
return text.substring(offset)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
|
/** split the string on the first of consecutive space only, further consecutive spaces are added to the next split */
|
||||||
|
@ -110,8 +105,7 @@ fun String.splitOnFirstSpacesOnly(): List<String> {
|
||||||
sb.append(c)
|
sb.append(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (sb.isNotBlank())
|
if (sb.isNotBlank()) out.add(sb.toString())
|
||||||
out.add(sb.toString())
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,8 +114,7 @@ fun CharSequence.isValidNumber(): Boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun String.decapitalize(locale: Locale): String {
|
fun String.decapitalize(locale: Locale): String {
|
||||||
if (isEmpty() || !this[0].isUpperCase())
|
if (isEmpty() || !this[0].isUpperCase()) return this
|
||||||
return this
|
|
||||||
return replaceFirstChar { it.lowercase(locale) }
|
return replaceFirstChar { it.lowercase(locale) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,7 +129,7 @@ fun containsValueWhenSplit(string: String?, value: String, split: String): Boole
|
||||||
|
|
||||||
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
|
fun isEmoji(c: Int): Boolean = mightBeEmoji(c) && isEmoji(newSingleCodePointString(c))
|
||||||
|
|
||||||
fun isEmoji(s: CharSequence): Boolean = mightBeEmoji(s) && s.matches(emoRegex)
|
fun isEmoji(text: CharSequence): Boolean = mightBeEmoji(text) && text.matches(emoRegex)
|
||||||
|
|
||||||
fun String.splitOnWhitespace() = split(whitespaceSplitRegex)
|
fun String.splitOnWhitespace() = split(whitespaceSplitRegex)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue