diff --git a/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt b/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt index bc694bbbb3..3c9f302ac3 100644 --- a/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt +++ b/app/src/main/java/com/osfans/trime/data/prefs/AppPrefs.kt @@ -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, diff --git a/app/src/main/java/com/osfans/trime/ui/main/MainActivity.kt b/app/src/main/java/com/osfans/trime/ui/main/MainActivity.kt index f01a0a6287..eb5b403cca 100644 --- a/app/src/main/java/com/osfans/trime/ui/main/MainActivity.kt +++ b/app/src/main/java/com/osfans/trime/ui/main/MainActivity.kt @@ -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 @@ -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() { @@ -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 = @@ -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 { leftMargin = systemBars.left rightMargin = systemBars.right + bottomMargin = maxOf(systemBars.bottom, ime.bottom) } binding.mainToolbar.root.topPadding = systemBars.top windowInsets @@ -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 { @@ -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().navController navController.graph = NavigationRoute.createGraph(navController) binding.mainToolbar.toolbar.setNavigationOnClickListener { @@ -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() { diff --git a/app/src/main/java/com/osfans/trime/ui/main/TestInputPanel.kt b/app/src/main/java/com/osfans/trime/ui/main/TestInputPanel.kt new file mode 100644 index 0000000000..5315274d03 --- /dev/null +++ b/app/src/main/java/com/osfans/trime/ui/main/TestInputPanel.kt @@ -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 + + 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() + } +} diff --git a/app/src/main/res/color/text_input_type_pill.xml b/app/src/main/res/color/text_input_type_pill.xml new file mode 100644 index 0000000000..aca1a6a4fc --- /dev/null +++ b/app/src/main/res/color/text_input_type_pill.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/drawable/bg_input_type_pill.xml b/app/src/main/res/drawable/bg_input_type_pill.xml new file mode 100644 index 0000000000..9b2a65e279 --- /dev/null +++ b/app/src/main/res/drawable/bg_input_type_pill.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_test_input_field.xml b/app/src/main/res/drawable/bg_test_input_field.xml new file mode 100644 index 0000000000..99f785403b --- /dev/null +++ b/app/src/main/res/drawable/bg_test_input_field.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_test_input_panel.xml b/app/src/main/res/drawable/bg_test_input_panel.xml new file mode 100644 index 0000000000..f10b2d8e55 --- /dev/null +++ b/app/src/main/res/drawable/bg_test_input_panel.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_email_24.xml b/app/src/main/res/drawable/ic_baseline_email_24.xml new file mode 100644 index 0000000000..61c5c90d31 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_email_24.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_link_24.xml b/app/src/main/res/drawable/ic_baseline_link_24.xml new file mode 100644 index 0000000000..13b2be53fc --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_link_24.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_lock_24.xml b/app/src/main/res/drawable/ic_baseline_lock_24.xml new file mode 100644 index 0000000000..1146bc5bbe --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_lock_24.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_numbers_24.xml b/app/src/main/res/drawable/ic_baseline_numbers_24.xml new file mode 100644 index 0000000000..30daaa1273 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_numbers_24.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_phone_24.xml b/app/src/main/res/drawable/ic_baseline_phone_24.xml new file mode 100644 index 0000000000..de7ac62a15 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_phone_24.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 98f6e23551..a9626b3f61 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,10 +13,17 @@ + + diff --git a/app/src/main/res/layout/test_input_panel.xml b/app/src/main/res/layout/test_input_panel.xml new file mode 100644 index 0000000000..84925ac3a0 --- /dev/null +++ b/app/src/main/res/layout/test_input_panel.xml @@ -0,0 +1,162 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index b438619c4a..d1ec127b52 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -9,4 +9,8 @@ SPDX-License-Identifier: GPL-3.0-or-later #0A7CB2 #28566d + + #2D2F30 + #616161 + @color/colorAccent diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index de6e7c2111..575ccf8167 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -89,6 +89,24 @@ 快捷栏 已复制 点击空格时,忽略Shift的锁定状态 + 在此测试输入 + 文本 + 数字 + 电话 + 邮箱 + 网址 + 密码 + 输入数字 + 输入电话号码 + 输入邮箱地址 + 输入网址 + 输入密码 + 测试输入 + 显示测试输入 + 隐藏测试输入 + 隐藏测试输入面板?您可以从工具栏恢复。 + 收起测试输入 + 展开测试输入 点击0-9时,忽略Shift的锁定状态 点击符号键时,忽略Shift的锁定状态 点击方向键时,保持Shift的锁定状态 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3903a6d220..ac2540060c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -93,6 +93,24 @@ 快捷欄 已複製 點擊空格時,忽略Shift的鎖定狀態 + 在此測試輸入 + 文字 + 數字 + 電話 + 郵箱 + 網址 + 密碼 + 輸入數字 + 輸入電話號碼 + 輸入郵箱地址 + 輸入網址 + 輸入密碼 + 測試輸入 + 顯示測試輸入 + 隱藏測試輸入 + 隱藏測試輸入面板?您可以從工具列恢復。 + 收起測試輸入 + 展開測試輸入 點擊0-9時,忽略Shift的鎖定狀態 點擊符號鍵時,忽略Shift的鎖定狀態 點選方向鍵時,保持Shift的鎖定狀態 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4459474fe6..42bca7131c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -23,4 +23,8 @@ SPDX-License-Identifier: GPL-3.0-or-later #FFFFFF + #FFFFFF + #E0E0E0 + @color/colorAccent + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83e18bcc05..4e2a385608 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,6 +93,24 @@ Quick bar Copied to clipboard! Ignore Shift locked for Space + Test your input here + Text + Number + Phone + Email + URL + Password + Enter a number + Enter a phone number + Enter an email address + Enter a URL + Enter a password + Test input + Show test input + Hide test input + Hide the test input panel? You can restore it from the toolbar. + Collapse test input + Expand test input Ignore Shift locked for 0-9 Ignore Shift locked for Symbol keys Keep Shift locked for Arrow keys diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7fc58e0666..43a8540e21 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -15,4 +15,21 @@ SPDX-License-Identifier: GPL-3.0-or-later