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

Add new SubtypeSupportInfo API to NlpProvider

This new API allows the UI to dynamically check if a subtype is supported by a given NLP Provider.
This commit is contained in:
Patrick Goldinger 2023-06-01 23:27:14 +02:00
parent 857e315e6c
commit e70a84bea7
No known key found for this signature in database
GPG Key ID: 533467C3DC7B9262
10 changed files with 224 additions and 30 deletions

View File

@ -46,6 +46,7 @@ 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.defaultFlorisOutlinedText
import dev.patrickgold.florisboard.lib.compose.florisHorizontalScroll
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.observeAsNonNullState
@ -252,7 +253,7 @@ private fun FlorisInvalidPluginBox(plugin: IndexedPlugin) {
val reason = (plugin.state as IndexedPluginState.Error).toString()
FlorisOutlinedBox(modifier = Modifier.defaultFlorisOutlinedBox()) {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
modifier = Modifier.defaultFlorisOutlinedText(),
text = "Unrecognised plugin with service name ${plugin.serviceName}\n\nReason: $reason",
color = MaterialTheme.colors.error,
fontStyle = FontStyle.Italic,

View File

@ -34,8 +34,10 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
@ -54,6 +56,7 @@ import dev.patrickgold.florisboard.R
import dev.patrickgold.florisboard.app.LocalNavController
import dev.patrickgold.florisboard.app.Routes
import dev.patrickgold.florisboard.app.florisPreferenceModel
import dev.patrickgold.florisboard.ime.core.ComputedSubtype
import dev.patrickgold.florisboard.ime.core.DisplayLanguageNamesIn
import dev.patrickgold.florisboard.ime.core.Subtype
import dev.patrickgold.florisboard.ime.core.SubtypeJsonConfig
@ -63,15 +66,19 @@ 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.nlp.SubtypeSupportInfo
import dev.patrickgold.florisboard.keyboardManager
import dev.patrickgold.florisboard.lib.FlorisLocale
import dev.patrickgold.florisboard.lib.compose.FlorisButtonBar
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownLikeButton
import dev.patrickgold.florisboard.lib.compose.FlorisDropdownMenu
import dev.patrickgold.florisboard.lib.compose.FlorisOutlinedBox
import dev.patrickgold.florisboard.lib.compose.FlorisScreen
import dev.patrickgold.florisboard.lib.compose.defaultFlorisOutlinedText
import dev.patrickgold.florisboard.lib.compose.stringRes
import dev.patrickgold.florisboard.lib.ext.ExtensionComponentName
import dev.patrickgold.florisboard.lib.observeAsNonNullState
import dev.patrickgold.florisboard.nlpManager
import dev.patrickgold.florisboard.subtypeManager
import dev.patrickgold.jetpref.datastore.model.observeAsState
import dev.patrickgold.jetpref.material.ui.JetPrefAlertDialog
@ -83,6 +90,7 @@ private val SelectComponentName = ExtensionComponentName("00", "00")
private val SelectNlpProviderId = SelectComponentName.toString()
private val SelectNlpProviders = SubtypeNlpProviderMap(
spelling = SelectNlpProviderId,
suggestion = SelectNlpProviderId,
)
private val SelectLayoutMap = SubtypeLayoutMap(
characters = SelectComponentName,
@ -96,6 +104,7 @@ private val SelectLayoutMap = SubtypeLayoutMap(
)
private val SelectLocale = FlorisLocale.from("00", "00")
private val SelectListKeys = listOf(SelectComponentName)
private val SelectNlpProviderKeys = listOf(SelectNlpProviderId)
private class SubtypeEditorState(init: Subtype?) {
companion object {
@ -124,10 +133,11 @@ private class SubtypeEditorState(init: Subtype?) {
val id: MutableState<Long> = mutableStateOf(init?.id ?: -1)
val primaryLocale: MutableState<FlorisLocale> = mutableStateOf(init?.primaryLocale ?: SelectLocale)
val secondaryLocales: MutableState<List<FlorisLocale>> = mutableStateOf(init?.secondaryLocales ?: listOf())
val nlpProviders: MutableState<SubtypeNlpProviderMap> = mutableStateOf(init?.nlpProviders ?: Subtype.FALLBACK.nlpProviders)
val nlpProviders: MutableState<SubtypeNlpProviderMap> = mutableStateOf(init?.nlpProviders ?: SelectNlpProviders)
val composer: MutableState<ExtensionComponentName> = mutableStateOf(init?.composer ?: SelectComponentName)
val currencySet: MutableState<ExtensionComponentName> = mutableStateOf(init?.currencySet ?: SelectComponentName)
val punctuationRule: MutableState<ExtensionComponentName> = mutableStateOf(init?.punctuationRule ?: Subtype.FALLBACK.punctuationRule)
val punctuationRule: MutableState<ExtensionComponentName> =
mutableStateOf(init?.punctuationRule ?: Subtype.FALLBACK.punctuationRule)
val popupMapping: MutableState<ExtensionComponentName> = mutableStateOf(init?.popupMapping ?: SelectComponentName)
val layoutMap: MutableState<SubtypeLayoutMap> = mutableStateOf(init?.layoutMap ?: SelectLayoutMap)
@ -168,14 +178,16 @@ private class SubtypeEditorState(init: Subtype?) {
@Composable
fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
title = stringRes(if (id == null) {
R.string.settings__localization__subtype_add_title
} else {
R.string.settings__localization__subtype_edit_title
})
title = stringRes(
if (id == null) {
R.string.settings__localization__subtype_add_title
} else {
R.string.settings__localization__subtype_edit_title
}
)
val selectValue = stringRes(R.string.settings__localization__subtype_select_placeholder)
val selectListValues = remember (selectValue) { listOf(selectValue) }
val selectListValues = remember(selectValue) { listOf(selectValue) }
val prefs by florisPreferenceModel()
val navController = LocalNavController.current
@ -183,6 +195,7 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
val configuration = LocalConfiguration.current
val lifecycleOwner = LocalLifecycleOwner.current
val keyboardManager by context.keyboardManager()
val nlpManager by context.nlpManager()
val subtypeManager by context.subtypeManager()
val displayLanguageNamesIn by prefs.localization.displayLanguageNamesIn.observeAsState()
@ -267,9 +280,10 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
content {
Column(modifier = Modifier.padding(8.dp)) {
if (id == null) {
Card(modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
Card(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp),
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
@ -303,7 +317,9 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
},
text = when (displayLanguageNamesIn) {
DisplayLanguageNamesIn.SYSTEM_LOCALE -> suggestedPreset.locale.displayName()
DisplayLanguageNamesIn.NATIVE_LOCALE -> suggestedPreset.locale.displayName(suggestedPreset.locale)
DisplayLanguageNamesIn.NATIVE_LOCALE -> suggestedPreset.locale.displayName(
suggestedPreset.locale
)
},
secondaryText = suggestedPreset.preferred.characters.componentId,
)
@ -373,16 +389,21 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
SubtypeGroupSpacer()
/*SubtypeProperty(stringRes(R.string.settings__localization__subtype_suggestion_provider)) {
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 plugins by nlpManager.plugins.pluginIndexFlow.collectAsState()
val nlpProviderMappings = remember(plugins) {
buildMap {
for (plugin in plugins) {
val packageContext = plugin.packageContext()
put(plugin.metadata.id, plugin.metadata.title.getOrNull(packageContext) ?: "??")
}
}
}
val nlpProviderMappingIds = remember(nlpProviderMappings) {
SelectListKeys + nlpProviderMappings.keys
SelectNlpProviderKeys + nlpProviderMappings.keys
}
val nlpProviderMappingLabels = remember(nlpProviderMappings) {
selectListValues + nlpProviderMappings.values.map { it }
@ -394,14 +415,64 @@ fun SubtypeEditorScreen(id: Long?) = FlorisScreen {
expanded = expanded,
selectedIndex = selectedIndex,
isError = showSelectAsError && selectedIndex == 0,
onSelectItem = { nlpProviders = SubtypeNlpProviderMap(
suggestion = nlpProviderMappingIds[it] as String,
spelling = nlpProviderMappingIds[it] as String
) },
onSelectItem = {
nlpProviders = SubtypeNlpProviderMap(
suggestion = nlpProviderMappingIds[it],
spelling = nlpProviderMappingIds[it],
)
},
onExpandRequest = { expanded = true },
onDismissRequest = { expanded = false },
)
}*/
val subtypeForPluginSupport = ComputedSubtype(
id = subtypeEditor.id.value,
primaryLocale = subtypeEditor.primaryLocale.value.languageTag(),
secondaryLocales = subtypeEditor.secondaryLocales.value.map { it.languageTag() },
)
val subtypeSupportInfo by produceState<SubtypeSupportInfo?>(
initialValue = null,
plugins,
subtypeForPluginSupport,
nlpProviders,
) {
value = nlpManager.plugins.getOrNull(nlpProviders.suggestion)
?.evaluateIsSupported(subtypeForPluginSupport)
}
val supportInfo = subtypeSupportInfo
if (supportInfo == null) {
FlorisOutlinedBox {
Text(
modifier = Modifier.defaultFlorisOutlinedText(),
text = "No plugin selected",
style = MaterialTheme.typography.body2,
)
}
} else if (supportInfo.isFullySupported()) {
FlorisOutlinedBox {
Text(
modifier = Modifier.defaultFlorisOutlinedText(),
text = "Supported",
style = MaterialTheme.typography.body2,
)
}
} else if (supportInfo.isPartiallySupported()) {
FlorisOutlinedBox {
Text(
modifier = Modifier.defaultFlorisOutlinedText(),
text = "Partially supported\nReason: ${supportInfo.reason}",
style = MaterialTheme.typography.body2,
)
}
} else if (supportInfo.isUnsupported()) {
FlorisOutlinedBox {
Text(
modifier = Modifier.defaultFlorisOutlinedText(),
text = "Unsupported\nReason: ${supportInfo.reason}",
style = MaterialTheme.typography.body2,
)
}
}
}
SubtypeGroupSpacer()
@ -610,7 +681,9 @@ private fun SubtypeLayoutDropdown(
@Composable
private fun SubtypeGroupSpacer() {
Spacer(modifier = Modifier
.fillMaxWidth()
.height(32.dp))
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(32.dp)
)
}

View File

@ -369,6 +369,10 @@ class NlpManager(context: Context) {
// Do nothing
}
override suspend fun evaluateIsSupported(subtype: ComputedSubtype): SubtypeSupportInfo {
return SubtypeSupportInfo.fullySupported()
}
override suspend fun preload(subtype: ComputedSubtype) {
// Do nothing
}

View File

@ -21,6 +21,7 @@ import dev.patrickgold.florisboard.ime.core.ComputedSubtype
import dev.patrickgold.florisboard.ime.keyboard.KeyProximityChecker
import dev.patrickgold.florisboard.ime.nlp.SpellingProvider
import dev.patrickgold.florisboard.ime.nlp.SpellingResult
import dev.patrickgold.florisboard.ime.nlp.SubtypeSupportInfo
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
import dev.patrickgold.florisboard.ime.nlp.SuggestionRequestFlags
@ -116,6 +117,16 @@ class LatinLanguageProviderService : FlorisPluginService(), SpellingProvider, Su
}
}
override suspend fun evaluateIsSupported(subtype: ComputedSubtype): SubtypeSupportInfo {
val baseDictionaries = getBaseDictionaryPaths(subtype)
return if (baseDictionaries.isNotEmpty()) {
SubtypeSupportInfo.fullySupported()
} else {
// TODO make string resource and translatable
SubtypeSupportInfo.unsupported("No dictionary could be found")
}
}
override suspend fun spell(
subtypeId: Long,
word: String,

View File

@ -301,3 +301,8 @@ fun Modifier.defaultFlorisOutlinedBox(): Modifier {
.fillMaxWidth()
.padding(vertical = 8.dp, horizontal = 16.dp)
}
fun Modifier.defaultFlorisOutlinedText(): Modifier {
return this
.padding(vertical = 8.dp, horizontal = 16.dp)
}

View File

@ -30,6 +30,7 @@ import dev.patrickgold.florisboard.BuildConfig
import dev.patrickgold.florisboard.ime.core.ComputedSubtype
import dev.patrickgold.florisboard.ime.nlp.SpellingProvider
import dev.patrickgold.florisboard.ime.nlp.SpellingResult
import dev.patrickgold.florisboard.ime.nlp.SubtypeSupportInfo
import dev.patrickgold.florisboard.ime.nlp.SuggestionCandidate
import dev.patrickgold.florisboard.ime.nlp.SuggestionProvider
import dev.patrickgold.florisboard.ime.nlp.SuggestionRequest
@ -67,6 +68,20 @@ class IndexedPlugin(
connection.bindService(serviceName)
}
override suspend fun evaluateIsSupported(subtype: ComputedSubtype): SubtypeSupportInfo {
val message = FlorisPluginMessage.requestToService(
action = FlorisPluginMessage.ACTION_EVALUATE_IS_SUPPORTED,
id = messageIdGenerator.getAndIncrement(),
data = Json.encodeToString(subtype),
)
connection.sendMessage(message)
return withTimeoutOrNull(5000L) {
val replyMessage = connection.replyMessages.first { it.id == message.id }
val resultData = replyMessage.data ?: return@withTimeoutOrNull null
return@withTimeoutOrNull Json.decodeFromString(resultData)
} ?: SubtypeSupportInfo.unsupported("!! Error in communication with plugin !!")
}
override suspend fun preload(subtype: ComputedSubtype) {
val message = FlorisPluginMessage.requestToService(
action = FlorisPluginMessage.ACTION_PRELOAD,

View File

@ -32,6 +32,15 @@ interface NlpProvider {
*/
suspend fun create()
/**
* Is called to check if the language provider supports the subtype using its primary/secondary language options.
*
* @param subtype Information about the subtype to check, primarily used for getting the primary and secondary
* language for correct dictionary selection.
* @return true if this provider is able to provide NLP services for given subtype, false otherwise.
*/
suspend fun evaluateIsSupported(subtype: ComputedSubtype): SubtypeSupportInfo
/**
* Is called at least once before a task specific request occurs, to allow for locale-specific preloading of
* dictionaries and language models.

View File

@ -0,0 +1,58 @@
/*
* Copyright (C) 2023 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
import kotlinx.serialization.Serializable
@Serializable
data class SubtypeSupportInfo(
val state: SubtypeSupportState,
val reason: String?,
) {
fun isFullySupported(): Boolean {
return state == SubtypeSupportState.FullySupported
}
fun isPartiallySupported(): Boolean {
return state == SubtypeSupportState.PartiallySupported
}
fun isUnsupported(): Boolean {
return state == SubtypeSupportState.Unsupported
}
companion object {
fun fullySupported(): SubtypeSupportInfo {
return SubtypeSupportInfo(SubtypeSupportState.FullySupported, null)
}
fun partiallySupported(reason: String? = null): SubtypeSupportInfo {
return SubtypeSupportInfo(SubtypeSupportState.PartiallySupported, reason)
}
fun unsupported(reason: String? = null): SubtypeSupportInfo {
return SubtypeSupportInfo(SubtypeSupportState.Unsupported, reason)
}
}
}
@Serializable
enum class SubtypeSupportState {
FullySupported,
PartiallySupported,
Unsupported;
}

View File

@ -46,9 +46,10 @@ data class FlorisPluginMessage(
const val TYPE_REQUEST = 1
const val TYPE_RESPONSE = 2
const val ACTION_PRELOAD = 1
const val ACTION_SPELL = 2
const val ACTION_SUGGEST = 3
const val ACTION_EVALUATE_IS_SUPPORTED = 1
const val ACTION_PRELOAD = 2
const val ACTION_SPELL = 3
const val ACTION_SUGGEST = 4
private const val M_SOURCE = 0x0000000F
private val O_SOURCE = M_SOURCE.countTrailingZeroBits()

View File

@ -106,6 +106,23 @@ abstract class FlorisPluginService : Service(), NlpProvider {
when (type) {
FlorisPluginMessage.TYPE_REQUEST -> when (action) {
FlorisPluginMessage.ACTION_EVALUATE_IS_SUPPORTED -> processAction("EVALUATE_IS_SUPPORTED") {
val data = message.data ?: error("Request message contains no data")
val id = message.id
val replyToMessenger = message.replyTo ?: error("Request message contains no replyTo field")
val subtype = Json.decodeFromString<ComputedSubtype>(data)
service.scope.launch {
flogDebug { "ACTION_EVALUATE_IS_SUPPORTED: $subtype" }
val info = service.evaluateIsSupported(subtype)
val responseMessage = FlorisPluginMessage.replyToConsumer(
action = FlorisPluginMessage.ACTION_EVALUATE_IS_SUPPORTED,
id = id,
data = Json.encodeToString(info),
)
replyToMessenger.send(responseMessage.toAndroidMessage())
}
}
FlorisPluginMessage.ACTION_PRELOAD -> processAction("PRELOAD") {
val data = message.data ?: error("Request message contains no data")
val subtype = Json.decodeFromString<ComputedSubtype>(data)