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
4 changes: 4 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 @@ -95,11 +95,15 @@ class AppPrefs(
const val ASCII_SWITCH_TIPS = "ascii_switch_tips"
const val INLINE_SUGGESTIONS = "inline_suggestions"
const val PREFERRED_VOICE_INPUT = "preferred_voice_input"
const val TEST_INPUT_VISIBLE = "test_input_visible"
const val TEST_INPUT_EXPANDED = "test_input_expanded"
}

val inlinePreeditMode = enum(R.string.inline_preedit_mode, INLINE_PREEDIT_MODE, InlinePreeditMode.DISABLE)
val asciiSwitchTips = switch(R.string.ascii_switch_tips, ASCII_SWITCH_TIPS, true)
val inlineSuggestions = switch(R.string.inline_suggestions, INLINE_SUGGESTIONS, true)
val testInputVisible = bool(TEST_INPUT_VISIBLE, true)
val testInputExpanded = bool(TEST_INPUT_EXPANDED, true)

val preferredVoiceInput = list(
R.string.preferred_voice_input,
Expand Down
20 changes: 19 additions & 1 deletion app/src/main/java/com/osfans/trime/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package com.osfans.trime.ui.main
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
Expand Down Expand Up @@ -38,7 +39,6 @@ import com.osfans.trime.util.item
import com.osfans.trime.util.parcelable
import com.osfans.trime.util.startActivity
import com.osfans.trime.worker.BackgroundSyncWork
import splitties.resources.styledColor
import splitties.views.topPadding

class MainActivity : AppCompatActivity() {
Expand All @@ -47,6 +47,8 @@ class MainActivity : AppCompatActivity() {
private val uiMode by AppPrefs.defaultInstance().advanced.uiMode

private lateinit var navController: NavController
private lateinit var activityBinding: ActivityMainBinding
private var showTestInputMenuItem: MenuItem? = null

override fun onCreate(savedInstanceState: Bundle?) {
val uiMode =
Expand All @@ -61,9 +63,11 @@ class MainActivity : AppCompatActivity() {
val binding = ActivityMainBinding.inflate(layoutInflater)
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = systemBars.left
rightMargin = systemBars.right
bottomMargin = maxOf(systemBars.bottom, ime.bottom)
}
binding.mainToolbar.root.topPadding = systemBars.top
windowInsets
Expand All @@ -73,6 +77,7 @@ class MainActivity : AppCompatActivity() {
.isAppearanceLightStatusBars = false

setContentView(binding.root)
activityBinding = binding
// always show toolbar back arrow icon
binding.mainToolbar.toolbar.navigationIcon =
DrawerArrowDrawable(this).apply {
Expand All @@ -83,6 +88,9 @@ class MainActivity : AppCompatActivity() {
// don't use `setSupportActionBar(binding.toolbar)` here,
// because navController would change toolbar title, we need to control it by ourselves
setupToolbarMenu(binding.mainToolbar.toolbar.menu)
binding.testInputPanel.bind { visible ->
showTestInputMenuItem?.isVisible = !visible
}
navController = binding.navHostFragment.getFragment<NavHostFragment>().navController
navController.graph = NavigationRoute.createGraph(navController)
binding.mainToolbar.toolbar.setNavigationOnClickListener {
Expand Down Expand Up @@ -166,6 +174,16 @@ class MainActivity : AppCompatActivity() {
// show menu item on demand
item.isVisible = false
}
showTestInputMenuItem =
menu.item(
R.string.show_test_input,
R.drawable.ic_baseline_keyboard_24,
iconTint = ContextCompat.getColor(this, R.color.toolbarForegroundColor),
showAsAction = true,
) {
activityBinding.testInputPanel.showExpanded()
}
showTestInputMenuItem?.isVisible = false
}

override fun onPause() {
Expand Down
254 changes: 254 additions & 0 deletions app/src/main/java/com/osfans/trime/ui/main/TestInputPanel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.osfans.trime.ui.main

import android.content.Context
import android.text.InputType
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.TextViewCompat
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.FragmentActivity
import com.osfans.trime.R
import com.osfans.trime.data.prefs.AppPrefs
import com.osfans.trime.databinding.TestInputPanelBinding
import splitties.resources.styledColor
import splitties.systemservices.inputMethodManager

class TestInputPanel
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
) : LinearLayout(context, attrs) {
private val binding: TestInputPanelBinding
private val activity: FragmentActivity by lazy {
var ctx: Context = context
while (ctx is android.content.ContextWrapper) {
if (ctx is FragmentActivity) return@lazy ctx
ctx = ctx.baseContext
}
throw IllegalStateException("TestInputPanel must be inflated with a FragmentActivity context.")
}
private var onVisibilityChanged: ((visible: Boolean) -> Unit)? = null

private data class InputTypeOption(
val pill: TextView,
@StringRes val hintRes: Int,
@DrawableRes val iconRes: Int,
val inputType: Int,
)

private lateinit var options: List<InputTypeOption>

init {
orientation = VERTICAL
setBackgroundResource(R.drawable.bg_test_input_panel)
val paddingPx = (12 * resources.displayMetrics.density).toInt()
setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
binding = TestInputPanelBinding.inflate(LayoutInflater.from(context), this)
}

fun bind(onVisibilityChanged: (visible: Boolean) -> Unit = {}) {
this.onVisibilityChanged = onVisibilityChanged

options =
listOf(
InputTypeOption(
binding.pillText,
R.string.test_input,
R.drawable.ic_baseline_keyboard_24,
InputType.TYPE_CLASS_TEXT,
),
InputTypeOption(
binding.pillNumber,
R.string.test_input_number,
R.drawable.ic_baseline_numbers_24,
InputType.TYPE_CLASS_NUMBER,
),
InputTypeOption(
binding.pillPassword,
R.string.test_input_password,
R.drawable.ic_baseline_lock_24,
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD,
),
InputTypeOption(
binding.pillPhone,
R.string.test_input_phone,
R.drawable.ic_baseline_phone_24,
InputType.TYPE_CLASS_PHONE,
),
InputTypeOption(
binding.pillEmail,
R.string.test_input_email,
R.drawable.ic_baseline_email_24,
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS,
),
InputTypeOption(
binding.pillUrl,
R.string.test_input_url,
R.drawable.ic_baseline_link_24,
InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI,
),
)

options.forEach { option ->
option.pill.setOnClickListener {
selectInputType(option, showIme = true)
}
}

binding.testInputClear.setOnClickListener {
binding.testInput.text = null
}

binding.testInput.doAfterTextChanged { text ->
binding.testInputClear.visibility =
if (text.isNullOrEmpty()) View.GONE else View.VISIBLE
}

binding.testInputCollapse.setOnClickListener {
val currentlyExpanded = binding.testInputContent.visibility == View.VISIBLE
setExpanded(expanded = !currentlyExpanded)
}

binding.testInputHide.setOnClickListener {
confirmHide()
}

binding.testInput.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
inputMethodManager.hideSoftInputFromWindow(binding.testInput.windowToken, 0)
}
}

binding.testInputTitle.setOnClickListener {
if (binding.testInputContent.visibility != View.VISIBLE) {
setExpanded(expanded = true)
}
}

selectInputType(options.first(), showIme = false)

val prefs = AppPrefs.defaultInstance().general
if (!prefs.testInputVisible.getValue()) {
setVisible(visible = false, persist = false)
} else {
setExpanded(expanded = prefs.testInputExpanded.getValue(), persist = false)
}
}

fun showExpanded() {
setVisible(visible = true)
setExpanded(expanded = true)
}

private fun confirmHide() {
AlertDialog
.Builder(activity)
.setTitle(R.string.hide_test_input)
.setMessage(R.string.hide_test_input_confirm)
.setPositiveButton(R.string.ok) { _, _ ->
setVisible(visible = false)
}.setNegativeButton(R.string.cancel, null)
.show()
}

private fun setExpanded(
expanded: Boolean,
persist: Boolean = true,
) {
binding.testInputContent.visibility = if (expanded) View.VISIBLE else View.GONE
binding.testInputCollapse.setImageResource(
if (expanded) {
R.drawable.ic_baseline_expand_less_24
} else {
R.drawable.ic_baseline_expand_more_24
},
)
binding.testInputCollapse.contentDescription =
activity.getString(
if (expanded) R.string.collapse_test_input else R.string.expand_test_input,
)

if (!expanded && binding.testInput.hasFocus()) {
inputMethodManager.hideSoftInputFromWindow(binding.testInput.windowToken, 0)
binding.testInput.clearFocus()
}

if (persist) {
AppPrefs.defaultInstance().general.testInputExpanded.setValue(expanded)
}
}

private fun setVisible(
visible: Boolean,
persist: Boolean = true,
) {
visibility = if (visible) View.VISIBLE else View.GONE

if (!visible) {
inputMethodManager.hideSoftInputFromWindow(binding.testInput.windowToken, 0)
binding.testInput.clearFocus()
}

onVisibilityChanged?.invoke(visible)

if (persist) {
AppPrefs.defaultInstance().general.testInputVisible.setValue(visible)
}
}

private fun selectInputType(
selected: InputTypeOption,
showIme: Boolean,
) {
options.forEach { option ->
val isSelected = option == selected
option.pill.isSelected = isSelected
updatePillIcon(option.pill, option.iconRes)
}

binding.testInput.inputType = selected.inputType
binding.testInput.setHint(selected.hintRes)
updateFieldIcon(selected.iconRes)

if (showIme) {
binding.testInput.requestFocus()
inputMethodManager.showSoftInput(binding.testInput, InputMethodManager.SHOW_IMPLICIT)
}
}

private fun updatePillIcon(
pill: TextView,
@DrawableRes iconRes: Int,
) {
val icon = ContextCompat.getDrawable(context, iconRes)?.mutate()
pill.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
TextViewCompat.setCompoundDrawableTintList(pill, pill.textColors)
}

private fun updateFieldIcon(
@DrawableRes iconRes: Int,
) {
val icon =
ContextCompat.getDrawable(context, iconRes)?.mutate()?.also {
DrawableCompat.setTint(it, context.styledColor(android.R.attr.textColorSecondary))
}
binding.testInput.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
binding.testInput.compoundDrawablePadding =
(8 * resources.displayMetrics.density).toInt()
}
}
11 changes: 11 additions & 0 deletions app/src/main/res/color/text_input_type_pill.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2015 - 2025 Rime community
~ SPDX-License-Identifier: GPL-3.0-or-later
-->

<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:color="?android:attr/textColorPrimary"
android:state_selected="true" />
<item android:color="?android:attr/textColorSecondary" />
</selector>
22 changes: 22 additions & 0 deletions app/src/main/res/drawable/bg_input_type_pill.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ SPDX-FileCopyrightText: 2015 - 2025 Rime community
~ SPDX-License-Identifier: GPL-3.0-or-later
-->

<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="?attr/colorControlHighlight" />
<corners android:radius="16dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@android:color/transparent" />
<stroke
android:width="1dp"
android:color="?attr/colorControlNormal" />
<corners android:radius="16dp" />
</shape>
</item>
</selector>
Loading
Loading