Skip to content

Commit

Permalink
feat: implements unrolled candidate view
Browse files Browse the repository at this point in the history
  • Loading branch information
WhiredPlanck committed Sep 1, 2024
1 parent 663e9c4 commit c23add2
Show file tree
Hide file tree
Showing 13 changed files with 555 additions and 16 deletions.
54 changes: 50 additions & 4 deletions app/src/main/java/com/osfans/trime/ime/bar/QuickBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.ViewAnimator
import androidx.lifecycle.lifecycleScope
import com.osfans.trime.R
import com.osfans.trime.core.RimeNotification.OptionNotification
import com.osfans.trime.core.RimeProto
import com.osfans.trime.core.SchemaItem
Expand All @@ -23,9 +24,12 @@ import com.osfans.trime.ime.bar.ui.CandidateUi
import com.osfans.trime.ime.bar.ui.TabUi
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
import com.osfans.trime.ime.candidates.CompactCandidateModule
import com.osfans.trime.ime.candidates.unrolled.window.FlexboxUnrolledCandidateWindow
import com.osfans.trime.ime.core.TrimeInputMethodService
import com.osfans.trime.ime.dependency.InputScope
import com.osfans.trime.ime.keyboard.KeyboardWindow
import com.osfans.trime.ime.window.BoardWindow
import com.osfans.trime.ime.window.BoardWindowManager
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject
import splitties.views.dsl.core.add
Expand All @@ -39,8 +43,11 @@ class QuickBar(
private val service: TrimeInputMethodService,
private val rime: RimeSession,
private val theme: Theme,
private val compactCandidate: CompactCandidateModule,
private val windowManager: BoardWindowManager,
lazyCompactCandidate: Lazy<CompactCandidateModule>,
) : InputBroadcastReceiver {
private val compactCandidate by lazyCompactCandidate

private val prefs = AppPrefs.defaultInstance()

private val showSwitchers get() = prefs.keyboard.switchesEnabled
Expand Down Expand Up @@ -93,14 +100,53 @@ class QuickBar(
TabUi(context)
}

private val barStateMachine = QuickBarStateMachine.new {
switchUiByState(it)
private val barStateMachine =
QuickBarStateMachine.new {
switchUiByState(it)
}

val unrollButtonStateMachine =
UnrollButtonStateMachine.new {
when (it) {
UnrollButtonStateMachine.State.ClickToAttachWindow -> {
setUnrollButtonToAttach()
setUnrollButtonEnabled(true)
}
UnrollButtonStateMachine.State.ClickToDetachWindow -> {
setUnrollButtonToDetach()
setUnrollButtonEnabled(true)
}
UnrollButtonStateMachine.State.Hidden -> {
setUnrollButtonEnabled(false)
}
}
}

private fun setUnrollButtonToAttach() {
candidateUi.unrollButton.setOnClickListener {
windowManager.attachWindow(
FlexboxUnrolledCandidateWindow(context, service, rime, theme, this, windowManager, compactCandidate),
)
}
candidateUi.unrollButton.setIcon(R.drawable.ic_baseline_expand_more_24)
}

private fun setUnrollButtonToDetach() {
candidateUi.unrollButton.setOnClickListener {
windowManager.attachWindow(KeyboardWindow)
}
candidateUi.unrollButton.setIcon(R.drawable.ic_baseline_expand_less_24)
}

private fun setUnrollButtonEnabled(enabled: Boolean) {
candidateUi.unrollButton.visibility = if (enabled) View.VISIBLE else View.INVISIBLE
}

override fun onInputContextUpdate(ctx: RimeProto.Context) {
barStateMachine.push(
QuickBarStateMachine.TransitionEvent.CandidatesUpdated,
QuickBarStateMachine.BooleanKey.CandidateEmpty to ctx.menu.candidates.isEmpty())
QuickBarStateMachine.BooleanKey.CandidateEmpty to ctx.menu.candidates.isEmpty(),
)
}

private fun switchUiByState(state: QuickBarStateMachine.State) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later
package com.osfans.trime.ime.bar

import com.osfans.trime.ime.bar.UnrollButtonStateMachine.BooleanKey.UnrolledCandidatesEmpty
import com.osfans.trime.ime.bar.UnrollButtonStateMachine.State.ClickToAttachWindow
import com.osfans.trime.ime.bar.UnrollButtonStateMachine.State.ClickToDetachWindow
import com.osfans.trime.ime.bar.UnrollButtonStateMachine.State.Hidden
import com.osfans.trime.util.BuildTransitionEvent
import com.osfans.trime.util.EventStateMachine
import com.osfans.trime.util.TransitionBuildBlock

object UnrollButtonStateMachine {
enum class State {
ClickToAttachWindow,
ClickToDetachWindow,
Hidden,
}

enum class BooleanKey : EventStateMachine.BooleanStateKey {
UnrolledCandidatesEmpty,
}

enum class TransitionEvent(val builder: TransitionBuildBlock<State, BooleanKey>) :
EventStateMachine.TransitionEvent<State, BooleanKey> by BuildTransitionEvent(builder) {
UnrolledCandidatesUpdated({
from(Hidden) transitTo ClickToAttachWindow on (UnrolledCandidatesEmpty to false)
from(ClickToAttachWindow) transitTo Hidden on (UnrolledCandidatesEmpty to true)
}),
UnrolledCandidatesAttached({
from(ClickToAttachWindow) transitTo ClickToDetachWindow
}),
UnrolledCandidatesDetached({
from(ClickToDetachWindow) transitTo Hidden on (UnrolledCandidatesEmpty to true)
from(ClickToDetachWindow) transitTo ClickToAttachWindow on (UnrolledCandidatesEmpty to false)
}),
}

fun new(block: (State) -> Unit) =
EventStateMachine<State, TransitionEvent, BooleanKey>(
initialState = Hidden,
externalBooleanStates =
mutableMapOf(
UnrolledCandidatesEmpty to true,
),
).apply {
onNewStateListener = block
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import com.osfans.trime.daemon.RimeSession
import com.osfans.trime.daemon.launchOnReady
import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.bar.QuickBar
import com.osfans.trime.ime.bar.UnrollButtonStateMachine
import com.osfans.trime.ime.broadcast.InputBroadcastReceiver
import com.osfans.trime.ime.candidates.adapter.CompactCandidateViewAdapter
import com.osfans.trime.ime.candidates.unrolled.decoration.FlexboxVerticalDecoration
Expand All @@ -41,6 +43,7 @@ class CompactCandidateModule(
val service: TrimeInputMethodService,
val rime: RimeSession,
val theme: Theme,
val bar: QuickBar,
) : InputBroadcastReceiver {
private val _unrolledCandidateOffset =
MutableSharedFlow<Int>(
Expand All @@ -50,19 +53,24 @@ class CompactCandidateModule(

val unrolledCandidateOffset = _unrolledCandidateOffset.asSharedFlow()

private fun refreshUnrolled() {
fun refreshUnrolled() {
runBlocking {
_unrolledCandidateOffset.emit(adapter.stickyOffset + view.childCount)
_unrolledCandidateOffset.emit(adapter.run { max(sticky, previous) } + view.childCount)
}
bar.unrollButtonStateMachine.push(
UnrollButtonStateMachine.TransitionEvent.UnrolledCandidatesUpdated,
UnrollButtonStateMachine.BooleanKey.UnrolledCandidatesEmpty to
(adapter.run { isLastPage && itemCount == layoutManager.childCount }),
)
}

val adapter by lazy {
CompactCandidateViewAdapter(theme).apply {
setOnDebouncedItemClick { _, _, position ->
rime.launchOnReady { it.selectCandidate(stickyOffset + position) }
rime.launchOnReady { it.selectCandidate(sticky + position) }
}
setOnItemLongClickListener { _, view, position ->
showCandidateAction(stickyOffset + position, items[position].text, view)
showCandidateAction(sticky + position, items[position].text, view)
true
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,25 @@ import splitties.views.dsl.core.wrapContent
import splitties.views.setPaddingDp

open class CompactCandidateViewAdapter(val theme: Theme) : BaseQuickAdapter<CandidateItem, CandidateViewHolder>() {
var stickyOffset: Int = 0
var sticky: Int = 0
private set

var isLastPage: Boolean = false
private set

var previous: Int = 0
private set

fun updateCandidates(
list: List<CandidateItem>,
offset: Int = 0,
isLastPage: Boolean,
previous: Int,
sticky: Int = 0,
) {
stickyOffset = offset
super.submitList(list.drop(offset))
this.isLastPage = isLastPage
this.previous = previous
this.sticky = sticky
super.submitList(list.drop(sticky))
}

override fun onCreateViewHolder(
Expand Down Expand Up @@ -51,7 +61,7 @@ open class CompactCandidateViewAdapter(val theme: Theme) : BaseQuickAdapter<Cand
holder.ui.setComment(comment)
holder.text = text
holder.comment = comment
holder.idx = position
holder.idx = sticky + position
holder.ui.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
minWidth = 0
flexGrow = 1f
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later
package com.osfans.trime.ime.candidates.adapter

import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import com.osfans.trime.core.CandidateItem
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.candidates.CandidateItemUi
import com.osfans.trime.ime.candidates.CandidateViewHolder

open class PagingCandidateViewAdapter(val theme: Theme) : PagingDataAdapter<CandidateItem, CandidateViewHolder>(diffCallback) {
companion object {
private val diffCallback =
object : DiffUtil.ItemCallback<CandidateItem>() {
override fun areItemsTheSame(
oldItem: CandidateItem,
newItem: CandidateItem,
): Boolean {
return oldItem === newItem
}

override fun areContentsTheSame(
oldItem: CandidateItem,
newItem: CandidateItem,
): Boolean {
return oldItem == newItem
}
}
}

var offset: Int = 0
private set

fun refreshWithOffset(offset: Int) {
this.offset = offset
refresh()
}

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): CandidateViewHolder {
return CandidateViewHolder(CandidateItemUi(parent.context, theme))
}

override fun onBindViewHolder(
holder: CandidateViewHolder,
position: Int,
) {
val (comment, text) = getItem(position)!!
holder.ui.setText(text)
holder.ui.setComment(comment)
holder.text = text
holder.comment = comment
holder.idx = position + offset
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later
package com.osfans.trime.ime.candidates.unrolled

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.osfans.trime.core.CandidateItem
import com.osfans.trime.daemon.RimeSession
import timber.log.Timber

class CandidatesPagingSource(val rime: RimeSession, val num: Int, val offset: Int) :
PagingSource<Int, CandidateItem>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, CandidateItem> {
// use candidate index for key, null means load from beginning (including offset)
val startIndex = params.key ?: offset
val pageSize = params.loadSize
Timber.d("getCandidates(offset=$startIndex, limit=$pageSize)")
val candidates =
rime.runOnReady {
getCandidates(startIndex, pageSize)
}
val prevKey = if (startIndex >= pageSize) startIndex - pageSize else null
val nextKey =
if (num > 0) {
if (startIndex + pageSize + 1 >= num) null else startIndex + pageSize
} else {
if (candidates.size < pageSize) null else startIndex + pageSize
}
return LoadResult.Page(candidates.toList(), prevKey, nextKey)
}

// always reload from beginning
override fun getRefreshKey(state: PagingState<Int, CandidateItem>) = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
//
// SPDX-License-Identifier: GPL-3.0-or-later
package com.osfans.trime.ime.candidates.unrolled

import android.annotation.SuppressLint
import android.content.Context
import androidx.constraintlayout.widget.ConstraintLayout
import com.osfans.trime.R
import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import splitties.views.dsl.constraintlayout.centerInParent
import splitties.views.dsl.constraintlayout.lParams
import splitties.views.dsl.core.add
import splitties.views.dsl.recyclerview.recyclerView

@SuppressLint("ViewConstructor")
class UnrolledCandidateLayout(context: Context, theme: Theme) : ConstraintLayout(context) {
val recyclerView =
recyclerView {
isVerticalScrollBarEnabled = false
}

init {
id = R.id.unrolled_candidate_view
background =
ColorManager.getDrawable(
context,
"candidate_background",
theme.generalStyle.candidateBorder,
"candidate_border_color",
theme.generalStyle.candidateBorderRound,
)

add(
recyclerView,
lParams {
centerInParent()
},
)
}

fun resetPosition() {
recyclerView.scrollToPosition(0)
}
}
Loading

0 comments on commit c23add2

Please sign in to comment.