0
0
mirror of https://github.com/florisboard/florisboard.git synced 2024-09-19 11:32:18 +02:00

Fixes to Chinese shape-based layout: mixed language support (#2093)

* Bug fix: expected content cannot match

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Reset suggestions when switching subtypes

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Draft: record last candidate or gesture commit position to help determine composing range

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Let the NlpProvider handle last candidate or gesture commit position when determining composing range.

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* draft: allow Enter to commit raw text for CJK

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Add full-width comma to CJK symbols

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Add minimal support for physical keyboard handling (space, enter)

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Force suggestion on for HanShapeBased to avoid user confusion

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* Delete pushes lastCommitPosition back instead of resetting to -1

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* fix: delete flogDebug without import

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>

* fix: allow composing range to change based on subtype switch

* fix: bug where HanShapeBased force suggestion on not reflected in displayed candidates

* fix: for speed, stub out getListOfWords and getFrequencyForWord

* Polish LANGUAGEPACK.md. Add warning about phonetic input. Add translations.

* Move Chinese language pack README to another file

---------

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>
This commit is contained in:
moonbeamcelery 2023-02-24 12:18:44 +00:00 committed by GitHub
parent a5dab5fb5a
commit f6e58f7534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 354 additions and 107 deletions

143
LANGUAGEPACKS-CHINESE.md Normal file
View File

@ -0,0 +1,143 @@
# Chinese language pack
[简体中文翻译点这里](#简体中文翻译)
[繁体中文翻譯點這裡](#繁体中文翻譯)
## Default barebones Chinese shape-based pack
Default barebones Chinese shape-based language pack which are always available.
Please download the [full Chinese language pack](#full-chinese-shape-based-pack) to access input methods such as wubi, quick cangjie, and other versions of these input methods.
This pack is released under the same license as Florisboard.
## Full Chinese shape-based pack
Chinese shape-based language pack based on fcitx5-table-extra.
This pack is released under a separate license. Please visit to download: [TODO: add link to release page](https://)
:no_entry: :no_entry: **WARNING** :no_entry: :no_entry:
- **Input methods that include pinyin, jyutping, zhuyin as (auxiliary) functions are only provided for convenience. Currently, FlorisBoard lacks phonetic input algorithms. These functions currently have poor user experience and are not recommended for daily use.**
The following input methods are included in this language pack:
- 中文 (中国) [T9笔画] / Chinese (China) [T9]
- 中文 (中国) [五笔98] / Chinese (China) [WUBI98]
- ~中文 (中国) [五笔98-拼音混打] / Chinese (China) [WUBI98PINYIN]~
- 中文 (中国) [五笔98-单字] / Chinese (China) [WUBI98SINGLE]
- 中文 (中国) [五笔-大字库] / Chinese (China) [WUBILARGE]
- 中文 (中国) [郑码] / Chinese (China) [ZHENGMA]
- 中文 (中国) [郑码-大字库] / Chinese (China) [ZHENGMALARGE]
- ~中文 (中国) [郑码-拼音混打] / Chinese (China) [ZHENGMAPINYIN]~
- ~中文 (香港) [廣東拼音] / Chinese (Hong Kong) [CANTONESE]~
- ~中文 (香港) [港式廣東話] / Chinese (Hong Kong) [CANTONHK]~
- 中文 (香港) [輕鬆-大字庫] / Chinese (Hong Kong) [EASYLARGE]
- ~中文 (香港) [粤語拼音-表格] / Chinese (Hong Kong) [JYUTPINGTABLE]~
- 中文 (香港) [速成三代] / Chinese (Hong Kong) [QUICK3]
- 中文 (香港) [經典速成] / Chinese (Hong Kong) [QUICKCLASSIC]
- 中文 (香港) [筆順五碼] / Chinese (Hong Kong) [STROKE5]
- 中文 (台灣) [行列] / Chinese (Taiwan) [ARRAY30]
- 中文 (台灣) [行列-大字库] / Chinese (Taiwan) [ARRAY30LARGE]
- 中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]
- 中文 (台灣) [倉頡三代] / Chinese (Taiwan) [CANGJIE3]
- 中文 (台灣) [倉頡五代] / Chinese (Taiwan) [CANGJIE5]
- 中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]
- 中文 (台灣) [速成五代] / Chinese (Taiwan) [QUICK5]
- 中文 (台灣) [快倉六] / Chinese (Taiwan) [SCJ6]
- ~中文 (台灣) [吳語注音] / Chinese (Taiwan) [WU]~
Third-party license: [https://github.com/fcitx/fcitx5-table-extra/blob/master/LICENSES/GPL-3.0-or-later.txt]
# 简体中文翻译
## 默认中文形码语言包
请下载[完整中文语言包](#fcitx5-中文形码语言包)来获得五笔、快速仓颉等输入法,及这些输入法的其他版本。
这一语言包的使用许可与FlorisBoard相同。
## Fcitx5 中文形码语言包
这一语言包由另一使用许可提供。下载请访问:[TODO: add link to release page](https://)
:no_entry: :no_entry: **注意** :no_entry: :no_entry:
- **含拼音、粤拼、注音等功能的输入法仅为便利提供。目前FlorisBoard缺乏音码输入算法这些功能目前使用体验不佳暂不推荐日常使用。**
语言包内含以下输入法:
- 中文 (中国) [T9笔画] / Chinese (China) [T9]
- 中文 (中国) [五笔98] / Chinese (China) [WUBI98]
- ~中文 (中国) [五笔98-拼音混打] / Chinese (China) [WUBI98PINYIN]~
- 中文 (中国) [五笔98-单字] / Chinese (China) [WUBI98SINGLE]
- 中文 (中国) [五笔-大字库] / Chinese (China) [WUBILARGE]
- 中文 (中国) [郑码] / Chinese (China) [ZHENGMA]
- 中文 (中国) [郑码-大字库] / Chinese (China) [ZHENGMALARGE]
- ~中文 (中国) [郑码-拼音混打] / Chinese (China) [ZHENGMAPINYIN]~
- ~中文 (香港) [廣東拼音] / Chinese (Hong Kong) [CANTONESE]~
- ~中文 (香港) [港式廣東話] / Chinese (Hong Kong) [CANTONHK]~
- 中文 (香港) [輕鬆-大字庫] / Chinese (Hong Kong) [EASYLARGE]
- ~中文 (香港) [粤語拼音-表格] / Chinese (Hong Kong) [JYUTPINGTABLE]~
- 中文 (香港) [速成三代] / Chinese (Hong Kong) [QUICK3]
- 中文 (香港) [經典速成] / Chinese (Hong Kong) [QUICKCLASSIC]
- 中文 (香港) [筆順五碼] / Chinese (Hong Kong) [STROKE5]
- 中文 (台灣) [行列] / Chinese (Taiwan) [ARRAY30]
- 中文 (台灣) [行列-大字库] / Chinese (Taiwan) [ARRAY30LARGE]
- 中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]
- 中文 (台灣) [倉頡三代] / Chinese (Taiwan) [CANGJIE3]
- 中文 (台灣) [倉頡五代] / Chinese (Taiwan) [CANGJIE5]
- 中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]
- 中文 (台灣) [速成五代] / Chinese (Taiwan) [QUICK5]
- 中文 (台灣) [快倉六] / Chinese (Taiwan) [SCJ6]
- ~中文 (台灣) [吳語注音] / Chinese (Taiwan) [WU]~
第三方许可证: [https://github.com/fcitx/fcitx5-table-extra/blob/master/LICENSES/GPL-3.0-or-later.txt]
# 繁体中文翻譯
## 預設中文形碼語言包
請下載[完整中文語言包](#fcitx5-中文形碼語言包)來獲得五筆、快速倉頡等輸入法,及這些輸入法的其他版本。
這一語言包的使用許可與FlorisBoard相同。
## Fcitx5 中文形碼語言包
這一語言包由另一使用許可提供。下載請訪問:[TODO: add link to release page](https://)
:no_entry: :no_entry: **注意** :no_entry: :no_entry:
- **含拼音、粵拼、注音等功能的輸入法僅為便利提供。目前FlorisBoard缺乏音碼輸入算法這些功能目前使用體驗不佳暫不推薦日常使用。**
語言包內含以下輸入法:
- 中文 (中国) [T9笔画] / Chinese (China) [T9]
- 中文 (中国) [五笔98] / Chinese (China) [WUBI98]
- ~中文 (中国) [五笔98-拼音混打] / Chinese (China) [WUBI98PINYIN]~
- 中文 (中国) [五笔98-单字] / Chinese (China) [WUBI98SINGLE]
- 中文 (中国) [五笔-大字库] / Chinese (China) [WUBILARGE]
- 中文 (中国) [郑码] / Chinese (China) [ZHENGMA]
- 中文 (中国) [郑码-大字库] / Chinese (China) [ZHENGMALARGE]
- ~中文 (中国) [郑码-拼音混打] / Chinese (China) [ZHENGMAPINYIN]~
- ~中文 (香港) [廣東拼音] / Chinese (Hong Kong) [CANTONESE]~
- ~中文 (香港) [港式廣東話] / Chinese (Hong Kong) [CANTONHK]~
- 中文 (香港) [輕鬆-大字庫] / Chinese (Hong Kong) [EASYLARGE]
- ~中文 (香港) [粤語拼音-表格] / Chinese (Hong Kong) [JYUTPINGTABLE]~
- 中文 (香港) [速成三代] / Chinese (Hong Kong) [QUICK3]
- 中文 (香港) [經典速成] / Chinese (Hong Kong) [QUICKCLASSIC]
- 中文 (香港) [筆順五碼] / Chinese (Hong Kong) [STROKE5]
- 中文 (台灣) [行列] / Chinese (Taiwan) [ARRAY30]
- 中文 (台灣) [行列-大字库] / Chinese (Taiwan) [ARRAY30LARGE]
- 中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]
- 中文 (台灣) [倉頡三代] / Chinese (Taiwan) [CANGJIE3]
- 中文 (台灣) [倉頡五代] / Chinese (Taiwan) [CANGJIE5]
- 中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]
- 中文 (台灣) [速成五代] / Chinese (Taiwan) [QUICK5]
- 中文 (台灣) [快倉六] / Chinese (Taiwan) [SCJ6]
- ~中文 (台灣) [吳語注音] / Chinese (Taiwan) [WU]~
第三方許可證: [https://github.com/fcitx/fcitx5-table-extra/blob/master/LICENSES/GPL-3.0-or-later.txt]

View File

@ -3,7 +3,7 @@
## Languages ## Languages
- [Summary](#summary) - [Summary](#summary)
- [Chinese / 中文](#chinese--中文) - [Chinese / 中文](LANGUAGEPACKS-CHINESE.md)
## Summary ## Summary
@ -14,54 +14,3 @@ language packs.
The homepage of default language packs included in FlorisBoard should link to this page. The homepage of default language packs included in FlorisBoard should link to this page.
## Chinese / 中文
TODO: translate into native language
### Default barebones Chinese shape-based pack
默认中文形码语言包 / 預設中文形碼語言包 / Default barebones Chinese shape-based language pack which are always available.
Please download the [full Chinese language pack](#full-chinese-shape-based-pack) to access input methods
such as wubi, boshiami, and other versions of cangjie.
This pack is released under the same license as Florisboard.
TODO: translate into native language
### Full Chinese shape-based pack
Fcitx5 中文形码语言包 / Fcitx5 中文形碼語言包 / Chinese shape-based language pack based on fcitx5-table-extra.
This pack is released under a separate license. Please visit: [TODO: add link to release page](https://)
The following input methods are included in this language pack:
- 中文 (中国) [T9笔画] / Chinese (China) [T9]
- 中文 (中国) [五笔98] / Chinese (China) [WUBI98]
- 中文 (中国) [五笔98-拼音混打] / Chinese (China) [WUBI98PINYIN]
- 中文 (中国) [五笔98-单字] / Chinese (China) [WUBI98SINGLE]
- 中文 (中国) [五笔-大字库] / Chinese (China) [WUBILARGE]
- 中文 (中国) [郑码] / Chinese (China) [ZHENGMA]
- 中文 (中国) [郑码-大字库] / Chinese (China) [ZHENGMALARGE]
- 中文 (中国) [郑码-拼音混打] / Chinese (China) [ZHENGMAPINYIN]
- 中文 (香港) [廣東拼音] / Chinese (Hong Kong) [CANTONESE]
- 中文 (香港) [港式廣東話] / Chinese (Hong Kong) [CANTONHK]
- 中文 (香港) [輕鬆-大字庫] / Chinese (Hong Kong) [EASYLARGE]
- 中文 (香港) [粤語拼音-表格] / Chinese (Hong Kong) [JYUTPINGTABLE]
- 中文 (香港) [速成三代] / Chinese (Hong Kong) [QUICK3]
- 中文 (香港) [經典速成] / Chinese (Hong Kong) [QUICKCLASSIC]
- 中文 (香港) [筆順五碼] / Chinese (Hong Kong) [STROKE5]
- 中文 (台灣) [行列] / Chinese (Taiwan) [ARRAY30]
- 中文 (台灣) [行列-大字库] / Chinese (Taiwan) [ARRAY30LARGE]
- 中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]
- 中文 (台灣) [倉頡三代] / Chinese (Taiwan) [CANGJIE3]
- 中文 (台灣) [倉頡五代] / Chinese (Taiwan) [CANGJIE5]
- 中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]
- 中文 (台灣) [速成五代] / Chinese (Taiwan) [QUICK5]
- 中文 (台灣) [快倉六] / Chinese (Taiwan) [SCJ6]
- 中文 (台灣) [吳語注音] / Chinese (Taiwan) [WU]
Third-party license: [https://github.com/fcitx/fcitx5-table-extra/blob/master/LICENSES/GPL-3.0-or-later.txt]
TODO: translate into native language

View File

@ -8,7 +8,7 @@
{ "code": -201, "label": "view_characters", "type": "system_gui" }, { "code": -201, "label": "view_characters", "type": "system_gui" },
{ "$": "char_width_selector", { "$": "char_width_selector",
"full": { "code": 12289, "label": "、", "popup": { "full": { "code": 12289, "label": "、", "popup": {
"main": { "code": 44, "label": "," } "main": { "code": 65292, "label": "" }
} }
}, },
"half": { "code": 65380, "label": "、", "popup": { "half": { "code": 65380, "label": "、", "popup": {

View File

@ -6,7 +6,7 @@
"title": "Default barebones Chinese shape-based pack", "title": "Default barebones Chinese shape-based pack",
"description": "默认中文形码语言包 / 預設中文形碼語言包 / Default barebones Chinese shape-based language pack which are always available.\n请访问下面的主页链接下载更多输入法 / 請訪問下面的主頁鏈接下載更多輸入法 / Please visit the homepage linked below to download more input methods.", "description": "默认中文形码语言包 / 預設中文形碼語言包 / Default barebones Chinese shape-based language pack which are always available.\n请访问下面的主页链接下载更多输入法 / 請訪問下面的主頁鏈接下載更多輸入法 / Please visit the homepage linked below to download more input methods.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>", "waelwindows", "moonbeamcelery" ], "maintainers": [ "patrickgold <patrick@patrickgold.dev>", "waelwindows", "moonbeamcelery" ],
"homepage": "https://github.com/florisboard/florisboard/blob/master/LANGUAGEPACKS.md#default-barebones-chinese-shape-based-pack", "homepage": "https://github.com/florisboard/florisboard/blob/master/LANGUAGEPACKS-CHINESE.md#default-barebones-chinese-shape-based-pack",
"license": "apache-2.0" "license": "apache-2.0"
}, },
"items": [ "items": [

View File

@ -25,6 +25,7 @@ import android.os.Bundle
import android.util.Size import android.util.Size
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
@ -635,6 +636,11 @@ class FlorisImeService : LifecycleInputMethodService() {
} }
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean =
if (keyboardManager.onHardwareKeyDown(keyCode, event)) true
else super.onKeyDown(keyCode, event)
private inner class ComposeInputView : AbstractComposeView(this) { private inner class ComposeInputView : AbstractComposeView(this) {
init { init {
isHapticFeedbackEnabled = true isHapticFeedbackEnabled = true

View File

@ -112,6 +112,7 @@ private fun DevtoolsInputStateOverlay() {
DevtoolsText(text = "After: \"${content.textAfterSelection}\"") DevtoolsText(text = "After: \"${content.textAfterSelection}\"")
DevtoolsText(text = "Composing: ${content.composing}") DevtoolsText(text = "Composing: ${content.composing}")
DevtoolsText(text = "CurrentWord: ${content.currentWord}") DevtoolsText(text = "CurrentWord: ${content.currentWord}")
DevtoolsText(text = "LastCommit: ${editorInstance.lastCommitPosition}")
} }
} }
} }

View File

@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.editor package dev.patrickgold.florisboard.ime.editor
import android.content.Context import android.content.Context
import android.icu.text.BreakIterator
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.os.SystemClock import android.os.SystemClock
import android.text.TextUtils import android.text.TextUtils
@ -33,6 +32,8 @@ import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.kotlin.guardedByLock import dev.patrickgold.florisboard.lib.kotlin.guardedByLock
import dev.patrickgold.florisboard.nlpManager import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager import dev.patrickgold.florisboard.subtypeManager
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
@ -82,6 +83,9 @@ abstract class AbstractEditorInstance(context: Context) {
_activeContentFlow.value = v _activeContentFlow.value = v
} }
private val expectedContentQueue = ExpectedContentQueue() private val expectedContentQueue = ExpectedContentQueue()
private val _lastCommitPosition = LastCommitPosition()
val lastCommitPosition
get() = LastCommitPosition(_lastCommitPosition)
fun expectedContent(): EditorContent? { fun expectedContent(): EditorContent? {
return runBlocking { expectedContentQueue.peekNewestOrNull() } return runBlocking { expectedContentQueue.peekNewestOrNull() }
@ -145,6 +149,7 @@ abstract class AbstractEditorInstance(context: Context) {
if (composing.isValid) { if (composing.isValid) {
currentInputConnection()?.setComposingRegion(EditorRange.Unspecified) currentInputConnection()?.setComposingRegion(EditorRange.Unspecified)
} }
_lastCommitPosition.handleUpdateSelection(newSelection)
} }
open fun handleSelectionUpdate(oldSelection: EditorRange, newSelection: EditorRange, composing: EditorRange) { open fun handleSelectionUpdate(oldSelection: EditorRange, newSelection: EditorRange, composing: EditorRange) {
@ -157,6 +162,7 @@ abstract class AbstractEditorInstance(context: Context) {
return return
} }
_lastCommitPosition.handleUpdateSelection(newSelection)
val expected = runBlocking { val expected = runBlocking {
expectedContentQueue.popUntilOrNull { expectedContentQueue.popUntilOrNull {
it.selection == newSelection && it.composing == composing && it.selection == newSelection && it.composing == composing &&
@ -207,6 +213,7 @@ abstract class AbstractEditorInstance(context: Context) {
activeCursorCapsMode = InputAttributes.CapsMode.NONE activeCursorCapsMode = InputAttributes.CapsMode.NONE
activeContent = EditorContent.Unspecified activeContent = EditorContent.Unspecified
runBlocking { expectedContentQueue.clear() } runBlocking { expectedContentQueue.clear() }
_lastCommitPosition.reset()
} }
private suspend fun generateContent( private suspend fun generateContent(
@ -231,7 +238,7 @@ abstract class AbstractEditorInstance(context: Context) {
// Determine local composing word range, if any // Determine local composing word range, if any
val localCurrentWord = val localCurrentWord =
if (shouldDetermineComposingRegion(editorInfo) && localSelection.isCursorMode && textBeforeSelection.isNotEmpty()) { if (shouldDetermineComposingRegion(editorInfo) && localSelection.isCursorMode && textBeforeSelection.isNotEmpty()) {
determineLocalComposing(textBeforeSelection) determineLocalComposing(textBeforeSelection, _lastCommitPosition.pos - offset)
} else { } else {
EditorRange.Unspecified EditorRange.Unspecified
} }
@ -253,7 +260,9 @@ abstract class AbstractEditorInstance(context: Context) {
textAfterSelection: CharSequence = this.textAfterSelection, textAfterSelection: CharSequence = this.textAfterSelection,
selectedText: CharSequence = this.selectedText, selectedText: CharSequence = this.selectedText,
): EditorContent { ): EditorContent {
return generateContent(editorInfo, selection, textBeforeSelection, textAfterSelection, selectedText) return generateContent(
editorInfo, selection, textBeforeSelection, textAfterSelection, selectedText
)
} }
private fun EditorContent.cursorCapsMode(): InputAttributes.CapsMode { private fun EditorContent.cursorCapsMode(): InputAttributes.CapsMode {
@ -275,8 +284,10 @@ abstract class AbstractEditorInstance(context: Context) {
return editorInfo.isRichInputEditor return editorInfo.isRichInputEditor
} }
private suspend fun determineLocalComposing(textBeforeSelection: CharSequence): EditorRange { private suspend fun determineLocalComposing(
return nlpManager.determineLocalComposing(textBeforeSelection, breakIterators) textBeforeSelection: CharSequence, localLastCommitPosition: Int
): EditorRange {
return nlpManager.determineLocalComposing(textBeforeSelection, breakIterators, localLastCommitPosition)
} }
private fun InputConnection.setComposingRegion(composing: EditorRange) { private fun InputConnection.setComposingRegion(composing: EditorRange) {
@ -403,8 +414,7 @@ abstract class AbstractEditorInstance(context: Context) {
val newContent = content.generateCopy( val newContent = content.generateCopy(
selection = newSelection, selection = newSelection,
textBeforeSelection = buildString { textBeforeSelection = buildString {
append(content.textBeforeSelection) append(content.textBeforeSelection.removeSuffix(content.composingText))
removeSuffix(content.composingText)
append(text) append(text)
}, },
selectedText = "", selectedText = "",
@ -412,6 +422,7 @@ abstract class AbstractEditorInstance(context: Context) {
expectedContentQueue.push(newContent) expectedContentQueue.push(newContent)
ic.setComposingText(text, 1) ic.setComposingText(text, 1)
ic.finishComposingText() ic.finishComposingText()
_lastCommitPosition.handleCommit(newContent.selection)
} }
ic.endBatchEdit() ic.endBatchEdit()
return true return true
@ -424,7 +435,7 @@ abstract class AbstractEditorInstance(context: Context) {
// Cannot perform below check due to editors which lie about their correct selection // Cannot perform below check due to editors which lie about their correct selection
//if (content.selection.isValid && content.selection.start == 0) return true //if (content.selection.isValid && content.selection.start == 0) return true
val oldTextBeforeSelection = content.textBeforeSelection val oldTextBeforeSelection = content.textBeforeSelection
return if (activeInfo.isRawInputEditor || oldTextBeforeSelection.isEmpty()) { return (if (activeInfo.isRawInputEditor || oldTextBeforeSelection.isEmpty()) {
// If editor is rich and text before selection is empty we seem to have an invalid state here, so we fall // If editor is rich and text before selection is empty we seem to have an invalid state here, so we fall
// back to emulating a hardware backspace. // back to emulating a hardware backspace.
when (type) { when (type) {
@ -438,7 +449,8 @@ abstract class AbstractEditorInstance(context: Context) {
TextType.CHARACTERS -> breakIterators.measureLastUChars(oldTextBeforeSelection, n, locale) TextType.CHARACTERS -> breakIterators.measureLastUChars(oldTextBeforeSelection, n, locale)
TextType.WORDS -> breakIterators.measureLastUWords(oldTextBeforeSelection, n, locale) TextType.WORDS -> breakIterators.measureLastUWords(oldTextBeforeSelection, n, locale)
} }
val newSelection = content.selection.translatedBy(-length) val selection = content.selection
val newSelection = selection.translatedBy(-length)
val newContent = content.generateCopy( val newContent = content.generateCopy(
selection = newSelection, selection = newSelection,
textBeforeSelection = oldTextBeforeSelection.dropLast(length), textBeforeSelection = oldTextBeforeSelection.dropLast(length),
@ -451,6 +463,21 @@ abstract class AbstractEditorInstance(context: Context) {
ic.endBatchEdit() ic.endBatchEdit()
true true
} }
}).also {
deleteMoveLastCommitPosition()
}
}
fun refreshComposing() {
val content = activeContent
val ic = currentInputConnection()
if (activeInfo.isRawInputEditor || ic == null) return
runBlocking {
val newContent = content.generateCopy()
if (newContent.composing != content.composing) {
expectedContentQueue.push(newContent)
ic.setComposingRegion(newContent.composing)
}
} }
} }
@ -632,4 +659,41 @@ abstract class AbstractEditorInstance(context: Context) {
} }
} }
} }
fun updateLastCommitPosition() = _lastCommitPosition.handleCommit(activeContent.selection)
fun deleteMoveLastCommitPosition() = _lastCommitPosition.handleDelete(activeContent.selection)
/**
* Class for handling history of last commit position.
*/
data class LastCommitPosition(var pos: Int = -1) {
constructor(other: LastCommitPosition): this(other.pos)
fun reset() {
pos = -1
}
fun handleCommit(selection: EditorRange) {
if (selection.isValid) {
pos = max(selection.start, selection.end)
} else {
reset()
}
}
fun handleUpdateSelection(selection: EditorRange) {
val start = min(selection.start, selection.end)
if (start < pos) {
reset()
}
}
fun handleDelete(selection: EditorRange) {
val start = min(selection.start, selection.end)
if (start < pos) {
pos = start
}
}
}
} }

View File

@ -139,7 +139,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
} }
override fun determineComposingEnabled(): Boolean { override fun determineComposingEnabled(): Boolean {
return prefs.suggestion.enabled.get() return nlpManager.isSuggestionOn()
} }
override fun determineComposer(composerName: ExtensionComponentName): Composer { override fun determineComposer(composerName: ExtensionComponentName): Composer {
@ -264,6 +264,9 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
super.commitText("$SPACE$text") super.commitText("$SPACE$text")
} else { } else {
super.commitText(text) super.commitText(text)
}.also {
// handled in finalizeComposingText if content.composing.isValid
updateLastCommitPosition()
} }
} }
} }
@ -286,6 +289,8 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
super.commitText("$SPACE$text") super.commitText("$SPACE$text")
} else { } else {
super.commitText(text) super.commitText(text)
}.also {
updateLastCommitPosition()
} }
} }
@ -303,7 +308,9 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
val mimeTypes = item.mimeTypes val mimeTypes = item.mimeTypes
return when (item.type) { return when (item.type) {
ItemType.TEXT -> { ItemType.TEXT -> {
commitText(item.text.toString()) commitText(item.text.toString()).also {
updateLastCommitPosition()
}
} }
ItemType.IMAGE, ItemType.VIDEO -> { ItemType.IMAGE, ItemType.VIDEO -> {
item.uri ?: return false item.uri ?: return false
@ -470,6 +477,14 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
} }
} }
fun tryPerformEnterCommitRaw(): Boolean {
return if (subtypeManager.activeSubtype.primaryLocale.language.startsWith("zh") && activeContent.composing.length > 0) {
finalizeComposingText(activeContent.composingText)
} else {
false
}
}
/** /**
* Performs a given [action] on the current input editor. * Performs a given [action] on the current input editor.
* *

View File

@ -35,6 +35,7 @@ import dev.patrickgold.florisboard.ime.ImeUiMode
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.core.Subtype import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.core.SubtypePreset import dev.patrickgold.florisboard.ime.core.SubtypePreset
import dev.patrickgold.florisboard.ime.editor.EditorContent
import dev.patrickgold.florisboard.ime.editor.FlorisEditorInfo import dev.patrickgold.florisboard.ime.editor.FlorisEditorInfo
import dev.patrickgold.florisboard.ime.editor.ImeOptions import dev.patrickgold.florisboard.ime.editor.ImeOptions
import dev.patrickgold.florisboard.ime.editor.InputAttributes import dev.patrickgold.florisboard.ime.editor.InputAttributes
@ -56,6 +57,7 @@ import dev.patrickgold.florisboard.ime.text.keyboard.TextKeyboardCache
import dev.patrickgold.florisboard.lib.android.showLongToast import dev.patrickgold.florisboard.lib.android.showLongToast
import dev.patrickgold.florisboard.lib.android.showShortToast import dev.patrickgold.florisboard.lib.android.showShortToast
import dev.patrickgold.florisboard.lib.devtools.LogTopic import dev.patrickgold.florisboard.lib.devtools.LogTopic
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import dev.patrickgold.florisboard.lib.devtools.flogError import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.kotlin.collectIn import dev.patrickgold.florisboard.lib.kotlin.collectIn
@ -144,16 +146,14 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
subtypeManager.activeSubtypeFlow.collectLatestIn(scope) { subtypeManager.activeSubtypeFlow.collectLatestIn(scope) {
reevaluateInputShiftState() reevaluateInputShiftState()
updateActiveEvaluators() updateActiveEvaluators()
editorInstance.refreshComposing()
resetSuggestions(editorInstance.activeContent)
} }
clipboardManager.primaryClipFlow.collectLatestIn(scope) { clipboardManager.primaryClipFlow.collectLatestIn(scope) {
updateActiveEvaluators() updateActiveEvaluators()
} }
editorInstance.activeContentFlow.collectIn(scope) { content -> editorInstance.activeContentFlow.collectIn(scope) { content ->
if (!activeState.isComposingEnabled) { resetSuggestions(content)
nlpManager.clearSuggestions()
return@collectIn
}
nlpManager.suggest(subtypeManager.activeSubtype, content)
} }
prefs.devtools.enabled.observeForever { prefs.devtools.enabled.observeForever {
reevaluateDebugFlags() reevaluateDebugFlags()
@ -213,6 +213,14 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
} }
} }
fun resetSuggestions(content: EditorContent) {
if (!(activeState.isComposingEnabled || nlpManager.isSuggestionOn())) {
nlpManager.clearSuggestions()
return
}
nlpManager.suggest(subtypeManager.activeSubtype, content)
}
/** /**
* @return If the language switch should be shown. * @return If the language switch should be shown.
*/ */
@ -429,6 +437,9 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
private fun handleEnter() { private fun handleEnter() {
val info = editorInstance.activeInfo val info = editorInstance.activeInfo
val isShiftPressed = inputEventDispatcher.isPressed(KeyCode.SHIFT) val isShiftPressed = inputEventDispatcher.isPressed(KeyCode.SHIFT)
if (editorInstance.tryPerformEnterCommitRaw()) {
return
}
if (info.imeOptions.flagNoEnterAction || info.inputAttributes.flagTextMultiLine && isShiftPressed) { if (info.imeOptions.flagNoEnterAction || info.inputAttributes.flagTextMultiLine && isShiftPressed) {
editorInstance.performEnter() editorInstance.performEnter()
} else { } else {
@ -497,6 +508,21 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
activeState.inputShiftState = InputShiftState.UNSHIFTED activeState.inputShiftState = InputShiftState.UNSHIFTED
} }
/**
* Handles a hardware [KeyEvent.KEYCODE_SPACE] event. Same as [handleSpace],
* but skips handling changing to characters keyboard and double space periods.
*/
fun handleHardwareKeyboardSpace() {
val candidate = nlpManager.getAutoCommitCandidate()
candidate?.let { commitCandidate(it) }
// Skip handling changing to characters keyboard and double space periods
// TODO: this is whether we commit space after selecting candidate. Should be determined by SuggestionProvider
if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace &&
candidate != null) { /* Do nothing */ } else {
editorInstance.commitText(KeyCode.SPACE.toChar().toString())
}
}
/** /**
* Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if * Handles a [KeyCode.SPACE] event. Also handles the auto-correction of two space taps if
* enabled by the user. * enabled by the user.
@ -524,6 +550,7 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
} }
} }
} }
// TODO: this is whether we commit space after selecting candidate. Should be determined by SuggestionProvider
if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace && if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace &&
candidate != null) { /* Do nothing */ } else { candidate != null) { /* Do nothing */ } else {
editorInstance.commitText(KeyCode.SPACE.toChar().toString()) editorInstance.commitText(KeyCode.SPACE.toChar().toString())
@ -797,6 +824,20 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
} }
} }
fun onHardwareKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
when (keyCode) {
KeyEvent.KEYCODE_SPACE -> {
handleHardwareKeyboardSpace()
return true
}
KeyEvent.KEYCODE_ENTER -> {
handleEnter()
return true
}
else -> return false
}
}
inner class KeyboardManagerResources { inner class KeyboardManagerResources {
val composers = MutableLiveData<Map<ExtensionComponentName, Composer>>(emptyMap()) val composers = MutableLiveData<Map<ExtensionComponentName, Composer>>(emptyMap())
val currencySets = MutableLiveData<Map<ExtensionComponentName, CurrencySet>>(emptyMap()) val currencySets = MutableLiveData<Map<ExtensionComponentName, CurrencySet>>(emptyMap())

View File

@ -17,7 +17,6 @@
package dev.patrickgold.florisboard.ime.nlp package dev.patrickgold.florisboard.ime.nlp
import android.content.Context import android.content.Context
import android.icu.text.BreakIterator
import android.os.Build import android.os.Build
import android.os.SystemClock import android.os.SystemClock
import android.util.LruCache import android.util.LruCache
@ -71,6 +70,8 @@ class NlpManager(context: Context) {
HanShapeBasedLanguageProvider.ProviderId to ProviderInstanceWrapper(HanShapeBasedLanguageProvider(context)), HanShapeBasedLanguageProvider.ProviderId to ProviderInstanceWrapper(HanShapeBasedLanguageProvider(context)),
) )
} }
// lock unnecessary because values constant
private val providersForceSuggestionOn = mutableMapOf<String, Boolean>()
private val internalSuggestionsGuard = Mutex() private val internalSuggestionsGuard = Mutex()
private var internalSuggestions by Delegates.observable(SystemClock.uptimeMillis() to listOf<SuggestionCandidate>()) { _, _, _ -> private var internalSuggestions by Delegates.observable(SystemClock.uptimeMillis() to listOf<SuggestionCandidate>()) { _, _, _ ->
@ -174,12 +175,26 @@ class NlpManager(context: Context) {
) )
} }
suspend fun determineLocalComposing(textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup): EditorRange { suspend fun determineLocalComposing(
textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup, localLastCommitPosition: Int
): EditorRange {
return getSuggestionProvider(subtypeManager.activeSubtype).determineLocalComposing( return getSuggestionProvider(subtypeManager.activeSubtype).determineLocalComposing(
subtypeManager.activeSubtype, textBeforeSelection, breakIterators subtypeManager.activeSubtype, textBeforeSelection, breakIterators, localLastCommitPosition
) )
} }
fun providerForcesSuggestionOn(subtype: Subtype): Boolean {
// Using a cache because I have no idea how fast the runBlocking is
return providersForceSuggestionOn.getOrPut(subtype.nlpProviders.suggestion) {
runBlocking {
getSuggestionProvider(subtype).forcesSuggestionOn
}
}
}
fun isSuggestionOn(): Boolean =
prefs.suggestion.enabled.get() || providerForcesSuggestionOn(subtypeManager.activeSubtype)
fun suggest(subtype: Subtype, content: EditorContent) { fun suggest(subtype: Subtype, content: EditorContent) {
val reqTime = SystemClock.uptimeMillis() val reqTime = SystemClock.uptimeMillis()
scope.launch { scope.launch {
@ -242,7 +257,7 @@ class NlpManager(context: Context) {
private fun assembleCandidates() { private fun assembleCandidates() {
runBlocking { runBlocking {
val candidates = when { val candidates = when {
prefs.suggestion.enabled.get() -> { isSuggestionOn() -> {
clipboardSuggestionProvider.suggest( clipboardSuggestionProvider.suggest(
subtype = Subtype.DEFAULT, subtype = Subtype.DEFAULT,
content = editorInstance.activeContent, content = editorInstance.activeContent,

View File

@ -208,7 +208,8 @@ interface SuggestionProvider : NlpProvider {
suspend fun determineLocalComposing( suspend fun determineLocalComposing(
subtype: Subtype, subtype: Subtype,
textBeforeSelection: CharSequence, textBeforeSelection: CharSequence,
breakIterators: BreakIteratorGroup breakIterators: BreakIteratorGroup,
localLastCommitPosition: Int,
): EditorRange { ): EditorRange {
return breakIterators.word(subtype.primaryLocale) { return breakIterators.word(subtype.primaryLocale) {
it.setText(textBeforeSelection.toString()) it.setText(textBeforeSelection.toString())
@ -222,6 +223,9 @@ interface SuggestionProvider : NlpProvider {
} }
} }
} }
val forcesSuggestionOn
get() = false
} }
/** /**

View File

@ -230,39 +230,41 @@ class HanShapeBasedLanguageProvider(val context: Context) : SpellingProvider, Su
} }
override suspend fun getListOfWords(subtype: Subtype): List<String> { override suspend fun getListOfWords(subtype: Subtype): List<String> {
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return emptyList(); return emptyList()
val layout: String = languagePackItem.hanShapeBasedTable // val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return emptyList();
try { // val layout: String = languagePackItem.hanShapeBasedTable
val database = languagePackExtension.hanShapeBasedSQLiteDatabase // try {
val cur = database.query(layout, arrayOf ( "text" ), "", arrayOf(), "", "", "weight DESC, code ASC", ""); // val database = languagePackExtension.hanShapeBasedSQLiteDatabase
cur.moveToFirst(); // val cur = database.query(layout, arrayOf ( "text" ), "", arrayOf(), "", "", "weight DESC, code ASC", "");
val rowCount = cur.getCount(); // cur.moveToFirst();
val suggestions = buildList { // val rowCount = cur.getCount();
for (n in 0 until rowCount) { // val suggestions = buildList {
val word = cur.getString(0); // for (n in 0 until rowCount) {
cur.moveToNext(); // val word = cur.getString(0);
add(word) // cur.moveToNext();
} // add(word)
} // }
flogDebug { "Read ${suggestions.size} words for ${subtype.primaryLocale.localeTag()}" } // }
return suggestions; // flogDebug { "Read ${suggestions.size} words for ${subtype.primaryLocale.localeTag()}" }
} catch (e: SQLiteException) { // return suggestions;
flogError { "Encountered an SQL error: ${e}" } // } catch (e: SQLiteException) {
return emptyList(); // flogError { "Encountered an SQL error: ${e}" }
} // return emptyList();
// }
} }
override suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double { override suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double {
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return 0.0; return 0.0
val layout: String = languagePackItem.hanShapeBasedTable // val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return 0.0;
try { // val layout: String = languagePackItem.hanShapeBasedTable
val database = languagePackExtension.hanShapeBasedSQLiteDatabase // try {
val cur = database.query(layout, arrayOf ( "weight" ), "code = ?", arrayOf(word), "", "", "", ""); // val database = languagePackExtension.hanShapeBasedSQLiteDatabase
cur.moveToFirst(); // val cur = database.query(layout, arrayOf ( "weight" ), "code = ?", arrayOf(word), "", "", "", "");
return try { cur.getDouble(0) } catch (e: Exception) { 0.0 }; // cur.moveToFirst();
} catch (e: SQLiteException) { // return try { cur.getDouble(0) } catch (e: Exception) { 0.0 };
return 0.0; // } catch (e: SQLiteException) {
} // return 0.0;
// }
} }
override suspend fun destroy() { override suspend fun destroy() {
@ -270,14 +272,19 @@ class HanShapeBasedLanguageProvider(val context: Context) : SpellingProvider, Su
// the app process is killed (which will most likely always be the case). // the app process is killed (which will most likely always be the case).
} }
override suspend fun determineLocalComposing(subtype: Subtype, textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup): EditorRange { override suspend fun determineLocalComposing(
subtype: Subtype,
textBeforeSelection: CharSequence,
breakIterators: BreakIteratorGroup,
localLastCommitPosition: Int
): EditorRange {
return breakIterators.character(subtype.primaryLocale) { return breakIterators.character(subtype.primaryLocale) {
it.setText(textBeforeSelection.toString()) it.setText(textBeforeSelection.toString())
val end = it.last() val end = it.last()
var start = end var start = end
var next = it.previous() var next = it.previous()
val keyCodeLocale = keyCode[subtype.primaryLocale.localeTag()]?: keyCode["default"]?: emptySet() val keyCodeLocale = keyCode[subtype.primaryLocale.localeTag()]?: keyCode["default"]?: emptySet()
while (next != BreakIterator.DONE) { while (next != BreakIterator.DONE && start > localLastCommitPosition) {
val sub = textBeforeSelection.substring(next, start) val sub = textBeforeSelection.substring(next, start)
if (! sub.all { char -> char in keyCodeLocale }) if (! sub.all { char -> char in keyCodeLocale })
break break
@ -294,4 +301,6 @@ class HanShapeBasedLanguageProvider(val context: Context) : SpellingProvider, Su
} }
} }
override val forcesSuggestionOn
get() = true
} }