Add initial inline suggestions support

This commit is contained in:
Aleksandras Kostarevas 2023-08-26 20:04:56 +03:00
parent 9d4ea1f7c1
commit bd0368d89f
5 changed files with 205 additions and 20 deletions

View File

@ -129,6 +129,7 @@ dependencies {
implementation 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'androidx.datastore:datastore-preferences:1.0.0'
implementation 'androidx.autofill:autofill:1.1.0'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'

View File

@ -115,7 +115,8 @@
<input-method xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsActivity="org.futo.inputmethod.latin.settings.SettingsActivity"
android:isDefault="@bool/im_is_default"
android:supportsSwitchingToNextInputMethod="true">
android:supportsSwitchingToNextInputMethod="true"
android:supportsInlineSuggestions="true">
<subtype android:icon="@drawable/ic_ime_switcher_dark"
android:label="@string/subtype_en_US"
android:subtypeId="0xc9194f98"

View File

@ -2,11 +2,17 @@ package org.futo.inputmethod.latin
import android.content.res.Configuration
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.CompletionInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestion
import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
import android.view.inputmethod.InputMethodSubtype
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -54,6 +60,7 @@ import org.futo.inputmethod.latin.uix.DynamicThemeProvider
import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner
import org.futo.inputmethod.latin.uix.KeyboardManagerForAction
import org.futo.inputmethod.latin.uix.THEME_KEY
import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest
import org.futo.inputmethod.latin.uix.deferGetSetting
import org.futo.inputmethod.latin.uix.deferSetSetting
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
@ -113,6 +120,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
return currWindowAction != null
}
private var inlineSuggestions: List<InlineSuggestion> = listOf()
private fun recreateKeyboard() {
legacyInputView = latinIMELegacy.onCreateInputView()
latinIMELegacy.loadKeyboard()
@ -224,13 +233,20 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
@Composable
private fun MainKeyboardViewWithActionBar() {
Column {
if (shouldShowSuggestionStrip) {
// Don't show suggested words when it's not meant to be shown
val suggestedWordsOrNull = if(shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
ActionBar(
suggestedWords,
suggestedWordsOrNull,
latinIMELegacy,
inlineSuggestions = inlineSuggestions,
onActionActivated = { onActionActivated(it) }
)
}
LegacyKeyboardView()
}
}
@ -506,4 +522,17 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
deferSetSetting(THEME_KEY, newTheme.key)
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest {
return createInlineSuggestionsRequest(this, this.activeColorScheme)
}
@RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
inlineSuggestions = response.inlineSuggestions
setContent()
return true
}
}

View File

@ -1,12 +1,11 @@
package org.futo.inputmethod.latin.uix
import android.os.Build
import android.view.inputmethod.InlineSuggestion
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.scrollable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@ -14,9 +13,9 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.Button
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
@ -24,12 +23,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -41,7 +37,6 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
@ -64,7 +59,6 @@ import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
@ -359,6 +353,7 @@ fun ActionBar(
words: SuggestedWords?,
suggestionStripListener: SuggestionStripView.Listener,
onActionActivated: (Action) -> Unit,
inlineSuggestions: List<InlineSuggestion>,
forceOpenActionsInitially: Boolean = false,
) {
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
@ -372,6 +367,8 @@ fun ActionBar(
if(isActionsOpen.value) {
ActionItems(onActionActivated)
} else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
@ -440,8 +437,9 @@ fun PreviewActionBarWithSuggestions(colorScheme: ColorScheme = DarkColorScheme)
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWords,
suggestionStripListener = ExampleListener(),
onActionActivated = { },
suggestionStripListener = ExampleListener()
inlineSuggestions = listOf()
)
}
}
@ -452,8 +450,9 @@ fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorSch
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWordsEmpty,
suggestionStripListener = ExampleListener(),
onActionActivated = { },
suggestionStripListener = ExampleListener()
inlineSuggestions = listOf()
)
}
}
@ -464,8 +463,9 @@ fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
UixThemeWrapper(colorScheme) {
ActionBar(
words = exampleSuggestedWordsEmpty,
onActionActivated = { },
suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf(),
forceOpenActionsInitially = true
)
}

View File

@ -0,0 +1,154 @@
package org.futo.inputmethod.latin.uix
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.Size
import android.util.TypedValue
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestion
import android.view.inputmethod.InlineSuggestionsRequest
import android.widget.inline.InlineContentView
import android.widget.inline.InlinePresentationSpec
import androidx.annotation.RequiresApi
import androidx.autofill.inline.UiVersions
import androidx.autofill.inline.common.ImageViewStyle
import androidx.autofill.inline.common.TextViewStyle
import androidx.autofill.inline.common.ViewStyle
import androidx.autofill.inline.v1.InlineSuggestionUi
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import kotlin.math.roundToInt
@SuppressLint("RestrictedApi")
@RequiresApi(Build.VERSION_CODES.R)
fun createInlineSuggestionsRequest(
context: Context,
activeColorScheme: ColorScheme
): InlineSuggestionsRequest {
val fromDp = { v: Float ->
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
v,
context.resources.displayMetrics
).roundToInt()
}
val stylesBuilder = UiVersions.newStylesBuilder()
val suggestionStyle = InlineSuggestionUi.newStyleBuilder()
.setSingleIconChipStyle(
ViewStyle.Builder()
.setBackgroundColor(activeColorScheme.secondaryContainer.toArgb())
.setPadding(0, 0, 0, 0)
.build()
)
.setChipStyle(
ViewStyle.Builder()
.setBackgroundColor(activeColorScheme.secondaryContainer.toArgb())
.setPadding(
fromDp(8.0f),
fromDp(0.0f),
fromDp(8.0f),
fromDp(0.0f),
)
.build()
)
.setStartIconStyle(ImageViewStyle.Builder().setLayoutMargin(0, 0, 0, 0).build())
.setTitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
fromDp(4.0f),
fromDp(0.0f),
fromDp(4.0f),
fromDp(0.0f),
)
.setTextColor(activeColorScheme.onSecondaryContainer.toArgb())
.setTextSize(14.0f)
.build()
)
.setSubtitleStyle(
TextViewStyle.Builder()
.setLayoutMargin(
fromDp(4.0f),
fromDp(0.0f),
fromDp(4.0f),
fromDp(0.0f),
)
.setTextColor(activeColorScheme.onSecondaryContainer.copy(alpha = 0.5f).toArgb())
.setTextSize(12.0f)
.build()
)
.setEndIconStyle(
ImageViewStyle.Builder()
.setLayoutMargin(0, 0, 0, 0)
.build()
)
.build()
stylesBuilder.addStyle(suggestionStyle)
val stylesBundle = stylesBuilder.build()
val spec = InlinePresentationSpec.Builder(
Size(0, 0),
Size(Int.MAX_VALUE, Int.MAX_VALUE)
).setStyle(stylesBundle).build()
return InlineSuggestionsRequest.Builder(listOf(spec)).let { request ->
request.setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED)
request.build()
}
}
@RequiresApi(Build.VERSION_CODES.R)
@Composable
fun InlineSuggestionView(inlineSuggestion: InlineSuggestion) = with(LocalDensity.current) {
val context = LocalContext.current
val size = Size(ViewGroup.LayoutParams.WRAP_CONTENT, 32.dp.toPx().toInt())
var inlineContentView by remember { mutableStateOf<InlineContentView?>(null) }
LaunchedEffect(Unit) {
try {
inlineSuggestion.inflate(context, size, context.mainExecutor) { inflatedView ->
if (inflatedView != null) {
inlineContentView = inflatedView
}
}
} catch (e: Exception) {
println(e.toString())
}
}
if (inlineContentView != null) {
AndroidView(
factory = { inlineContentView!! },
modifier = Modifier.padding(4.dp, 0.dp)
)
}
}
@RequiresApi(Build.VERSION_CODES.R)
@Composable
fun RowScope.InlineSuggestions(suggestions: List<InlineSuggestion>) {
LazyRow(modifier = Modifier.weight(1.0f).padding(0.dp, 4.dp)) {
items(suggestions.size) {
InlineSuggestionView(suggestions[it])
}
}
}