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

Add Chinese Shape Based Layouts (#2054)

* feat(ime/nlp): Add `HanShapeBasedLanguageProvider`

* feat: Manually set default NLP to be HanShapeBased

* feat: Temporarily disable adding spaces

This commit should give insight into how the keyboard adds spaces, this
should then be refined into not adding a space after commiting a CJK
text suggestion

* fix(ime/nlp): Remove empty str suggest in HanShape

* feat(ime/nlp): Handle locale variants in HanShape

this should facilitate multiple layouts in the zh locale

* fix(ime/nlp): Handle query params in HanShape

This also helps performance as the DBC doesn't have to compile the query
for every string the user writes

* Space behavior QoL updates for Han shape-based layout (#1)

* Separate space behavior for zh* and latin, and allow space when there is no suggestion.

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

* Add checking if locale is CJK

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

* refactor: Change predicate to a getter & rename

* chore: Remove TODO `supportsAutoSpace` message

* fix: Fix spaces after sugg. in non-space subtypes

* fix: Fix auto space predicate in `PhantomSpace`

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>
Co-authored-by: waelwindows <waelwindows9922@gmail.com>

* Draft: editor screen exposes nlpProviders and shape-based Chinese input methods as variants

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

* Fix defaults for zhengma preset

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

* Add word tables for added input methods

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

* Fix: bug in zhengma preset

* Draft: support composing with special characters by delegating nlpProvider to decide composing range.

* Catch SQLite errors such as layout (locale variant) not found (e.g. using HanShapeBased with JIS)

* fixup: remove TODO

* fix: partly addresses 2101, allow searching for locale in English for phones lacking system locale IME

* Adds support for importing "language packs" (sqlite3 db for HanShapeBased for now)

* Changes language pack to zip files. Adds a basic language pack class for storing metadata of IMEs.

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

* Implement language pack as a type of Flex extension, and draft its import and view UI

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

* fix: input method name translation

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

* Trim down to zhengma, quickclassic, and cangjie for the barebones Chinese shape-based pack. Polish extension user documentation.

* Fix hack to allow multiple language pack extensions to co-exist.

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

* Replace quickclassic with boshiamy

* Fix href in LANGUAGEPACKS.md

* build(nix): Clean up nix flake

* refactor: Encapsulate lanaguage pack query in HSB

* feat(ime/nlp): Implement `getListOfWords` in HSB

* feat(ime/nlp): Implement `getFrequencyForWord`

* chore: Normalize weights for freq in `han.sqlite3`

* chore(ime/nlp): Add some logging for HSB

* Update app/src/main/assets/ime/keyboard/org.florisboard.localization/extension.json

Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>

Signed-off-by: moonbeamcelery <moonbeamcelery@proton.me>
Co-authored-by: moonbeamcelery <114041522+moonbeamcelery@users.noreply.github.com>
Co-authored-by: moonbeamcelery <moonbeamcelery@proton.me>
Co-authored-by: Patrick Goldinger <patrick@patrickgold.dev>
This commit is contained in:
Waelwindows 2023-01-15 16:22:10 +00:00 committed by GitHub
parent 9776ac1812
commit a5dab5fb5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1316 additions and 25 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use_flake

4
.gitignore vendored
View File

@ -43,3 +43,7 @@ crowdin.properties
# C++
.cxx/
# Nix stuff
.direnv/
result

67
LANGUAGEPACKS.md Normal file
View File

@ -0,0 +1,67 @@
# Language Packs
## Languages
- [Summary](#summary)
- [Chinese / 中文](#chinese--中文)
## Summary
Stub.
This page should describe how language packs work, how to import them, and point to the location of downloadable
language packs.
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

@ -646,6 +646,21 @@
"preferred": {
"characters": "org.florisboard.layouts:urdu_phonetic"
}
},
{
"languageTag": "zh-CN-zhengma",
"composer": "org.florisboard.composers:appender",
"nlpProviders": {
"spelling": "org.florisboard.nlp.providers.han.shape",
"suggestion": "org.florisboard.nlp.providers.han.shape"
},
"currencySet": "org.florisboard.currencysets:yen",
"popupMapping": "org.florisboard.localization:en",
"preferred": {
"characters": "org.florisboard.layouts:qwerty",
"symbols": "org.florisboard.layouts:cjk",
"symbols2": "org.florisboard.layouts:cjk"
}
},
{
"languageTag": "bn-BD",

View File

@ -0,0 +1,32 @@
{
"$": "ime.extension.languagepack",
"meta": {
"id": "org.florisboard.hanshapebasedbasicpack",
"version": "0.0.0",
"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.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>", "waelwindows", "moonbeamcelery" ],
"homepage": "https://github.com/florisboard/florisboard/blob/master/LANGUAGEPACKS.md#default-barebones-chinese-shape-based-pack",
"license": "apache-2.0"
},
"items": [
{
"id": "zh_CN_zhengma",
"label": "中文 (中国) [郑码] / Chinese (China) [ZHENGMA]",
"authors": [ "waelwindows", "moonbeamcelery" ],
"hanShapeBasedKeyCode": "abcdefghijklmnopqrstuvwxyz"
},
{
"id": "zh_TW_boshiamy",
"label": "中文 (台灣) [嘸蝦米] / Chinese (Taiwan) [BOSHIAMY]",
"authors": [ "waelwindows", "moonbeamcelery" ],
"hanShapeBasedKeyCode": ",.'abcdefghijklmnopqrstuvwxyz[]"
},
{
"id": "zh_TW_cangjielarge",
"label": "中文 (台灣) [倉頡-大字庫] / Chinese (Taiwan) [CANGJIELARGE]",
"authors": [ "waelwindows", "moonbeamcelery" ],
"hanShapeBasedKeyCode": "abcdefghijklmnopqrstuvwxyz&"
}
]
}

View File

@ -0,0 +1,23 @@
{
"$": "ime.extension.languagepack",
"meta": {
"id": "org.florisboard.languagepack",
"version": "0.0.0",
"title": "Default language pack",
"description": "Default language pack which are always available.",
"maintainers": [ "patrickgold <patrick@patrickgold.dev>" ],
"license": "apache-2.0"
},
"items": [
{
"id": "de_DE_neobone",
"label": "Deutsch (Deutschland) [NEOBONE] / German (Germany) [NEOBONE]",
"authors": [ "patrickgold" ]
},
{
"id": "ja_JP_jis",
"label": "日本語 (日本) [JIS] / Japanese (Japan) [JIS]",
"authors": [ "patrickgold" ]
}
]
}

View File

@ -44,6 +44,8 @@ import dev.patrickgold.florisboard.app.settings.dictionary.UserDictionaryType
import dev.patrickgold.florisboard.app.settings.gestures.GesturesScreen
import dev.patrickgold.florisboard.app.settings.keyboard.InputFeedbackScreen
import dev.patrickgold.florisboard.app.settings.keyboard.KeyboardScreen
import dev.patrickgold.florisboard.app.settings.localization.LanguagePackManagerScreen
import dev.patrickgold.florisboard.app.settings.localization.LanguagePackManagerScreenAction
import dev.patrickgold.florisboard.app.settings.localization.LocalizationScreen
import dev.patrickgold.florisboard.app.settings.localization.SelectLocaleScreen
import dev.patrickgold.florisboard.app.settings.localization.SubtypeEditorScreen
@ -67,6 +69,9 @@ object Routes {
const val Localization = "settings/localization"
const val SelectLocale = "settings/localization/select-locale"
const val LanguagePackManager = "settings/localization/language-pack-manage/{action}"
fun LanguagePackManager(action: LanguagePackManagerScreenAction) =
LanguagePackManager.curlyFormat("action" to action.id)
const val SubtypeAdd = "settings/localization/subtype/add"
const val SubtypeEdit = "settings/localization/subtype/edit/{id}"
fun SubtypeEdit(id: Long) = SubtypeEdit.curlyFormat("id" to id)
@ -147,6 +152,12 @@ object Routes {
composable(Settings.Localization) { LocalizationScreen() }
composable(Settings.SelectLocale) { SelectLocaleScreen() }
composable(Settings.LanguagePackManager) { navBackStack ->
val action = navBackStack.arguments?.getString("action")?.let { actionId ->
LanguagePackManagerScreenAction.values().firstOrNull { it.id == actionId }
}
LanguagePackManagerScreen(action)
}
composable(Settings.SubtypeAdd) { SubtypeEditorScreen(null) }
composable(Settings.SubtypeEdit) { navBackStack ->
val id = navBackStack.arguments?.getString("id")?.toLongOrNull()

View File

@ -37,6 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.ime.nlp.LanguagePackComponent
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponent
import dev.patrickgold.florisboard.lib.compose.FlorisIconButton
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
@ -96,6 +97,22 @@ fun ExtensionComponentView(
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
)
}
is LanguagePackComponent -> {
val text = remember(
component.authors, component.locale, component.hanShapeBasedKeyCode,
) {
buildString {
appendLine("authors = ${component.authors}")
appendLine("locale = ${component.locale.localeTag()}")
appendLine("hanShapeBasedKeyCode = ${component.hanShapeBasedKeyCode}")
}
}
Text(
text = text,
style = MaterialTheme.typography.body2,
color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current),
)
}
else -> { }
}
}

View File

@ -48,6 +48,7 @@ import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
import dev.patrickgold.florisboard.lib.NATIVE_NULLPTR
import dev.patrickgold.florisboard.lib.android.showLongToast
@ -60,6 +61,7 @@ import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import dev.patrickgold.florisboard.lib.io.FileRegistry
import dev.patrickgold.florisboard.lib.kotlin.resultOk
@ -82,6 +84,11 @@ enum class ExtensionImportScreenType(
id = "ext-theme",
titleResId = R.string.ext__import__ext_theme,
supportedFiles = listOf(FileRegistry.FlexExtension),
),
EXT_LANGUAGEPACK(
id = "ext-languagepack",
titleResId = R.string.ext__import__ext_languagepack,
supportedFiles = listOf(FileRegistry.FlexExtension),
);
}
@ -174,6 +181,9 @@ fun ExtensionImportScreen(type: ExtensionImportScreenType, initUuid: String?) =
ExtensionImportScreenType.EXT_THEME -> {
ext.takeIf { it is ThemeExtension }?.let { extensionManager.import(it) }
}
ExtensionImportScreenType.EXT_LANGUAGEPACK -> {
ext.takeIf { it is LanguagePackExtension }?.let { extensionManager.import(it) }
}
}
}
}.onSuccess {

View File

@ -46,6 +46,7 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.ime.theme.ThemeExtension
import dev.patrickgold.florisboard.ime.theme.ThemeExtensionComponentImpl
import dev.patrickgold.florisboard.lib.android.showLongToast
@ -174,6 +175,18 @@ private fun ViewScreen(ext: Extension) = FlorisScreen {
)
}
}
is LanguagePackExtension -> {
ExtensionComponentListView(
title = stringRes(R.string.ext__meta__components_language_pack),
components = ext.items,
) { component ->
ExtensionComponentView(
modifier = Modifier.defaultFlorisOutlinedBox(),
meta = ext.meta,
component = component,
)
}
}
else -> {
// Render nothing
}

View File

@ -0,0 +1,216 @@
/*
* Copyright (C) 2021 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.app.settings.localization
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RadioButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.ext.ExtensionImportScreenType
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.nlp.LanguagePackComponent
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.lib.android.showLongToast
import dev.patrickgold.florisboard.lib.compose.FlorisConfirmDeleteDialog
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisTextButton
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.rippleClickable
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.Extension
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.datastore.ui.ExperimentalJetPrefDatastoreUi
import dev.patrickgold.jetpref.datastore.ui.Preference
import dev.patrickgold.jetpref.material.ui.JetPrefListItem
enum class LanguagePackManagerScreenAction(val id: String) {
MANAGE("manage-installed-language-packs");
}
// TODO: this file is based on ThemeManagerScreen.kt and can arguably be merged.
@OptIn(ExperimentalJetPrefDatastoreUi::class)
@Composable
fun LanguagePackManagerScreen(action: LanguagePackManagerScreenAction?) = FlorisScreen {
title = stringRes(when (action) {
LanguagePackManagerScreenAction.MANAGE -> R.string.settings__localization__language_pack_title
else -> error("LanguagePack manager screen action must not be null")
})
val prefs by florisPreferenceModel()
val navController = LocalNavController.current
val context = LocalContext.current
val extensionManager by context.extensionManager()
val indexedLanguagePackExtensions by extensionManager.languagePacks.observeAsNonNullState()
val selectedManagerLanguagePackId = remember { mutableStateOf<ExtensionComponentName?>(null) }
val extGroupedLanguagePacks = remember(indexedLanguagePackExtensions) {
buildMap<String, List<LanguagePackComponent>> {
for (ext in indexedLanguagePackExtensions) {
put(ext.meta.id, ext.items)
}
}.mapValues { (_, configs) -> configs.sortedBy { it.label } }
}
fun getLanguagePackIdPref(): Nothing = TODO("Not implemented yet")
fun setLanguagePack(extId: String, componentId: String) {
val extComponentName = ExtensionComponentName(extId, componentId)
when (action) {
LanguagePackManagerScreenAction.MANAGE -> {
selectedManagerLanguagePackId.value = extComponentName
}
}
}
val activeLanguagePackId by when (action) {
LanguagePackManagerScreenAction.MANAGE -> selectedManagerLanguagePackId
}
var languagePackExtToDelete by remember { mutableStateOf<Extension?>(null) }
content {
val grayColor = LocalContentColor.current.copy(alpha = 0.56f)
if (action == LanguagePackManagerScreenAction.MANAGE) {
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
) {
this@content.Preference(
onClick = { navController.navigate(
Routes.Ext.Import(ExtensionImportScreenType.EXT_LANGUAGEPACK, null)
) },
iconId = R.drawable.ic_input,
title = stringRes(R.string.action__import),
)
}
}
for ((extensionId, configs) in extGroupedLanguagePacks) key(extensionId) {
val ext = extensionManager.getExtensionById(extensionId)!!
FlorisOutlinedBox(
modifier = Modifier.defaultFlorisOutlinedBox(),
title = ext.meta.title,
onTitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
subtitle = extensionId,
onSubtitleClick = { navController.navigate(Routes.Ext.View(extensionId)) },
) {
Column(
// Allowing horizontal scroll to fit translations in descriptions.
Modifier.horizontalScroll(rememberScrollState()).width(intrinsicSize = IntrinsicSize.Max),
) {
for (config in configs) key(extensionId, config.id) {
JetPrefListItem(
modifier = Modifier.rippleClickable {
setLanguagePack(extensionId, config.id)
},
// icon = {
// RadioButton(
// selected = activeLanguagePackId?.extensionId == extensionId &&
// activeLanguagePackId?.componentId == config.id,
// onClick = null,
// )
// },
text = config.label,
// trailing = {
// Icon(
// modifier = Modifier.size(ButtonDefaults.IconSize),
// painter = painterResource(if (config.isNightLanguagePack) {
// R.drawable.ic_dark_mode
// } else {
// R.drawable.ic_light_mode
// }),
// contentDescription = null,
// tint = grayColor,
// )
// },
)
}
}
if (action == LanguagePackManagerScreenAction.MANAGE && extensionManager.canDelete(ext)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 6.dp),
) {
FlorisTextButton(
onClick = {
languagePackExtToDelete = ext
},
icon = painterResource(R.drawable.ic_delete),
text = stringRes(R.string.action__delete),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colors.error,
),
)
Spacer(modifier = Modifier.weight(1f))
// FlorisTextButton(
// onClick = {
// navController.navigate(Routes.Ext.Edit(ext.meta.id))
// },
// icon = painterResource(R.drawable.ic_edit),
// text = stringRes(R.string.action__edit),
// )
}
}
}
}
if (languagePackExtToDelete != null) {
FlorisConfirmDeleteDialog(
onConfirm = {
runCatching {
extensionManager.delete(languagePackExtToDelete!!)
}.onFailure { error ->
context.showLongToast(
R.string.error__snackbar_message,
"error_message" to error.localizedMessage,
)
}
languagePackExtToDelete = null
},
onDismiss = { languagePackExtToDelete = null },
what = languagePackExtToDelete!!.meta.title,
)
}
}
}

View File

@ -16,6 +16,8 @@
package dev.patrickgold.florisboard.app.settings.localization
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExtendedFloatingActionButton
import androidx.compose.material.Icon
@ -30,12 +32,22 @@ import androidx.compose.ui.unit.dp
import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.settings.advanced.Restore
import dev.patrickgold.florisboard.app.settings.theme.ThemeManagerScreenAction
import dev.patrickgold.florisboard.cacheManager
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.android.readToFile
import dev.patrickgold.florisboard.lib.android.showLongToast
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.FlorisWarningCard
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.io.ZipUtils
import dev.patrickgold.florisboard.lib.io.parentDir
import dev.patrickgold.florisboard.lib.io.subFile
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.subtypeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
@ -53,6 +65,7 @@ fun LocalizationScreen() = FlorisScreen {
val context = LocalContext.current
val keyboardManager by context.keyboardManager()
val subtypeManager by context.subtypeManager()
val cacheManager by context.cacheManager()
floatingActionButton {
ExtendedFloatingActionButton(
@ -67,12 +80,21 @@ fun LocalizationScreen() = FlorisScreen {
)
}
content {
ListPreference(
prefs.localization.displayLanguageNamesIn,
title = stringRes(R.string.settings__localization__display_language_names_in__label),
entries = DisplayLanguageNamesIn.listEntries(),
)
Preference(
// iconId = R.drawable.ic_edit,
title = stringRes(R.string.settings__localization__language_pack_title),
summary = stringRes(R.string.settings__localization__language_pack_summary),
onClick = {
navController.navigate(Routes.Settings.LanguagePackManager(LanguagePackManagerScreenAction.MANAGE))
},
)
PreferenceGroup(title = stringRes(R.string.settings__localization__group_subtypes__label)) {
val subtypes by subtypeManager.subtypesFlow.collectAsState()
if (subtypes.isEmpty()) {

View File

@ -38,6 +38,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
@ -64,14 +65,15 @@ fun SelectLocaleScreen() = FlorisScreen {
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
var searchTermValue by remember { mutableStateOf(TextFieldValue()) }
val systemLocales = remember(displayLanguageNamesIn) {
FlorisLocale.installedSystemLocales().sortedBy { locale ->
val context = LocalContext.current
val systemLocales =
FlorisLocale.extendedAvailableLocales(context).sortedBy { locale ->
when (displayLanguageNamesIn) {
DisplayLanguageNamesIn.SYSTEM_LOCALE -> locale.displayName()
DisplayLanguageNamesIn.NATIVE_LOCALE -> locale.displayName(locale)
}.lowercase()
}
}
val filteredSystemLocales = remember(searchTermValue) {
if (searchTermValue.text.isBlank()) {
systemLocales
@ -80,6 +82,7 @@ fun SelectLocaleScreen() = FlorisScreen {
systemLocales.filter { locale ->
locale.displayName().lowercase().contains(term) ||
locale.displayName(locale).lowercase().contains(term) ||
locale.displayName(FlorisLocale.ENGLISH).lowercase().contains(term) ||
locale.languageTag().lowercase().startsWith(term) ||
locale.localeTag().lowercase().startsWith(term)
}

View File

@ -63,6 +63,9 @@ import dev.patrickgold.florisboard.ime.core.SubtypePreset
import dev.patrickgold.florisboard.ime.keyboard.LayoutArrangementComponent
import dev.patrickgold.florisboard.ime.keyboard.LayoutType
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
import dev.patrickgold.florisboard.ime.text.key.KeyCode
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
@ -136,6 +139,7 @@ private class SubtypeEditorState(init: Subtype?) {
primaryLocale.value = subtype.primaryLocale
secondaryLocales.value = subtype.secondaryLocales
composer.value = subtype.composer
nlpProviders.value = subtype.nlpProviders
currencySet.value = subtype.currencySet
punctuationRule.value = subtype.punctuationRule
popupMapping.value = subtype.popupMapping
@ -201,6 +205,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
var currencySet by subtypeEditor.currencySet
var popupMapping by subtypeEditor.popupMapping
var layoutMap by subtypeEditor.layoutMap
var nlpProviders by subtypeEditor.nlpProviders
var showSubtypePresetsDialog by rememberSaveable { mutableStateOf(false) }
var showSelectAsError by rememberSaveable { mutableStateOf(false) }
@ -371,6 +376,38 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
SubtypeGroupSpacer()
SubtypeProperty(stringRes(R.string.settings__localization__subtype_suggestion_provider)) {
// TODO: Put this map somewhere more formal (another KeyboardExtension field?)
// optionally use a string resource below
val nlpProviderMappings = mapOf(
LatinLanguageProvider.ProviderId to "Latin",
HanShapeBasedLanguageProvider.ProviderId to "Chinese shape-based"
)
val nlpProviderMappingIds = remember(nlpProviderMappings) {
SelectListKeys + nlpProviderMappings.keys
}
val nlpProviderMappingLabels = remember(nlpProviderMappings) {
selectListValues + nlpProviderMappings.values.map { it }
}
var expanded by remember { mutableStateOf(false) }
val selectedIndex = nlpProviderMappingIds.indexOf(nlpProviders.suggestion).coerceAtLeast(0)
FlorisDropdownMenu(
items = nlpProviderMappingLabels,
expanded = expanded,
selectedIndex = selectedIndex,
isError = showSelectAsError && selectedIndex == 0,
onSelectItem = { nlpProviders = SubtypeNlpProviderMap(
suggestion = nlpProviderMappingIds[it] as String,
spelling = nlpProviderMappingIds[it] as String
) },
onExpandRequest = { expanded = true },
onDismissRequest = { expanded = false },
)
}
SubtypeGroupSpacer()
SubtypeProperty(stringRes(R.string.settings__localization__subtype_symbols_layout)) {
val layoutType = LayoutType.SYMBOLS
SubtypeLayoutDropdown(

View File

@ -24,6 +24,7 @@ import dev.patrickgold.florisboard.ime.keyboard.extCoreLayout
import dev.patrickgold.florisboard.ime.keyboard.extCorePopupMapping
import dev.patrickgold.florisboard.ime.keyboard.extCorePunctuationRule
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import kotlinx.serialization.SerialName

View File

@ -31,6 +31,7 @@ import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.kotlin.guardedByLock
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.MutableStateFlow
@ -53,6 +54,7 @@ abstract class AbstractEditorInstance(context: Context) {
private val keyboardManager by context.keyboardManager()
private val subtypeManager by context.subtypeManager()
private val nlpManager by context.nlpManager()
private val scope = MainScope()
protected val breakIterators = BreakIteratorGroup()
@ -274,17 +276,7 @@ abstract class AbstractEditorInstance(context: Context) {
}
private suspend fun determineLocalComposing(textBeforeSelection: CharSequence): EditorRange {
return breakIterators.word(subtypeManager.activeSubtype.primaryLocale) {
it.setText(textBeforeSelection.toString())
val end = it.last()
val isWord = it.ruleStatus != BreakIterator.WORD_NONE
if (isWord) {
val start = it.previous()
EditorRange(start, end)
} else {
EditorRange.Unspecified
}
}
return nlpManager.determineLocalComposing(textBeforeSelection, breakIterators)
}
private fun InputConnection.setComposingRegion(composing: EditorRange) {

View File

@ -42,6 +42,7 @@ import dev.patrickgold.florisboard.lib.android.AndroidVersion
import dev.patrickgold.florisboard.lib.android.showShortToast
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager
import kotlinx.coroutines.runBlocking
import java.util.concurrent.atomic.AtomicInteger
@ -54,6 +55,7 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
private val appContext by context.appContext()
private val clipboardManager by context.clipboardManager()
private val keyboardManager by context.keyboardManager()
private val subtypeManager by context.subtypeManager()
private val nlpManager by context.nlpManager()
private val activeState get() = keyboardManager.activeState
@ -512,15 +514,16 @@ class EditorInstance(context: Context) : AbstractEditorInstance(context) {
}
private fun PhantomSpaceState.determine(text: String, forceActive: Boolean = false): Boolean {
val content = activeContent
val selection = content.selection
if (!(isActive || forceActive) || selection.isNotValid || selection.start <= 0 || text.isEmpty()) return false
val textBefore = content.getTextBeforeCursor(1)
val punctuationRule = nlpManager.getActivePunctuationRule()
return textBefore.isNotEmpty() &&
(punctuationRule.symbolsPrecedingPhantomSpace.contains(textBefore[textBefore.length - 1]) ||
textBefore[textBefore.length - 1].isLetterOrDigit()) &&
(punctuationRule.symbolsFollowingPhantomSpace.contains(text[0]) || text[0].isLetterOrDigit())
val content = activeContent
val selection = content.selection
if (!(isActive || forceActive) || selection.isNotValid || selection.start <= 0 || text.isEmpty()) return false
val textBefore = content.getTextBeforeCursor(1)
val punctuationRule = nlpManager.getActivePunctuationRule()
if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace) return false;
return textBefore.isNotEmpty() &&
(punctuationRule.symbolsPrecedingPhantomSpace.contains(textBefore[textBefore.length - 1]) ||
textBefore[textBefore.length - 1].isLetterOrDigit()) &&
(punctuationRule.symbolsFollowingPhantomSpace.contains(text[0]) || text[0].isLetterOrDigit())
}
class AutoSpaceState {

View File

@ -502,7 +502,8 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
* enabled by the user.
*/
private fun handleSpace(data: KeyData) {
nlpManager.getAutoCommitCandidate()?.let { commitCandidate(it) }
val candidate = nlpManager.getAutoCommitCandidate()
candidate?.let { commitCandidate(it) }
if (prefs.keyboard.spaceBarSwitchesToCharacters.get()) {
when (activeState.keyboardMode) {
KeyboardMode.NUMERIC_ADVANCED,
@ -523,7 +524,10 @@ class KeyboardManager(context: Context) : InputKeyEventReceiver {
}
}
}
editorInstance.commitText(KeyCode.SPACE.toChar().toString())
if (!subtypeManager.activeSubtype.primaryLocale.supportsAutoSpace &&
candidate != null) { /* Do nothing */ } else {
editorInstance.commitText(KeyCode.SPACE.toChar().toString())
}
}
/**

View File

@ -0,0 +1,92 @@
package dev.patrickgold.florisboard.ime.nlp
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.assetManager
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.android.copy
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.ext.Extension
import dev.patrickgold.florisboard.lib.ext.ExtensionComponent
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.ext.ExtensionEditor
import dev.patrickgold.florisboard.lib.ext.ExtensionMeta
import dev.patrickgold.florisboard.lib.io.FlorisRef
import dev.patrickgold.florisboard.lib.io.FsDir
import dev.patrickgold.florisboard.lib.io.parentDir
import dev.patrickgold.florisboard.lib.io.subFile
import dev.patrickgold.florisboard.lib.kotlin.tryOrNull
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@Serializable
class LanguagePackComponent(
override val id: String,
override val label: String,
override val authors: List<String>,
val locale: FlorisLocale = FlorisLocale.fromTag(id),
val hanShapeBasedKeyCode: String = "abcdefghijklmnopqrstuvwxyz",
) : ExtensionComponent {
@Transient var parent: LanguagePackExtension? = null
@SerialName("hanShapeBasedTable")
private val _hanShapeBasedTable: String? = null // Allows overriding the sqlite3 table to query in the json
val hanShapeBasedTable
get() = _hanShapeBasedTable ?: locale.variant
}
@SerialName(LanguagePackExtension.SERIAL_TYPE)
@Serializable
class LanguagePackExtension( // FIXME: how to make this support multiple types of language packs, and selectively load?
override val meta: ExtensionMeta,
override val dependencies: List<String>? = null,
val items: List<LanguagePackComponent> = listOf(),
val hanShapeBasedSQLite: String = "han.sqlite3",
) : Extension() {
override fun components(): List<ExtensionComponent> = items
override fun edit(): ExtensionEditor {
TODO("LOL LMAO")
}
companion object {
const val SERIAL_TYPE = "ime.extension.languagepack"
}
override fun serialType() = SERIAL_TYPE
@Transient var hanShapeBasedSQLiteDatabase: SQLiteDatabase = SQLiteDatabase.create(null)
override fun onAfterLoad(context: Context, cacheDir: FsDir) {
// FIXME: this is loading language packs of all subtypes when they load.
super.onAfterLoad(context, cacheDir)
val databasePath = workingDir?.subFile(hanShapeBasedSQLite)?.path
if (databasePath == null) {
flogError { "Han shape-based language pack not found or loaded" }
} else try {
// TODO: use lock on database?
hanShapeBasedSQLiteDatabase.takeIf { it.isOpen }?.close()
hanShapeBasedSQLiteDatabase =
SQLiteDatabase.openDatabase(databasePath, null, SQLiteDatabase.OPEN_READONLY);
} catch (e: SQLiteException) {
flogError { "SQLiteException in openDatabase: path=$databasePath, error='${e}'" }
}
}
override fun onBeforeUnload(context: Context, cacheDir: FsDir) {
super.onBeforeUnload(context, cacheDir)
hanShapeBasedSQLiteDatabase.takeIf { it.isOpen }?.close()
}
}

View File

@ -17,6 +17,7 @@
package dev.patrickgold.florisboard.ime.nlp
import android.content.Context
import android.icu.text.BreakIterator
import android.os.Build
import android.os.SystemClock
import android.util.LruCache
@ -32,7 +33,9 @@ import dev.patrickgold.florisboard.editorInstance
import dev.patrickgold.florisboard.ime.clipboard.provider.ItemType
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.editor.EditorContent
import dev.patrickgold.florisboard.ime.editor.EditorRange
import dev.patrickgold.florisboard.ime.nlp.latin.LatinLanguageProvider
import dev.patrickgold.florisboard.ime.nlp.han.HanShapeBasedLanguageProvider
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.lib.kotlin.collectLatestIn
@ -65,6 +68,7 @@ class NlpManager(context: Context) {
private val providers = guardedByLock {
mapOf(
LatinLanguageProvider.ProviderId to ProviderInstanceWrapper(LatinLanguageProvider(context)),
HanShapeBasedLanguageProvider.ProviderId to ProviderInstanceWrapper(HanShapeBasedLanguageProvider(context)),
)
}
@ -170,6 +174,12 @@ class NlpManager(context: Context) {
)
}
suspend fun determineLocalComposing(textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup): EditorRange {
return getSuggestionProvider(subtypeManager.activeSubtype).determineLocalComposing(
subtypeManager.activeSubtype, textBeforeSelection, breakIterators
)
}
fun suggest(subtype: Subtype, content: EditorContent) {
val reqTime = SystemClock.uptimeMillis()
scope.launch {

View File

@ -16,8 +16,10 @@
package dev.patrickgold.florisboard.ime.nlp
import android.icu.text.BreakIterator
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.editor.EditorContent
import dev.patrickgold.florisboard.ime.editor.EditorRange
/**
* Base interface for any NLP provider implementation. NLP providers maintain their own internal state and only receive
@ -193,6 +195,33 @@ interface SuggestionProvider : NlpProvider {
* exist, 0.0 should be returned.
*/
suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double
/**
* When initializing composing text given a new context, the suggestion engine determines the composing range.
* The default behavior gets the last word according to the current subtype's primaryLocale.
* @param subtype The current subtype used to determine word or character boundary.
* @param textBeforeSelection The text whose end we want to compose.
* @param breakIterators cache of BreakIterator(s) to determine boundary.
*
* @return EditorRange indicating composing range.
*/
suspend fun determineLocalComposing(
subtype: Subtype,
textBeforeSelection: CharSequence,
breakIterators: BreakIteratorGroup
): EditorRange {
return breakIterators.word(subtype.primaryLocale) {
it.setText(textBeforeSelection.toString())
val end = it.last()
val isWord = it.ruleStatus != BreakIterator.WORD_NONE
if (isWord) {
val start = it.previous()
EditorRange(start, end)
} else {
EditorRange.Unspecified
}
}
}
}
/**

View File

@ -0,0 +1,297 @@
/*
* Copyright (C) 2022 Patrick Goldinger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dev.patrickgold.florisboard.ime.nlp.han
import android.content.Context
import android.database.sqlite.SQLiteException
import android.icu.text.BreakIterator
import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.editor.EditorContent
import dev.patrickgold.florisboard.ime.editor.EditorRange
import dev.patrickgold.florisboard.ime.nlp.BreakIteratorGroup
import dev.patrickgold.florisboard.ime.nlp.LanguagePackComponent
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.ime.nlp.SpellingProvider
import dev.patrickgold.florisboard.ime.nlp.SpellingResult
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
import dev.patrickgold.florisboard.ime.nlp.WordSuggestionCandidate
import dev.patrickgold.florisboard.lib.devtools.flogDebug
import dev.patrickgold.florisboard.lib.devtools.flogError
import dev.patrickgold.florisboard.subtypeManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class HanShapeBasedLanguageProvider(val context: Context) : SpellingProvider, SuggestionProvider {
companion object {
// Default user ID used for all subtypes, unless otherwise specified.
// See `ime/core/Subtype.kt` Line 210 and 211 for the default usage
const val ProviderId = "org.florisboard.nlp.providers.han.shape"
const val DB_PATH = "han.sqlite3";
}
private val appContext by context.appContext()
private val maxFreqBySubType = mutableMapOf<String, Int>();
private val extensionManager by context.extensionManager()
private val subtypeManager by context.subtypeManager()
private val allLanguagePacks: List<LanguagePackExtension>
// Assume other types of extensions do not extend LanguagePackExtension
get() = extensionManager.languagePacks.value ?: listOf()
private var __connectedActiveLanguagePacks: Set<LanguagePackExtension> = setOf() // FIXME: hack for not able to observe extensionManager.languagePacks and subtypeManager.subtypes
private var languagePackItems: Map<String, LanguagePackComponent> = mapOf() // init in refreshLanguagePacks()
private var keyCode: Map<String, Set<Char>> = mapOf() // init in refreshLanguagePacks()
private val activeLanguagePacks // language packs referenced in subtypes
get() = buildSet {
val locales = subtypeManager.subtypes.map { it.primaryLocale.localeTag() }.toSet()
for (languagePack in allLanguagePacks) {
// FIXME: skip checking language pack type because it always is for now
if (languagePack.items.any { it.locale.localeTag() in locales }) {
add(languagePack)
}
}
}
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) // same as NlpManager's preload()
override val providerId = ProviderId
init {
// // FIXME: observeForever only callable on the main thread.
// extensionManager.languagePacks.observeForever { refreshLanguagePacks() }
}
private fun refreshLanguagePacks() {
scope.launch { create() }
}
override suspend fun create() {
// Here we initialize our provider, set up all things which are not language dependent.
// Refresh language pack parsing
// build index of available language packs
languagePackItems = buildMap {
for (languagePack in allLanguagePacks) {
// FIXME: skip checking language pack type because it always is for now
// if (languagePack is HanShapeBasedLanguagePackExtensionImpl)
for (languagePackItem in languagePack.items) {
put(languagePackItem.locale.localeTag(), languagePackItem)
// FIXME: how to put this in deserialization?
languagePackItem.parent = languagePack
}
}
}.toMap()
keyCode = buildMap {
languagePackItems.forEach { (tag, languagePackItem) ->
put(tag, languagePackItem.hanShapeBasedKeyCode.toSet())
}
put("default", "abcdefghijklmnopqrstuvwxyz".toSet())
}.toMap()
// Load all actively used language packs.
val activeLanguagePacks = activeLanguagePacks
for (activeLanguagePack in activeLanguagePacks) {
if (!activeLanguagePack.isLoaded()) {
// populates activeLanguagePack.hanShapeBasedSQLiteDatabase
// FIXME: every time this is copied over to cache.
activeLanguagePack.load(context)
}
}
__connectedActiveLanguagePacks = activeLanguagePacks
}
override suspend fun preload(subtype: Subtype) = withContext(Dispatchers.IO) {
// Here we have the chance to preload dictionaries and prepare a neural network for a specific language.
// Is kept in sync with the active keyboard subtype of the user, however a new preload does not necessary mean
// the previous language is not needed anymore (e.g. if the user constantly switches between two subtypes)
// To read a file from the APK assets the following methods can be used:
// appContext.assets.open()
// appContext.assets.reader()
// appContext.assets.bufferedReader()
// appContext.assets.readText()
// To copy an APK file/dir to the file system cache (appContext.cacheDir), the following methods are available:
// appContext.assets.copy()
// appContext.assets.copyRecursively()
// The subtype we get here contains a lot of data, however we are only interested in subtype.primaryLocale and
// subtype.secondaryLocales.
}
override suspend fun spell(
subtype: Subtype,
word: String,
precedingWords: List<String>,
followingWords: List<String>,
maxSuggestionCount: Int,
allowPossiblyOffensive: Boolean,
isPrivateSession: Boolean,
): SpellingResult {
return when (word.lowercase()) {
// Use typo for typing errors
"typo" -> SpellingResult.typo(arrayOf("typo1", "typo2", "typo3"))
// Use grammar error if the algorithm can detect this. On Android 11 and lower grammar errors are visually
// marked as typos due to a lack of support
"gerror" -> SpellingResult.grammarError(arrayOf("grammar1", "grammar2", "grammar3"))
// Use valid word for valid input
else -> SpellingResult.validWord()
}
}
override suspend fun suggest(
subtype: Subtype,
content: EditorContent,
maxCandidateCount: Int,
allowPossiblyOffensive: Boolean,
isPrivateSession: Boolean,
): List<SuggestionCandidate> {
if (__connectedActiveLanguagePacks != activeLanguagePacks) {
// FIXME: hack for not able to observe extensionManager.languagePacks
refreshLanguagePacks()
}
if (content.composingText.isEmpty()) {
return emptyList();
}
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return emptyList();
val layout: String = languagePackItem.hanShapeBasedTable
try {
val database = languagePackExtension.hanShapeBasedSQLiteDatabase
val cur = database.query(layout, arrayOf ( "code", "text" ), "code LIKE ? || '%'", arrayOf(content.composingText), "", "", "code ASC, weight DESC", "$maxCandidateCount");
cur.moveToFirst();
val rowCount = cur.getCount();
flogDebug { "Query was '${content.composingText}'" }
val suggestions = buildList {
for (n in 0 until rowCount) {
val code = cur.getString(0);
val word = cur.getString(1);
cur.moveToNext();
add(WordSuggestionCandidate(
text = "$word",
secondaryText = code,
confidence = 0.5,
isEligibleForAutoCommit = n == 0,
// We set ourselves as the source provider so we can get notify events for our candidate
sourceProvider = this@HanShapeBasedLanguageProvider,
))
}
}
return suggestions
} catch (e: IllegalStateException) {
flogError { "Invalid layout '${layout}' not found" }
return emptyList();
} catch (e: SQLiteException) {
flogError { "SQLiteException: layout=$layout, composing=${content.composingText}, error='${e}'" }
return emptyList();
}
}
override suspend fun notifySuggestionAccepted(subtype: Subtype, candidate: SuggestionCandidate) {
// We can use flogDebug, flogInfo, flogWarning and flogError for debug logging, which is a wrapper for Logcat
flogDebug { candidate.toString() }
}
override suspend fun notifySuggestionReverted(subtype: Subtype, candidate: SuggestionCandidate) {
flogDebug { candidate.toString() }
}
override suspend fun removeSuggestion(subtype: Subtype, candidate: SuggestionCandidate): Boolean {
flogDebug { candidate.toString() }
return false
}
fun getLanguagePack(subtype: Subtype): Pair<LanguagePackComponent, LanguagePackExtension>? {
val languagePackItem = languagePackItems[subtype.primaryLocale.localeTag()]
val languagePackExtension = languagePackItem?.parent
if (languagePackItem == null || languagePackExtension == null) {
flogError { "Could not read language pack item / extension" }
return null;
}
return Pair(languagePackItem, languagePackExtension)
}
override suspend fun getListOfWords(subtype: Subtype): List<String> {
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return emptyList();
val layout: String = languagePackItem.hanShapeBasedTable
try {
val database = languagePackExtension.hanShapeBasedSQLiteDatabase
val cur = database.query(layout, arrayOf ( "text" ), "", arrayOf(), "", "", "weight DESC, code ASC", "");
cur.moveToFirst();
val rowCount = cur.getCount();
val suggestions = buildList {
for (n in 0 until rowCount) {
val word = cur.getString(0);
cur.moveToNext();
add(word)
}
}
flogDebug { "Read ${suggestions.size} words for ${subtype.primaryLocale.localeTag()}" }
return suggestions;
} catch (e: SQLiteException) {
flogError { "Encountered an SQL error: ${e}" }
return emptyList();
}
}
override suspend fun getFrequencyForWord(subtype: Subtype, word: String): Double {
val (languagePackItem, languagePackExtension) = getLanguagePack(subtype) ?: return 0.0;
val layout: String = languagePackItem.hanShapeBasedTable
try {
val database = languagePackExtension.hanShapeBasedSQLiteDatabase
val cur = database.query(layout, arrayOf ( "weight" ), "code = ?", arrayOf(word), "", "", "", "");
cur.moveToFirst();
return try { cur.getDouble(0) } catch (e: Exception) { 0.0 };
} catch (e: SQLiteException) {
return 0.0;
}
}
override suspend fun destroy() {
// Here we have the chance to de-allocate memory and finish our work. However this might never be called if
// the app process is killed (which will most likely always be the case).
}
override suspend fun determineLocalComposing(subtype: Subtype, textBeforeSelection: CharSequence, breakIterators: BreakIteratorGroup): EditorRange {
return breakIterators.character(subtype.primaryLocale) {
it.setText(textBeforeSelection.toString())
val end = it.last()
var start = end
var next = it.previous()
val keyCodeLocale = keyCode[subtype.primaryLocale.localeTag()]?: keyCode["default"]?: emptySet()
while (next != BreakIterator.DONE) {
val sub = textBeforeSelection.substring(next, start)
if (! sub.all { char -> char in keyCodeLocale })
break
start = next
next = it.previous()
}
if (start != end) {
flogDebug { "Determined $start - $end as composing: ${textBeforeSelection.substring(start, end)}" }
EditorRange(start, end)
} else {
flogDebug { "Determined Unspecified as composing" }
EditorRange.Unspecified
}
}
}
}

View File

@ -16,6 +16,9 @@
package dev.patrickgold.florisboard.lib
import android.content.Context
import dev.patrickgold.florisboard.extensionManager
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.lib.kotlin.titlecase
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
@ -125,6 +128,31 @@ class FlorisLocale private constructor(val base: Locale) {
* @see java.util.Locale.getAvailableLocales
*/
fun installedSystemLocales(): List<FlorisLocale> = Locale.getAvailableLocales().map { from(it) }
/**
* Returns a list of all installed locales and custom locales.
*
*/
fun extendedAvailableLocales(context: Context): List<FlorisLocale> {
val systemLocales = installedSystemLocales()
val extensionManager by context.extensionManager()
val systemLocalesSet = buildSet {
for (locale in systemLocales) {
add(locale.localeTag())
}
}.toSet()
val extraLocales = buildList {
for (languagePackExtension in extensionManager.languagePacks.value ?: listOf()) {
for (languagePackItem in languagePackExtension.items) {
val locale = languagePackItem.locale
if (from(locale.language, locale.country).localeTag() in systemLocalesSet) {
add(locale.localeTag())
}
}
}
}.toSet()
return systemLocales + extraLocales.map { fromTag(it) }
}
}
/**
@ -195,6 +223,16 @@ class FlorisLocale private constructor(val base: Locale) {
else -> true
}
/**
* Returns true if suggestions in this language should have spaces added after, false otherwise.
* TODO: this is absolutely not exhaustive and hard-coded, find solution based on ICU or system
*/
val supportsAutoSpace: Boolean
get() = when (language) {
"zh", "ko", "jp", "th" -> false
else -> true
}
/**
* Generates the language tag for this locale in the format `xx`,
* `xx-YY` or `xx-YY-zzz` and returns it as a string.

View File

@ -23,6 +23,7 @@ import androidx.lifecycle.LiveData
import dev.patrickgold.florisboard.appContext
import dev.patrickgold.florisboard.assetManager
import dev.patrickgold.florisboard.ime.keyboard.KeyboardExtension
import dev.patrickgold.florisboard.ime.nlp.LanguagePackExtension
import dev.patrickgold.florisboard.ime.text.composing.Appender
import dev.patrickgold.florisboard.ime.text.composing.Composer
import dev.patrickgold.florisboard.ime.text.composing.HangulUnicode
@ -62,6 +63,7 @@ val ExtensionJsonConfig = Json {
polymorphic(Extension::class) {
subclass(KeyboardExtension::class, KeyboardExtension.serializer())
subclass(ThemeExtension::class, ThemeExtension.serializer())
subclass(LanguagePackExtension::class, LanguagePackExtension.serializer())
}
polymorphic(Composer::class) {
subclass(Appender::class, Appender.serializer())
@ -77,6 +79,7 @@ class ExtensionManager(context: Context) {
companion object {
const val IME_KEYBOARD_PATH = "ime/keyboard"
const val IME_THEME_PATH = "ime/theme"
const val IME_LANGUAGEPACK_PATH = "ime/languagepack"
private const val FILE_OBSERVER_MASK =
FileObserver.CLOSE_WRITE or FileObserver.DELETE or FileObserver.MOVED_FROM or FileObserver.MOVED_TO
@ -88,10 +91,12 @@ class ExtensionManager(context: Context) {
val keyboardExtensions = ExtensionIndex(KeyboardExtension.serializer(), IME_KEYBOARD_PATH)
val themes = ExtensionIndex(ThemeExtension.serializer(), IME_THEME_PATH)
val languagePacks = ExtensionIndex(LanguagePackExtension.serializer(), IME_LANGUAGEPACK_PATH)
fun init() {
keyboardExtensions.init()
themes.init()
languagePacks.init()
}
fun import(ext: Extension) {
@ -100,6 +105,7 @@ class ExtensionManager(context: Context) {
val relGroupPath = when (ext) {
is KeyboardExtension -> IME_KEYBOARD_PATH
is ThemeExtension -> IME_THEME_PATH
is LanguagePackExtension -> IME_LANGUAGEPACK_PATH
else -> error("Unknown extension type")
}
ext.sourceRef = FlorisRef.internal(relGroupPath).subRef(extFileName)
@ -125,6 +131,7 @@ class ExtensionManager(context: Context) {
fun getExtensionById(id: String): Extension? {
keyboardExtensions.value?.find { it.meta.id == id }?.let { return it }
themes.value?.find { it.meta.id == id }?.let { return it }
languagePacks.value?.find { it.meta.id == id }?.let { return it }
return null
}

View File

@ -101,10 +101,13 @@
<string name="settings__localization__display_language_names_in__label" comment="Label of Display language names in preference">Display language names in</string>
<string name="settings__localization__group_subtypes__label" comment="Label of subtypes group">Subtypes</string>
<string name="settings__localization__subtype_add_title" comment="Title of subtype dialog when adding a new subtype">Add subtype</string>
<string name="settings__localization__language_pack_title" comment="Title of the language pack manager screen for managing installed and custom language packs">Manage installed language packs</string>
<string name="settings__localization__language_pack_summary" comment="Summary of preference item for adding a new language pack">Experimental: manage extensions that add support for specific languages (shape-based Chinese input for now)</string>
<string name="settings__localization__subtype_edit_title" comment="Title of subtype dialog when editing an existing subtype">Edit subtype</string>
<string name="settings__localization__subtype_locale" comment="Label for locale dropdown in subtype dialog">Primary language</string>
<string name="settings__localization__subtype_popup_mapping" comment="Label for popup mapping dropdown in subtype screen">Popup mapping</string>
<string name="settings__localization__subtype_characters_layout" comment="Label for layout dropdown in subtype dialog">Characters layout</string>
<string name="settings__localization__subtype_suggestion_provider" comment="Label for suggestion provider dropdown in subtype dialog">Suggestion engine</string>
<string name="settings__localization__subtype_symbols_layout" comment="Label for layout dropdown in subtype dialog">Symbols primary layout</string>
<string name="settings__localization__subtype_symbols2_layout" comment="Label for layout dropdown in subtype dialog">Symbols secondary layout</string>
<string name="settings__localization__subtype_composer" comment="Label for composer dropdown in subtype dialog.">Composer</string>
@ -599,6 +602,7 @@
<string name="ext__meta__authors">Authors</string>
<string name="ext__meta__components">Bundled components</string>
<string name="ext__meta__components_theme">Bundled themes</string>
<string name="ext__meta__components_language_pack">Bundled language packs</string>
<string name="ext__meta__components_none_found">This extension archive does not contain any bundled components.</string>
<string name="ext__meta__description">Description</string>
<string name="ext__meta__homepage">Homepage</string>
@ -638,6 +642,7 @@
<string name="ext__import__ext_any" comment="Title of Importer screen for import of any supported FlorisBoard extension">Import extension</string>
<string name="ext__import__ext_keyboard" comment="Title of Importer screen for keyboard extension import">Import keyboard extension</string>
<string name="ext__import__ext_theme" comment="Title of Importer screen for theme extension import">Import theme extension</string>
<string name="ext__import__ext_languagepack" comment="Title of Importer screen for language pack extension import">Import language pack extension</string>
<string name="ext__import__file_skip" comment="Label when a file cannot be imported in the current context. The actual reason string is in a separate text view below this string.">File can not be imported. Reason:</string>
<string name="ext__import__file_skip_unsupported" comment="Reason string when file is unsupported">Unsupported or unrecognized file type.</string>
<string name="ext__import__file_skip_ext_core" comment="Reason string when ext has core extension ID">Unable to replace or update default extension packages provided with the app core assets. Consider updating the app itself if you intend to use a newer version of a core extension package.</string>

42
flake.lock Normal file
View File

@ -0,0 +1,42 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1673387755,
"narHash": "sha256-7y2wuml3bnR2jRgSpDTdeKx9HeAuGW7rYpk4G+Ibeuk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ce1aa29621356706746c53e2d480da7c68f6c972",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

81
flake.nix Normal file
View File

@ -0,0 +1,81 @@
{
description = "florisboard build environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}: let
supportedSystems = with flake-utils.lib.system; [x86_64-linux x86_64-darwin aarch64-darwin];
forAllSystems = f: nixpkgs.lib.genAttrs supportedSystems (system: f system);
android = {
versions = {
tools = "26.1.1";
platformTools = "33.0.3";
buildTools = "31.0.0";
ndk = "22.1.7171670";
cmake = "3.18.1";
emulator = "31.3.9";
};
platforms = ["32"];
};
in
flake-utils.lib.eachSystem supportedSystems
(
system: let
pkgs = import nixpkgs {
inherit system;
config = {
android_sdk.accept_license = true; # accept all of the sdk licenses
allowUnfree = true; # needed to get android stuff to compile
};
};
# Make the android enviornment we specify
android-composition = pkgs.androidenv.composeAndroidPackages {
toolsVersion = android.versions.tools;
platformToolsVersion = android.versions.platformTools;
buildToolsVersions = [android.versions.buildTools];
platformVersions = android.platforms;
cmakeVersions = [android.versions.cmake];
includeNDK = true;
ndkVersions = [android.versions.ndk];
includeEmulator = true;
emulatorVersion = android.versions.emulator;
};
android-sdk =
(pkgs.androidenv.composeAndroidPackages {
toolsVersion = android.versions.tools;
platformToolsVersion = android.versions.platformTools;
buildToolsVersions = [android.versions.buildTools];
platformVersions = android.platforms;
cmakeVersions = [android.versions.cmake];
includeNDK = true;
ndkVersions = [android.versions.ndk];
})
.androidsdk;
in rec {
devShells.default = pkgs.mkShell rec {
ANDROID_SDK_ROOT = "${android-sdk}/libexec/android-sdk";
ANDROID_NDK_ROOT = "${ANDROID_SDK_ROOT}/ndk-bundle";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${android.versions.buildTools}/aapt2";
JAVA_HOME = "${pkgs.jdk8.home}";
nativeBulidInputs = with pkgs; [
android-sdk
jdk8
clang
kotlin-language-server
];
# Use the same cmakeVersion here
shellHook = ''
export PATH="$(echo "$ANDROID_SDK_ROOT/cmake/${android.versions.cmake}".*/bin):$PATH"
'';
};
}
);
}

View File

@ -0,0 +1,219 @@
#!/usr/bin/env python3
# Execute in this folder to convert to sqlite:
# python3 convert_fcitx5_sqlite.py
# Or for a subset of tables only:
# python3 convert_fcitx5_sqlite.py cangjie-large.txt quick-classic.txt wubi-large.txt zhengma.txt
# https://github.com/fcitx/fcitx5-table-extra/tree/master/tables
# The tables are in public domain per their README.
import os
import sys
import re
import json
import glob
import sqlite3
import collections
def put_table(database, schema, table):
length, table = table['LengthReal'], table['Data']
assert re.fullmatch('[a-zA-Z0-9_]+', schema) is not None
columns = len(table[0])
assert all(len(x) == columns for x in table)
with sqlite3.connect(database) as con:
cur = con.cursor()
if columns == 3:
cur.execute(f'create table {schema}(code VARCHAR({length}), text TEXT, weight INT)')
cur.executemany(f'insert into {schema} values(?, ?, ?)', table)
elif columns == 4:
# hard-coded 5-long stem
length_stem = max(len(x[3]) for x in table if x[3] is not None)
cur.execute(f'create table {schema}(code VARCHAR({length}), text TEXT, weight INT, stem VARCHAR({length_stem}))')
cur.executemany(f'insert into {schema} values(?, ?, ?, ?)', table)
else:
raise ValueError(f'Number of columns ({columns}) not supported')
fcitx_fields_translate = {
'组词规则': 'Rule',
'数据': 'Data',
'提示': 'Prompt',
'拼音长度': 'PinyinLength',
'键码': 'KeyCode',
'拼音': 'Pinyin',
'码长': 'Length',
'构词': 'ConstructPhrase',
}
def parse_fcitx_table(table):
with open(table, 'rt') as f:
lines = [line.strip('\n') for line in f.readlines()]
parsed = dict()
field_now = ''
for idx, line in enumerate(lines):
if '\ufeff' in line:
line = line.replace('\ufeff', '')
if not line or line.startswith(';'):
continue
if line.startswith('[') and line.endswith(']'):
# starting a table
field_now = line[1:-1]
field_now = fcitx_fields_translate.get(field_now, field_now)
table_now = parsed[field_now] = []
else:
if field_now:
# appending to a table
if field_now == 'Data':
# Parse first ' ' or '\t' as splitting point.
# Assume ' ' and '\t' may be in the text.
split = len(line)
for x in ' \t':
try:
split = min(split, line.index(x))
except ValueError:
pass
if split == len(line):
print(f'Throwing away row with one column:')
print(repr(line))
line = None
else:
line = (line[:split], line[split+1:])
# elif field_now == 'Rule':
else:
line = line.split('=')
assert len(line) == 2
# else:
# raise ValueError(f'Table field {field_now} not recognized')
if line is not None:
table_now.append(line)
else:
# parsing other settings
assert '=' in line, f'{table} has line without "=":\n{line}'
split = line.index('=')
field = line[:split]
field = fcitx_fields_translate.get(field, field)
parsed[field] = line[split+1:]
return parsed
def clean_fcitx_table(table):
# process Data with special field.
out = dict(table)
# compute actual KeyCode used.
keycode_real = set()
for x in out['Data']:
keycode_real |= set(x[0])
# Prompt: just add to word list and KeyCode.
if 'Prompt' in out and out['Prompt'] in keycode_real:
out['KeyCode'] += out['Prompt']
# Pinyin: just add to word list and KeyCode.
if 'Pinyin' in out and out['Pinyin'] in keycode_real:
out['KeyCode'] += out['Pinyin']
# ConstructPhrase: add to "stem" column. (for zhengma_large)
if 'ConstructPhrase' in out and out['ConstructPhrase'] in keycode_real:
conchar = out['ConstructPhrase']
# separate constructing and non-constructing parts of the table
table_noncon = [x for x in out['Data'] if conchar not in x[0]]
table_con = [(x[0][1:], x[1]) for x in out['Data'] if conchar in x[0]]
# do a join on text
dict_con = {x[1]: x[0] for x in table_con}
assert len(table_con) == len(dict_con), \
'ConstructPhrase entries not unique'
assert all(not conchar in x for x in dict_con.values()), \
'ConstructPhrase appearing after starts'
out['Data'] = [(x[0], x[1], dict_con.get(x[1], None))
for x in table_noncon]
# Weight: just use order.
counter = collections.Counter(x[0] for x in out['Data'])
for idx, x in enumerate(out['Data']):
weight = counter[x[0]]
counter.subtract((x[0],))
x = x[:2] + (weight,) + x[2:]
out['Data'][idx] = x
assert not len(list(counter.elements()))
# compute KeyCodeReal one more time after trimming table
keycode_real = set()
for x in out['Data']:
keycode_real |= set(x[0])
out['KeyCodeReal'] = keycode_real
# actual seek length
out['LengthReal'] = max(len(x[0]) for x in out['Data'])
return out
# Loading
tables = dict()
file_list = sys.argv[1:] if len(sys.argv) > 1 else glob.glob('[a-z]*.txt')
assert all(x.endswith('.txt') for x in file_list)
for x in file_list:
print(f'Processing {x}...')
schema = x[:-4].replace('-', '').replace('_', '')
tables[schema] = parse_fcitx_table(x)
conf = parse_fcitx_table(x[:-4] + '.conf.in')
conf = {k: {x[0]: x[1] for x in v} for k, v in conf.items()}
tables[schema]['.conf.in'] = conf
tables[schema]['FlorisLocale'] = f"{conf['InputMethod']['LangCode']}_{schema}"
# Fixing
if 'wubi98_pinyin' in tables:
tables['wubi98pinyin']['KeyCode'] += 'z'
keycode = set(tables['wubi98pinyin']['KeyCode']) | set(tables['wubi98pinyin']['Pinyin'])
for idx, x in enumerate(tables['wubi98pinyin']['Data']):
if not all(ch in keycode for ch in x[0]):
x = list(x)
x[0] = ''.join(ch for ch in x[0] if ch in keycode)
tables['wubi98pinyin']['Data'][idx] = tuple(x)
if 'easylarge' in tables:
tables['easylarge']['KeyCode'] += '|'
# Cleaning
for schema, table in tables.items():
print(f'Cleaning {schema}, with {len(table["Data"])} items...', end='')
tables[schema] = clean_fcitx_table(table)
print(f' Done, with {len(tables[schema]["Data"])} items.')
# Analysis
if True:
for schema, table in tables.items():
print(f'Analyzing {schema}... LengthReal = {table["LengthReal"]}')
specials = ["Prompt", "Pinyin", "ConstructPhrase"]
for field in specials:
if field in table:
has = [x for x in table['Data'] if table[field] in x[0]]
if has:
print(f'There are {len(has)}/{len(table["Data"])} with {field}={table[field]}')
keycode = set(table['KeyCode'])
keycode_real = set(table['KeyCodeReal'])
if keycode != keycode_real:
print(f'KeyCode mismatch:')
print(f'Claimed not used: ' + ''.join(sorted(keycode - keycode_real)))
print(f'Exists unclaimed: ' + ''.join(sorted(keycode_real - keycode)))
# Writing
language_pack = [dict(id=table['FlorisLocale'], hanShapeBasedKeyCode=table['KeyCode']) for schema, table in tables.items()]
with open('./extension-draft.json', 'wt') as f:
json.dump({'$': 'ime.extension.languagepack', 'items': sorted(language_pack, key=lambda x: x['id'])}, f, indent=2)
database = './han.sqlite3'
if os.path.exists(database):
os.remove(database)
for schema, table in tables.items():
put_table(database, schema, table)
# put_table(database, table['FlorisLocale'], table)
print({schema: table['KeyCode'] for schema, table in tables.items()})
# Final display
with sqlite3.connect(database) as con:
cur = con.cursor()
# for schema in ['zh_CN_zhengmapinyin', 'zh_CN_zhengmalarge', 'zh_CN_wubilarge', 'zh_CN_wubi98', 'zh_TW_cangjie5', 'zh_HK_stroke5']:
for schema in ['zhengmapinyin', 'zhengmalarge', 'wubilarge', 'wubi98', 'cangjie5', 'stroke5']:
if schema not in tables: continue
cur.execute(f'select * from {schema} order by length(code) desc')
print(cur.fetchmany(10))