From bd0368d89f6d77a7b39c76a8dc6741f867f44a9c Mon Sep 17 00:00:00 2001 From: Aleksandras Kostarevas Date: Sat, 26 Aug 2023 20:04:56 +0300 Subject: [PATCH] Add initial inline suggestions support --- build.gradle | 1 + java/res/xml/method.xml | 3 +- .../org/futo/inputmethod/latin/LatinIME.kt | 41 ++++- .../futo/inputmethod/latin/uix/ActionBar.kt | 26 +-- .../latin/uix/InlineSuggestionView.kt | 154 ++++++++++++++++++ 5 files changed, 205 insertions(+), 20 deletions(-) create mode 100644 java/src/org/futo/inputmethod/latin/uix/InlineSuggestionView.kt diff --git a/build.gradle b/build.gradle index 83042614d..0eebdf035 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml index 131920a21..151f87308 100644 --- a/java/res/xml/method.xml +++ b/java/res/xml/method.xml @@ -115,7 +115,8 @@ + android:supportsSwitchingToNextInputMethod="true" + android:supportsInlineSuggestions="true"> = 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) { - ActionBar( - suggestedWords, - latinIMELegacy, - onActionActivated = { onActionActivated(it) } - ) + // Don't show suggested words when it's not meant to be shown + val suggestedWordsOrNull = if(shouldShowSuggestionStrip) { + suggestedWords + } else { + null } + + ActionBar( + 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 + } } \ No newline at end of file diff --git a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt index 3bdcf4ae3..ce8ce70d4 100644 --- a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt +++ b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt @@ -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, 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 ) } diff --git a/java/src/org/futo/inputmethod/latin/uix/InlineSuggestionView.kt b/java/src/org/futo/inputmethod/latin/uix/InlineSuggestionView.kt new file mode 100644 index 000000000..ff7b1c22f --- /dev/null +++ b/java/src/org/futo/inputmethod/latin/uix/InlineSuggestionView.kt @@ -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(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) { + LazyRow(modifier = Modifier.weight(1.0f).padding(0.dp, 4.dp)) { + items(suggestions.size) { + InlineSuggestionView(suggestions[it]) + } + } +} \ No newline at end of file