Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Autofill for Inline Suggestion Support #1557

Merged
merged 10 commits into from
Jan 14, 2025
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.activity)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.autofill)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.navigation.fragment)
Expand Down
30 changes: 25 additions & 5 deletions app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.bar.ui.AlwaysUi
import com.osfans.trime.ime.bar.ui.CandidateUi
import com.osfans.trime.ime.bar.ui.SuggestionUi
import com.osfans.trime.ime.bar.ui.TabUi
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
import com.osfans.trime.ime.candidates.compact.CompactCandidateModule
import com.osfans.trime.ime.candidates.CandidateModule
import com.osfans.trime.ime.candidates.unrolled.window.FlexboxUnrolledCandidateWindow
import com.osfans.trime.ime.core.TrimeInputMethodService
import com.osfans.trime.ime.dependency.InputScope
Expand All @@ -44,9 +45,9 @@ class QuickBar(
private val rime: RimeSession,
private val theme: Theme,
private val windowManager: BoardWindowManager,
lazyCompactCandidate: Lazy<CompactCandidateModule>,
lazyCandidate: Lazy<CandidateModule>,
) : InputBroadcastReceiver {
private val compactCandidate by lazyCompactCandidate
private val candidate by lazyCandidate

private val prefs = AppPrefs.defaultInstance()

Expand Down Expand Up @@ -93,7 +94,18 @@ class QuickBar(
}

private val candidateUi by lazy {
CandidateUi(context, compactCandidate.view)
CandidateUi(context, candidate.compactCandidateModule.view)
}

private val suggestionUi by lazy {
SuggestionUi(context, candidate.suggestionCandidateModule.view).apply {
homeButton.setOnClickListener {
barStateMachine.push(
QuickBarStateMachine.TransitionEvent.SuggestionUpdated,
QuickBarStateMachine.BooleanKey.SuggestionEmpty to true,
)
}
}
}

private val tabUi by lazy {
Expand Down Expand Up @@ -125,7 +137,7 @@ class QuickBar(
private fun setUnrollButtonToAttach() {
candidateUi.unrollButton.setOnClickListener {
windowManager.attachWindow(
FlexboxUnrolledCandidateWindow(context, service, rime, theme, this, windowManager, compactCandidate),
FlexboxUnrolledCandidateWindow(context, service, rime, theme, this, windowManager, candidate.compactCandidateModule),
)
}
candidateUi.unrollButton.setIcon(R.drawable.ic_baseline_expand_more_24)
Expand Down Expand Up @@ -182,6 +194,7 @@ class QuickBar(
add(alwaysUi.root, lParams(matchParent, matchParent))
add(candidateUi.root, lParams(matchParent, matchParent))
add(tabUi.root, lParams(matchParent, matchParent))
add(suggestionUi.root, lParams(matchParent, matchParent))

evalAlwaysUiState()
}
Expand Down Expand Up @@ -225,4 +238,11 @@ class QuickBar(
override fun onWindowDetached(window: BoardWindow) {
barStateMachine.push(QuickBarStateMachine.TransitionEvent.WindowDetached)
}

override fun onInlineSuggestion(views: List<View>) {
barStateMachine.push(
QuickBarStateMachine.TransitionEvent.SuggestionUpdated,
QuickBarStateMachine.BooleanKey.SuggestionEmpty to views.isEmpty(),
)
}
}
12 changes: 12 additions & 0 deletions app/src/main/java/com/osfans/trime/ime/bar/QuickBarStateMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
package com.osfans.trime.ime.bar

import com.osfans.trime.ime.bar.QuickBarStateMachine.BooleanKey.CandidateEmpty
import com.osfans.trime.ime.bar.QuickBarStateMachine.BooleanKey.SuggestionEmpty
import com.osfans.trime.ime.bar.QuickBarStateMachine.State.Always
import com.osfans.trime.ime.bar.QuickBarStateMachine.State.Candidate
import com.osfans.trime.ime.bar.QuickBarStateMachine.State.Suggestion
import com.osfans.trime.ime.bar.QuickBarStateMachine.State.Tab
import com.osfans.trime.util.BuildTransitionEvent
import com.osfans.trime.util.EventStateMachine
Expand All @@ -20,10 +22,12 @@ object QuickBarStateMachine {
Always,
Candidate,
Tab,
Suggestion,
}

enum class BooleanKey : EventStateMachine.BooleanStateKey {
CandidateEmpty,
SuggestionEmpty,
}

enum class TransitionEvent(
Expand All @@ -32,16 +36,23 @@ object QuickBarStateMachine {
CandidatesUpdated({
from(Always) transitTo Candidate on (CandidateEmpty to false)
from(Candidate) transitTo Always on (CandidateEmpty to true)
from(Suggestion) transitTo Candidate on (CandidateEmpty to false)
}),
BarBoardWindowAttached({
from(Always) transitTo Tab
from(Candidate) transitTo Tab
from(Suggestion) transitTo Tab
}),
WindowDetached({
// candidate state has higher priority so here it goes first
from(Tab) transitTo Candidate on (CandidateEmpty to false)
from(Tab) transitTo Suggestion on (SuggestionEmpty to false)
from(Tab) transitTo Always
}),
SuggestionUpdated({
from(Always) transitTo Suggestion on (SuggestionEmpty to false)
from(Suggestion) transitTo Always on (SuggestionEmpty to true)
}),
}

fun new(block: (State) -> Unit) =
Expand All @@ -50,6 +61,7 @@ object QuickBarStateMachine {
externalBooleanStates =
mutableMapOf(
CandidateEmpty to true,
SuggestionEmpty to true,
),
).apply {
onNewStateListener = block
Expand Down
45 changes: 45 additions & 0 deletions app/src/main/java/com/osfans/trime/ime/bar/ui/SuggestionUi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later

package com.osfans.trime.ime.bar.ui

import android.content.Context
import android.view.View
import com.osfans.trime.R
import splitties.dimensions.dp
import splitties.views.dsl.constraintlayout.after
import splitties.views.dsl.constraintlayout.centerVertically
import splitties.views.dsl.constraintlayout.constraintLayout
import splitties.views.dsl.constraintlayout.endOfParent
import splitties.views.dsl.constraintlayout.lParams
import splitties.views.dsl.constraintlayout.startOfParent
import splitties.views.dsl.core.Ui
import splitties.views.dsl.core.add

class SuggestionUi(
override val ctx: Context,
private val compatView: View,
) : Ui {
val homeButton =
ToolButton(ctx, R.drawable.ic_trime_status)

override val root =
ctx.constraintLayout {
add(
homeButton,
lParams(dp(40)) {
centerVertically()
startOfParent()
},
)
add(
compatView,
lParams {
centerVertically()
after(homeButton)
endOfParent()
},
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.osfans.trime.ime.broadcast

import android.view.View
import android.view.inputmethod.EditorInfo
import com.osfans.trime.core.RimeMessage
import com.osfans.trime.core.RimeProto
Expand All @@ -29,4 +30,6 @@ interface InputBroadcastReceiver {
fun onWindowDetached(window: BoardWindow) {}

fun onEnterKeyLabelUpdate(label: String) {}

fun onInlineSuggestion(views: List<View>) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package com.osfans.trime.ime.broadcast

import android.view.View
import android.view.inputmethod.EditorInfo
import com.osfans.trime.core.RimeMessage
import com.osfans.trime.core.RimeProto
Expand Down Expand Up @@ -68,4 +69,8 @@ class InputBroadcaster : InputBroadcastReceiver {
override fun onEnterKeyLabelUpdate(label: String) {
receivers.forEach { it.onEnterKeyLabelUpdate(label) }
}

override fun onInlineSuggestion(views: List<View>) {
receivers.forEach { it.onInlineSuggestion(views) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.osfans.trime.ime.candidates

import android.content.Context
import com.osfans.trime.daemon.RimeSession
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.bar.QuickBar
import com.osfans.trime.ime.candidates.compact.CompactCandidateModule
import com.osfans.trime.ime.candidates.suggestion.SuggestionCandidateModule
import com.osfans.trime.ime.core.TrimeInputMethodService
import com.osfans.trime.ime.dependency.InputScope
import me.tatarka.inject.annotations.Inject

@InputScope
@Inject
class CandidateModule(
val context: Context,
val service: TrimeInputMethodService,
val rime: RimeSession,
val theme: Theme,
val bar: QuickBar,
) {
val compactCandidateModule = CompactCandidateModule(context, service, rime, theme, bar)
val suggestionCandidateModule = SuggestionCandidateModule(context, service, rime, theme, bar)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,16 @@ import com.osfans.trime.ime.bar.UnrollButtonStateMachine
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
import com.osfans.trime.ime.candidates.unrolled.decoration.FlexboxVerticalDecoration
import com.osfans.trime.ime.core.TrimeInputMethodService
import com.osfans.trime.ime.dependency.InputScope
import com.osfans.trime.ime.keyboard.InputFeedbackManager
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import me.tatarka.inject.annotations.Inject
import splitties.dimensions.dp
import splitties.views.dsl.recyclerview.recyclerView
import kotlin.math.max

@InputScope
@Inject
class CompactCandidateModule(
val context: Context,
val service: TrimeInputMethodService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.osfans.trime.ime.candidates.suggestion

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.Icon
import android.os.Build
import android.util.Size
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
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.core.graphics.ColorUtils
import com.osfans.trime.R
import com.osfans.trime.data.theme.ColorManager
import splitties.dimensions.dp
import java.util.concurrent.Executor
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

class InlineSuggestionHandler(
private val context: Context,
) {
@SuppressLint("NewApi", "RestrictedApi")
fun createRequest(): InlineSuggestionsRequest {
val firstTextColor = ColorManager.getColor("candidate_text_color")!!
val backColor = ColorManager.getColor("candidate_background")!!

val style =
InlineSuggestionUi
.newStyleBuilder()
.setSingleIconChipStyle(
ViewStyle
.Builder()
.setBackgroundColor(Color.TRANSPARENT)
.setPadding(0, 0, 0, 0)
.build(),
).setChipStyle(
ViewStyle
.Builder()
.setBackground(
Icon.createWithResource(context, R.drawable.bg_inline_suggestion).apply {
setTint(ColorUtils.blendARGB(backColor, firstTextColor, 0.2f))
},
).build(),
).setTitleStyle(
TextViewStyle
.Builder()
.setLayoutMargin(context.dp(4), 0, context.dp(4), 0)
.setTextColor(firstTextColor)
.setTextSize(14f)
.build(),
).setSubtitleStyle(
TextViewStyle
.Builder()
.setTextColor(
ColorUtils.blendARGB(firstTextColor, backColor, 0.3f),
).setTextSize(12f)
.build(),
).setStartIconStyle(
ImageViewStyle
.Builder()
.setTintList(ColorStateList.valueOf(firstTextColor))
.build(),
).setEndIconStyle(
ImageViewStyle
.Builder()
.setTintList(ColorStateList.valueOf(firstTextColor))
.build(),
).build()
val styleBundle =
UiVersions
.newStylesBuilder()
.addStyle(style)
.build()
val spec =
InlinePresentationSpec
.Builder(Size(0, 0), Size(context.dp(160), Int.MAX_VALUE))
.setStyle(styleBundle)
.build()
return InlineSuggestionsRequest
.Builder(listOf(spec))
.setMaxSuggestionCount(InlineSuggestionsRequest.SUGGESTION_COUNT_UNLIMITED)
.build()
}

private val suggestionSize by lazy {
Size(ViewGroup.LayoutParams.WRAP_CONTENT, context.dp(INLINE_SUGGESTION_HEIGHT))
}

@RequiresApi(Build.VERSION_CODES.R)
suspend fun inflateSuggestion(response: InlineSuggestionsResponse): List<View> =
response.inlineSuggestions.map {
suspendCoroutine { c ->
it.inflate(context, suggestionSize, directExecutor) { v ->
c.resume(v)
}
}
}

companion object {
private const val INLINE_SUGGESTION_HEIGHT = 40

private val directExecutor by lazy {
Executor { it.run() }
}
}
}
Loading
Loading