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 'com.google.code.findbugs:jsr305:3.0.2'
implementation 'androidx.datastore:datastore-preferences:1.0.0' 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-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest' debugImplementation 'androidx.compose.ui:ui-test-manifest'

View File

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

View File

@ -2,11 +2,17 @@ package org.futo.inputmethod.latin
import android.content.res.Configuration import android.content.res.Configuration
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.inputmethod.CompletionInfo import android.view.inputmethod.CompletionInfo
import android.view.inputmethod.EditorInfo 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 android.view.inputmethod.InputMethodSubtype
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row 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.DynamicThemeProviderOwner
import org.futo.inputmethod.latin.uix.KeyboardManagerForAction import org.futo.inputmethod.latin.uix.KeyboardManagerForAction
import org.futo.inputmethod.latin.uix.THEME_KEY 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.deferGetSetting
import org.futo.inputmethod.latin.uix.deferSetSetting import org.futo.inputmethod.latin.uix.deferSetSetting
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
@ -113,6 +120,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
return currWindowAction != null return currWindowAction != null
} }
private var inlineSuggestions: List<InlineSuggestion> = listOf()
private fun recreateKeyboard() { private fun recreateKeyboard() {
legacyInputView = latinIMELegacy.onCreateInputView() legacyInputView = latinIMELegacy.onCreateInputView()
latinIMELegacy.loadKeyboard() latinIMELegacy.loadKeyboard()
@ -224,13 +233,20 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
@Composable @Composable
private fun MainKeyboardViewWithActionBar() { private fun MainKeyboardViewWithActionBar() {
Column { Column {
if (shouldShowSuggestionStrip) { // Don't show suggested words when it's not meant to be shown
ActionBar( val suggestedWordsOrNull = if(shouldShowSuggestionStrip) {
suggestedWords, suggestedWords
latinIMELegacy, } else {
onActionActivated = { onActionActivated(it) } null
)
} }
ActionBar(
suggestedWordsOrNull,
latinIMELegacy,
inlineSuggestions = inlineSuggestions,
onActionActivated = { onActionActivated(it) }
)
LegacyKeyboardView() LegacyKeyboardView()
} }
} }
@ -506,4 +522,17 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
deferSetSetting(THEME_KEY, newTheme.key) 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 package org.futo.inputmethod.latin.uix
import android.os.Build import android.os.Build
import android.view.inputmethod.InlineSuggestion
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Canvas import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -24,12 +23,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.CornerRadius
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate 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
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED 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.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
@ -359,6 +353,7 @@ fun ActionBar(
words: SuggestedWords?, words: SuggestedWords?,
suggestionStripListener: SuggestionStripView.Listener, suggestionStripListener: SuggestionStripView.Listener,
onActionActivated: (Action) -> Unit, onActionActivated: (Action) -> Unit,
inlineSuggestions: List<InlineSuggestion>,
forceOpenActionsInitially: Boolean = false, forceOpenActionsInitially: Boolean = false,
) { ) {
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) } val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
@ -372,6 +367,8 @@ fun ActionBar(
if(isActionsOpen.value) { if(isActionsOpen.value) {
ActionItems(onActionActivated) ActionItems(onActionActivated)
} else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if(words != null) { } else if(words != null) {
SuggestionItems(words) { SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually( suggestionStripListener.pickSuggestionManually(
@ -440,8 +437,9 @@ fun PreviewActionBarWithSuggestions(colorScheme: ColorScheme = DarkColorScheme)
UixThemeWrapper(colorScheme) { UixThemeWrapper(colorScheme) {
ActionBar( ActionBar(
words = exampleSuggestedWords, words = exampleSuggestedWords,
suggestionStripListener = ExampleListener(),
onActionActivated = { }, onActionActivated = { },
suggestionStripListener = ExampleListener() inlineSuggestions = listOf()
) )
} }
} }
@ -452,8 +450,9 @@ fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorSch
UixThemeWrapper(colorScheme) { UixThemeWrapper(colorScheme) {
ActionBar( ActionBar(
words = exampleSuggestedWordsEmpty, words = exampleSuggestedWordsEmpty,
suggestionStripListener = ExampleListener(),
onActionActivated = { }, onActionActivated = { },
suggestionStripListener = ExampleListener() inlineSuggestions = listOf()
) )
} }
} }
@ -464,8 +463,9 @@ fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
UixThemeWrapper(colorScheme) { UixThemeWrapper(colorScheme) {
ActionBar( ActionBar(
words = exampleSuggestedWordsEmpty, words = exampleSuggestedWordsEmpty,
onActionActivated = { },
suggestionStripListener = ExampleListener(), suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf(),
forceOpenActionsInitially = true 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])
}
}
}