Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.content.SharedPreferences
import androidx.annotation.Keep
import com.osfans.trime.R
import com.osfans.trime.data.base.DataManager
import com.osfans.trime.ime.candidates.compact.HorizontalCandidateMode
import com.osfans.trime.ime.candidates.popup.PopupCandidatesLayout
import com.osfans.trime.ime.candidates.popup.PopupCandidatesMode
import com.osfans.trime.ime.composition.PopupPosition
Expand Down Expand Up @@ -152,6 +153,9 @@ class AppPrefs(
const val HOOK_SHIFT_NUM = "hook_shift_num"
const val HOOK_SHIFT_SYMBOL = "hook_shift_symbol"
const val HOOK_SHIFT_ARROW = "hook_shift_arrow"

const val MAX_SPAN_COUNT = "max_span_count"
const val MAX_SPAN_COUNT_LANDSCAPE = "max_span_count_landscape"
}

enum class LandscapeMode(override val stringRes: Int) : PreferenceDelegateEnum {
Expand Down Expand Up @@ -292,6 +296,32 @@ class AppPrefs(
"dp",
)

val horizontalCandidateMode = enum(R.string.horizontal_candidate_style, Candidates.HORIZONTAL_CANDIDATE_MODE, HorizontalCandidateMode.AUTO_FILL)

val maxSpanCount = int(
R.string.max_span_count,
MAX_SPAN_COUNT,
6,
1,
10,
enableUiOn = {
shared.getString(Candidates.HORIZONTAL_CANDIDATE_MODE, null) ==
HorizontalCandidateMode.AUTO_FILL.name
},
)

val maxSpanCountLandscape = int(
R.string.max_span_count_landscape,
MAX_SPAN_COUNT_LANDSCAPE,
8,
4,
12,
enableUiOn = {
shared.getString(Candidates.HORIZONTAL_CANDIDATE_MODE, null) ==
HorizontalCandidateMode.AUTO_FILL.name
},
)

val hookCtrlA = switch(R.string.hook_ctrl_a, HOOK_CTRL_A, false)
val hookCtrlCV = switch(R.string.hook_ctrl_cv, HOOK_CTRL_CV, false)
val hookCtrlLR = switch(R.string.hook_ctrl_lr, HOOK_CTRL_LR, false)
Expand All @@ -309,6 +339,7 @@ class AppPrefs(
const val MODE = "show_candidates_window"
const val LAYOUT = "candidates_layout"
const val POSITION = "candidates_window_position"
const val HORIZONTAL_CANDIDATE_MODE = "horizontal_candidate_mode"
}

val mode = enum(R.string.show_candidates_window, MODE, PopupCandidatesMode.DISABLED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@
package com.osfans.trime.ime.candidates.compact

import android.content.Context
import android.content.res.Configuration
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.RectShape
import android.view.View
import android.widget.PopupMenu
import androidx.core.text.bold
import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager
import com.osfans.trime.R
import com.osfans.trime.core.RimeMessage
import com.osfans.trime.daemon.RimeSession
import com.osfans.trime.daemon.launchOnReady
import com.osfans.trime.data.prefs.AppPrefs
import com.osfans.trime.data.theme.ColorManager
import com.osfans.trime.data.theme.Theme
import com.osfans.trime.ime.bar.QuickBar
Expand Down Expand Up @@ -47,6 +50,30 @@ class CompactCandidateModule(
val theme: Theme,
val bar: QuickBar,
) : InputBroadcastReceiver {
private val fillStyle by AppPrefs.defaultInstance().keyboard.horizontalCandidateMode

private val maxSpanCountPref by lazy {
AppPrefs.defaultInstance().keyboard.run {
if (context.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
maxSpanCount
} else {
maxSpanCountLandscape
}
}
}

private var layoutMinWidth = 0
private var layoutFlexGrow = 0f

/**
* (for [HorizontalCandidateMode.AutoFill] only)
* Second layout pass is needed when:
* [^1] total candidates count < maxSpanCount && [^2] RecyclerView cannot display all of them
* In that case, displayed candidates should be stretched evenly (by setting flexGrow to 1.0f).
*/
private var secondLayoutPassNeeded = false
private var secondLayoutPassDone = false

private val _unrolledCandidateOffset =
MutableSharedFlow<Int>(
replay = 1,
Expand Down Expand Up @@ -81,6 +108,11 @@ class CompactCandidateModule(
}
}

fun updateLayoutParams(minWidth: Int, flexGrow: Float) {
layoutMinWidth = minWidth
layoutFlexGrow = flexGrow
}

val layoutManager by lazy {
object : FlexboxLayoutManager(context) {
override fun canScrollHorizontally(): Boolean = false
Expand All @@ -89,7 +121,24 @@ class CompactCandidateModule(

override fun onLayoutCompleted(state: RecyclerView.State?) {
super.onLayoutCompleted(state)
refreshUnrolled(this.childCount)
val cnt = this.childCount
if (secondLayoutPassNeeded) {
if (cnt < adapter.itemCount) {
// [^2] RecyclerView can't display all candidates
// update LayoutParams in onLayoutCompleted would trigger another
// onLayoutCompleted, skip the second one to avoid infinite loop
if (secondLayoutPassDone) return
secondLayoutPassDone = true
for (i in 0 until cnt) {
getChildAt(i)!!.updateLayoutParams<LayoutParams> {
flexGrow = 1f
}
}
} else {
secondLayoutPassNeeded = false
}
}
refreshUnrolled(cnt)
}
}
}
Expand All @@ -113,9 +162,59 @@ class CompactCandidateModule(
}
}

init {
// Update layoutMinWidth when view size changes
view.addOnLayoutChangeListener(
object : View.OnLayoutChangeListener {
override fun onLayoutChange(
v: View,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int,
) {
if (fillStyle == HorizontalCandidateMode.AUTO_FILL) {
val maxSpanCount = maxSpanCountPref.getValue()
layoutMinWidth = v.width / maxSpanCount - separatorDrawable.intrinsicWidth
}
}
},
)
}

override fun onCandidateListUpdate(data: RimeMessage.CandidateListMessage.Data) {
val (total, highlighted, candidates) = data

val maxSpanCount = maxSpanCountPref.getValue()

when (fillStyle) {
HorizontalCandidateMode.NEVER_FILL -> {
layoutMinWidth = 0
layoutFlexGrow = 0f
secondLayoutPassNeeded = false
}
HorizontalCandidateMode.AUTO_FILL -> {
layoutMinWidth = view.width / maxSpanCount - separatorDrawable.intrinsicWidth
layoutFlexGrow = if (candidates.size < maxSpanCount) 0f else 1f
// [^1] total candidates count < maxSpanCount
secondLayoutPassNeeded = candidates.size < maxSpanCount
secondLayoutPassDone = false
}
HorizontalCandidateMode.ALWAYS_FILL -> {
layoutMinWidth = 0
layoutFlexGrow = 1f
secondLayoutPassNeeded = false
}
}

adapter.updateLayoutParams(layoutMinWidth, layoutFlexGrow)
adapter.updateCandidates(candidates, total, highlighted)

// not sure why empty candidates won't trigger `FlexboxLayoutManager#onLayoutCompleted()`
if (candidates.isEmpty()) {
refreshUnrolled(0)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ open class CompactCandidateViewAdapter(
var highlightedIdx: Int = -1
private set

var layoutMinWidth: Int = 0
private set

var layoutFlexGrow: Float = 0f
private set

fun updateLayoutParams(minWidth: Int, flexGrow: Float) {
layoutMinWidth = minWidth
layoutFlexGrow = flexGrow
}

fun updateCandidates(
data: Array<CandidateItem>,
total: Int,
Expand Down Expand Up @@ -71,8 +82,8 @@ open class CompactCandidateViewAdapter(
holder.comment = item.comment
holder.idx = position // unused
holder.ui.root.updateLayoutParams<FlexboxLayoutManager.LayoutParams> {
minWidth = 0
flexGrow = 0f
minWidth = this@CompactCandidateViewAdapter.layoutMinWidth
flexGrow = this@CompactCandidateViewAdapter.layoutFlexGrow
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.osfans.trime.ime.candidates.compact

import com.osfans.trime.R
import com.osfans.trime.data.prefs.PreferenceDelegateEnum

enum class HorizontalCandidateMode(
override val stringRes: Int,
) : PreferenceDelegateEnum {
NEVER_FILL(R.string.horizontal_candidate_never_fill),
AUTO_FILL(R.string.horizontal_candidate_auto_fill),
ALWAYS_FILL(R.string.horizontal_candidate_always_fill),
}
6 changes: 6 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@
<string name="ascii_switch_tips">切换中英文模式时显示提示</string>
<string name="key_double_tap_timeout">按键双击超时</string>
<string name="key_slide_step_size">按键滑动步长</string>
<string name="horizontal_candidate_style">横向候选词样式</string>
<string name="horizontal_candidate_never_fill">从不填充宽度</string>
<string name="horizontal_candidate_auto_fill">按需填充宽度</string>
<string name="horizontal_candidate_always_fill">总是填充宽度</string>
<string name="max_span_count">每行最大候选词数</string>
<string name="max_span_count_landscape">每行最大候选词数(横屏)</string>
<string name="user_dictionary">用户词典</string>
<string name="exported_n_entries">已导出 %1$d 个词条</string>
<string name="backed_up_x_to_sync_dir">已备份 %1$s 到同步目录</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
<string name="ascii_switch_tips">切換中英文模式時顯示提示</string>
<string name="key_double_tap_timeout">按鍵連擊逾時</string>
<string name="key_slide_step_size">按鍵滑動步長</string>
<string name="horizontal_candidate_style">橫向候選詞樣式</string>
<string name="horizontal_candidate_never_fill">從不填充寬度</string>
<string name="horizontal_candidate_auto_fill">按需填充寬度</string>
<string name="horizontal_candidate_always_fill">總是填充寬度</string>
<string name="max_span_count">每行最大候選詞數</string>
<string name="max_span_count_landscape">每行最大候選詞數(橫屏)</string>
<string name="user_dictionary">使用者詞典</string>
<string name="exported_n_entries">已匯出 %1$d 個詞條</string>
<string name="backed_up_x_to_sync_dir">已備份 %1$s 到同步目錄</string>
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,12 @@
<string name="ascii_switch_tips">Show tips when switching ASCII mode</string>
<string name="key_double_tap_timeout">Key double tap timeout</string>
<string name="key_slide_step_size">Step size for key sliding</string>
<string name="horizontal_candidate_style">Horizontal candidate style</string>
<string name="horizontal_candidate_never_fill">Never fill width</string>
<string name="horizontal_candidate_auto_fill">Fill width on demand</string>
<string name="horizontal_candidate_always_fill">Always fill width</string>
<string name="max_span_count">Maximum candidates per row</string>
<string name="max_span_count_landscape">Maximum candidates per row (landscape)</string>
<string name="user_dictionary">User dictionary</string>
<string name="import_">Import</string>
<string name="import_error">Import error</string>
Expand Down
Loading