3 Commits
Author SHA1 Message Date
ajp_anton e9686f6cb3 Release 2.0 2026-06-22 01:25:11 +00:00
ajp_anton d9b55c6811 Clean up navigation module code 2026-06-22 01:11:33 +00:00
ajp_anton 0956bbc4d7 Add side arrow navigation buttons 2026-06-22 00:42:17 +00:00
9 changed files with 1893 additions and 285 deletions
+1
View File
@@ -4,3 +4,4 @@ build/
app/build/ app/build/
local/ local/
local.properties local.properties
*.log
@@ -39,6 +39,22 @@ enum class NavButtonAction(
} }
} }
enum class SideArrowLongPressAction(
val storageValue: String,
@get:StringRes val labelRes: Int,
) {
NOTHING("nothing", R.string.side_arrow_long_press_nothing),
SINGLE_STEP("single_step", R.string.side_arrow_long_press_single_step),
MULTIPLE_STEPS("multiple_steps", R.string.side_arrow_long_press_multiple_steps),
;
companion object {
fun fromStorageValue(value: String?): SideArrowLongPressAction {
return entries.firstOrNull { it.storageValue == value } ?: MULTIPLE_STEPS
}
}
}
data class NavButtonConfig( data class NavButtonConfig(
val pressAction: NavButtonAction, val pressAction: NavButtonAction,
val longPressAction: NavButtonAction, val longPressAction: NavButtonAction,
@@ -91,12 +107,78 @@ class NavButtonSettingsStore(context: Context) {
} }
} }
fun isSideArrowsEnabled(): Boolean {
return currentPreferences().getBoolean(KEY_SIDE_ARROWS_ENABLED, false)
}
fun setSideArrowsEnabled(enabled: Boolean) {
persistChanges {
putBoolean(KEY_SIDE_ARROWS_ENABLED, enabled)
}
}
fun getSideArrowLongPressAction(): SideArrowLongPressAction {
return SideArrowLongPressAction.fromStorageValue(
currentPreferences().getString(KEY_SIDE_ARROW_LONG_PRESS_ACTION, null),
)
}
fun setSideArrowLongPressAction(action: SideArrowLongPressAction) {
persistChanges {
putString(KEY_SIDE_ARROW_LONG_PRESS_ACTION, action.storageValue)
}
}
fun getSideArrowRepeatStepsPerSecond(): Float {
return sanitizeSideArrowRepeatSpeed(
currentPreferences().getFloat(
KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
),
)
}
fun setSideArrowRepeatStepsPerSecond(stepsPerSecond: Float) {
persistChanges {
putFloat(
KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
sanitizeSideArrowRepeatSpeed(stepsPerSecond),
)
}
}
fun getSideArrowLongPressDurationMs(): Int {
return sanitizeDuration(
currentPreferences().getInt(
KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS,
defaultLongPressDurationMs(),
),
)
}
fun setSideArrowLongPressDurationMs(durationMs: Int) {
persistChanges {
putInt(KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS, sanitizeDuration(durationMs))
}
}
fun defaultLongPressDurationMs(): Int { fun defaultLongPressDurationMs(): Int {
return sanitizeDuration(ViewConfiguration.getLongPressTimeout()) return sanitizeDuration(ViewConfiguration.getLongPressTimeout())
} }
fun sanitizeDuration(durationMs: Int): Int { fun sanitizeDuration(durationMs: Int): Int {
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS) return durationMs.coerceIn(MIN_DURATION_MS, MAX_DURATION_MS)
}
fun sanitizeSideArrowRepeatSpeed(stepsPerSecond: Float): Float {
return if (stepsPerSecond.isFinite()) {
stepsPerSecond.coerceIn(
MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
)
} else {
DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
}
} }
private fun currentPreferences(): SharedPreferences { private fun currentPreferences(): SharedPreferences {
@@ -129,18 +211,26 @@ class NavButtonSettingsStore(context: Context) {
companion object { companion object {
private const val TAG = "NavButtons" private const val TAG = "NavButtons"
const val PREFERENCES_NAME = "nav_button_settings" const val PREFERENCES_NAME = "nav_button_settings"
const val ACTION_SETTINGS_CHANGED = "se.ajpanton.navbuttons.SETTINGS_CHANGED"
const val EXTRA_SIDE_ARROWS_ENABLED = "side_arrows_enabled"
const val KEY_PRESS_ACTION = "press_action" const val KEY_PRESS_ACTION = "press_action"
const val KEY_LONG_PRESS_ACTION = "long_press_action" const val KEY_LONG_PRESS_ACTION = "long_press_action"
const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms" const val KEY_LONG_PRESS_DURATION_MS = "long_press_duration_ms"
const val KEY_SIDE_ARROWS_ENABLED = "side_arrows_enabled"
const val KEY_SIDE_ARROW_LONG_PRESS_ACTION = "side_arrow_long_press_action"
const val KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = "side_arrow_repeat_steps_per_second"
const val KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS = "side_arrow_long_press_duration_ms"
private const val KEY_LAST_UPDATED_AT_MS = "last_updated_at_ms" private const val KEY_LAST_UPDATED_AT_MS = "last_updated_at_ms"
const val MIN_LONG_PRESS_DURATION_MS = 100
const val MAX_LONG_PRESS_DURATION_MS = 10_000
const val MIN_DURATION_MS = 100 const val MIN_DURATION_MS = 100
const val MAX_DURATION_MS = 10_000 const val MAX_DURATION_MS = 10_000
const val DURATION_STEP_MS = 100 const val DURATION_STEP_MS = 100
const val HOLD_REPEAT_START_DELAY_MS = 700L const val HOLD_REPEAT_START_DELAY_MS = 700L
const val HOLD_REPEAT_INTERVAL_MS = 100L const val HOLD_REPEAT_INTERVAL_MS = 100L
const val DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = 20f
const val MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = 0.1f
const val MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND = 100f
const val SIDE_ARROW_REPEAT_SPEED_STEP = 1f
fun pressActionKey(buttonId: NavButtonId): String = key(buttonId, KEY_PRESS_ACTION) fun pressActionKey(buttonId: NavButtonId): String = key(buttonId, KEY_PRESS_ACTION)
@@ -161,13 +251,15 @@ class NavButtonSettingsStore(context: Context) {
pressActionKey(buttonId), pressActionKey(buttonId),
longPressActionKey(buttonId), longPressActionKey(buttonId),
) )
} } + KEY_SIDE_ARROW_LONG_PRESS_ACTION
} }
private val durationKeys by lazy(LazyThreadSafetyMode.NONE) { private val durationKeys by lazy(LazyThreadSafetyMode.NONE) {
NavButtonId.entries.map(::longPressDurationKey) NavButtonId.entries.map(::longPressDurationKey) + KEY_SIDE_ARROW_LONG_PRESS_DURATION_MS
} }
private val floatKeys = listOf(KEY_SIDE_ARROW_REPEAT_STEPS_PER_SECOND)
private fun key(buttonId: NavButtonId, suffix: String): String { private fun key(buttonId: NavButtonId, suffix: String): String {
return "${buttonId.preferencePrefix}_$suffix" return "${buttonId.preferencePrefix}_$suffix"
} }
@@ -220,7 +312,10 @@ class NavButtonSettingsStore(context: Context) {
} }
private fun hasStoredConfig(preferences: SharedPreferences): Boolean { private fun hasStoredConfig(preferences: SharedPreferences): Boolean {
return actionKeys.any(preferences::contains) || durationKeys.any(preferences::contains) return actionKeys.any(preferences::contains) ||
durationKeys.any(preferences::contains) ||
floatKeys.any(preferences::contains) ||
preferences.contains(KEY_SIDE_ARROWS_ENABLED)
} }
private fun copyPreferences(source: SharedPreferences, target: SharedPreferences) { private fun copyPreferences(source: SharedPreferences, target: SharedPreferences) {
@@ -242,6 +337,27 @@ class NavButtonSettingsStore(context: Context) {
} }
} }
for (key in floatKeys) {
if (source.contains(key)) {
editor.putFloat(
key,
source.getFloat(key, DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND)
.coerceIn(
MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND,
),
)
} else {
editor.remove(key)
}
}
if (source.contains(KEY_SIDE_ARROWS_ENABLED)) {
editor.putBoolean(KEY_SIDE_ARROWS_ENABLED, source.getBoolean(KEY_SIDE_ARROWS_ENABLED, false))
} else {
editor.remove(KEY_SIDE_ARROWS_ENABLED)
}
if (source.contains(KEY_LAST_UPDATED_AT_MS)) { if (source.contains(KEY_LAST_UPDATED_AT_MS)) {
editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L)) editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L))
} else { } else {
@@ -252,7 +368,7 @@ class NavButtonSettingsStore(context: Context) {
} }
private fun sanitizeDuration(durationMs: Int): Int { private fun sanitizeDuration(durationMs: Int): Int {
return durationMs.coerceIn(MIN_LONG_PRESS_DURATION_MS, MAX_LONG_PRESS_DURATION_MS) return durationMs.coerceIn(MIN_DURATION_MS, MAX_DURATION_MS)
} }
} }
} }
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,7 @@
package se.ajpanton.navbuttons package se.ajpanton.navbuttons
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -13,10 +14,14 @@ import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Button import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText import android.widget.EditText
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import java.text.DecimalFormatSymbols
import java.util.Locale
class SettingsActivity : Activity() { class SettingsActivity : Activity() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
@@ -61,23 +66,36 @@ class SettingsActivity : Activity() {
false, false,
) )
if (index > 0) { if (index > 0) {
val marginTop = resources.getDimensionPixelSize(R.dimen.section_spacing) applySectionTopMargin(sectionView)
val layoutParams = sectionView.layoutParams as? ViewGroup.MarginLayoutParams
if (layoutParams != null) {
layoutParams.topMargin = marginTop
} else {
sectionView.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
topMargin = marginTop
}
}
} }
NavButtonSectionController(sectionView, buttonId).bind() NavButtonSectionController(sectionView, buttonId).bind()
sectionsContainer.addView(sectionView) sectionsContainer.addView(sectionView)
} }
val sideArrowsSectionView = inflater.inflate(
R.layout.item_side_arrows_section,
sectionsContainer,
false,
)
applySectionTopMargin(sideArrowsSectionView)
SideArrowsSectionController(sideArrowsSectionView).bind()
sectionsContainer.addView(sideArrowsSectionView)
}
private fun applySectionTopMargin(sectionView: View) {
val marginTop = resources.getDimensionPixelSize(R.dimen.section_spacing)
val layoutParams = sectionView.layoutParams as? ViewGroup.MarginLayoutParams
if (layoutParams != null) {
layoutParams.topMargin = marginTop
} else {
sectionView.layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
topMargin = marginTop
}
}
} }
private fun startTestPreview(durationMs: Int) { private fun startTestPreview(durationMs: Int) {
@@ -96,6 +114,91 @@ class SettingsActivity : Activity() {
previewOverlay.visibility = View.GONE previewOverlay.visibility = View.GONE
} }
private fun attachRepeatButton(button: Button, action: () -> Unit) {
var repeated = false
val repeatRunnable = object : Runnable {
override fun run() {
repeated = true
action()
handler.postDelayed(this, NavButtonSettingsStore.HOLD_REPEAT_INTERVAL_MS)
}
}
button.setOnClickListener {
if (!repeated) {
action()
}
}
button.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
repeated = false
handler.postDelayed(
repeatRunnable,
NavButtonSettingsStore.HOLD_REPEAT_START_DELAY_MS,
)
false
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
handler.removeCallbacks(repeatRunnable)
false
}
else -> false
}
}
}
private fun attachTestButton(button: Button, durationProvider: () -> Int) {
button.setOnTouchListener { view, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
view.isPressed = true
startTestPreview(durationProvider())
true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
view.isPressed = false
stopTestPreview()
true
}
else -> false
}
}
}
private fun <T> bindSpinner(
spinner: Spinner,
items: List<T>,
selectedItem: T,
labelRes: (T) -> Int,
onSelected: (T) -> Unit,
) {
val adapter = ArrayAdapter(
this,
R.layout.item_spinner_selected,
items.map { getString(labelRes(it)) },
)
adapter.setDropDownViewResource(R.layout.item_spinner_dropdown)
spinner.adapter = adapter
spinner.setSelection(items.indexOf(selectedItem).coerceAtLeast(0), false)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
onSelected(items[position])
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
}
}
private inner class NavButtonSectionController( private inner class NavButtonSectionController(
sectionView: View, sectionView: View,
private val buttonId: NavButtonId, private val buttonId: NavButtonId,
@@ -130,7 +233,7 @@ class SettingsActivity : Activity() {
applyDuration(config.longPressDurationMs, persist = false) applyDuration(config.longPressDurationMs, persist = false)
bindDurationInput() bindDurationInput()
bindStepButtons() bindStepButtons()
bindTestButton() attachTestButton(testButton, ::resolveDurationForUse)
} }
private fun bindActionSpinner( private fun bindActionSpinner(
@@ -138,27 +241,12 @@ class SettingsActivity : Activity() {
selectedAction: NavButtonAction, selectedAction: NavButtonAction,
storeAction: (NavButtonId, NavButtonAction) -> Unit, storeAction: (NavButtonId, NavButtonAction) -> Unit,
) { ) {
val labels = NavButtonAction.entries.map { getString(it.labelRes) } bindSpinner(
val adapter = ArrayAdapter( spinner,
this@SettingsActivity, NavButtonAction.entries,
R.layout.item_spinner_selected, selectedAction,
labels, NavButtonAction::labelRes,
) ) { action -> storeAction(buttonId, action) }
adapter.setDropDownViewResource(R.layout.item_spinner_dropdown)
spinner.adapter = adapter
spinner.setSelection(NavButtonAction.entries.indexOf(selectedAction), false)
spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long,
) {
storeAction(buttonId, NavButtonAction.entries[position])
}
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
}
} }
private fun bindDurationInput() { private fun bindDurationInput() {
@@ -206,67 +294,6 @@ class SettingsActivity : Activity() {
} }
} }
private fun bindTestButton() {
testButton.setOnTouchListener { view, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
view.isPressed = true
startTestPreview(resolveDurationForUse())
true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
view.isPressed = false
stopTestPreview()
true
}
else -> false
}
}
}
private fun attachRepeatButton(button: Button, action: () -> Unit) {
var repeated = false
val repeatRunnable = object : Runnable {
override fun run() {
repeated = true
action()
handler.postDelayed(this, NavButtonSettingsStore.HOLD_REPEAT_INTERVAL_MS)
}
}
button.setOnClickListener {
if (!repeated) {
action()
}
}
button.setOnTouchListener { view, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
repeated = false
handler.postDelayed(
repeatRunnable,
NavButtonSettingsStore.HOLD_REPEAT_START_DELAY_MS,
)
false
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL,
-> {
handler.removeCallbacks(repeatRunnable)
false
}
else -> false
}
}
}
private fun adjustDuration(deltaMs: Int) { private fun adjustDuration(deltaMs: Int) {
val current = resolveDurationForUse() val current = resolveDurationForUse()
val updated = (current + deltaMs).coerceIn( val updated = (current + deltaMs).coerceIn(
@@ -303,4 +330,268 @@ class SettingsActivity : Activity() {
} }
} }
} }
private inner class SideArrowsSectionController(sectionView: View) {
private val sideArrowsCheckbox: CheckBox = sectionView.findViewById(R.id.side_arrows_checkbox)
private val optionsContainer: LinearLayout =
sectionView.findViewById(R.id.side_arrows_options_container)
private val longPressActionSpinner: Spinner =
sectionView.findViewById(R.id.side_arrow_long_press_action_spinner)
private val repeatSpeedContainer: LinearLayout =
sectionView.findViewById(R.id.side_arrow_repeat_speed_container)
private val repeatSpeedInput: EditText =
sectionView.findViewById(R.id.side_arrow_repeat_speed_input)
private val speedMinusButton: Button =
sectionView.findViewById(R.id.side_arrow_speed_minus_button)
private val speedPlusButton: Button =
sectionView.findViewById(R.id.side_arrow_speed_plus_button)
private val durationInput: EditText =
sectionView.findViewById(R.id.side_arrow_long_press_duration_input)
private val durationMinusButton: Button =
sectionView.findViewById(R.id.side_arrow_duration_minus_button)
private val durationPlusButton: Button =
sectionView.findViewById(R.id.side_arrow_duration_plus_button)
private val testButton: Button = sectionView.findViewById(R.id.side_arrow_test_button)
private val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator
private var lastValidDurationMs = settingsStore.defaultLongPressDurationMs()
private var lastValidRepeatSpeed = NavButtonSettingsStore.DEFAULT_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
private var updatingDurationText = false
private var updatingRepeatSpeedText = false
fun bind() {
sideArrowsCheckbox.isChecked = settingsStore.isSideArrowsEnabled()
bindLongPressActionSpinner()
applyRepeatSpeed(settingsStore.getSideArrowRepeatStepsPerSecond(), persist = false)
bindRepeatSpeedInput()
bindRepeatSpeedButtons()
applyDuration(settingsStore.getSideArrowLongPressDurationMs(), persist = false)
bindDurationInput()
bindDurationButtons()
attachTestButton(testButton, ::resolveDurationForUse)
updateOptionsEnabled()
sideArrowsCheckbox.setOnCheckedChangeListener { _, isChecked ->
settingsStore.setSideArrowsEnabled(isChecked)
updateOptionsEnabled()
sendBroadcast(
Intent(NavButtonSettingsStore.ACTION_SETTINGS_CHANGED).putExtra(
NavButtonSettingsStore.EXTRA_SIDE_ARROWS_ENABLED,
isChecked,
),
)
}
}
private fun bindLongPressActionSpinner() {
bindSpinner(
longPressActionSpinner,
SideArrowLongPressAction.entries,
settingsStore.getSideArrowLongPressAction(),
SideArrowLongPressAction::labelRes,
) { action ->
settingsStore.setSideArrowLongPressAction(action)
updateRepeatSpeedVisibility(action)
}
updateRepeatSpeedVisibility(settingsStore.getSideArrowLongPressAction())
}
private fun bindRepeatSpeedInput() {
repeatSpeedInput.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
normalizeTypedRepeatSpeed()
}
}
repeatSpeedInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int,
) = Unit
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int,
) = Unit
override fun afterTextChanged(editable: Editable?) {
if (updatingRepeatSpeedText) {
return
}
val parsed = parseRepeatSpeed(editable?.toString()) ?: return
if (parsed in NavButtonSettingsStore.MIN_SIDE_ARROW_REPEAT_STEPS_PER_SECOND..
NavButtonSettingsStore.MAX_SIDE_ARROW_REPEAT_STEPS_PER_SECOND
) {
lastValidRepeatSpeed = parsed
settingsStore.setSideArrowRepeatStepsPerSecond(parsed)
}
}
})
}
private fun bindRepeatSpeedButtons() {
attachRepeatButton(speedMinusButton) {
adjustRepeatSpeed(-NavButtonSettingsStore.SIDE_ARROW_REPEAT_SPEED_STEP)
}
attachRepeatButton(speedPlusButton) {
adjustRepeatSpeed(NavButtonSettingsStore.SIDE_ARROW_REPEAT_SPEED_STEP)
}
}
private fun bindDurationInput() {
durationInput.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
normalizeTypedDuration()
}
}
durationInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?,
start: Int,
count: Int,
after: Int,
) = Unit
override fun onTextChanged(
s: CharSequence?,
start: Int,
before: Int,
count: Int,
) = Unit
override fun afterTextChanged(editable: Editable?) {
if (updatingDurationText) {
return
}
val parsed = editable?.toString()?.toIntOrNull() ?: return
if (parsed in NavButtonSettingsStore.MIN_DURATION_MS..NavButtonSettingsStore.MAX_DURATION_MS) {
lastValidDurationMs = parsed
settingsStore.setSideArrowLongPressDurationMs(parsed)
}
}
})
}
private fun bindDurationButtons() {
attachRepeatButton(durationMinusButton) {
adjustDuration(-NavButtonSettingsStore.DURATION_STEP_MS)
}
attachRepeatButton(durationPlusButton) {
adjustDuration(NavButtonSettingsStore.DURATION_STEP_MS)
}
}
private fun updateOptionsEnabled() {
val enabled = sideArrowsCheckbox.isChecked
optionsContainer.alpha = if (enabled) 1f else 0.45f
setEnabledRecursively(optionsContainer, enabled)
}
private fun setEnabledRecursively(view: View, enabled: Boolean) {
view.isEnabled = enabled
if (view is ViewGroup) {
repeat(view.childCount) { index ->
setEnabledRecursively(view.getChildAt(index), enabled)
}
}
}
private fun updateRepeatSpeedVisibility(action: SideArrowLongPressAction) {
repeatSpeedContainer.visibility =
if (action == SideArrowLongPressAction.MULTIPLE_STEPS) View.VISIBLE else View.GONE
}
private fun adjustRepeatSpeed(delta: Float) {
applyRepeatSpeed(resolveRepeatSpeedForUse() + delta, persist = true)
}
private fun normalizeTypedRepeatSpeed(): Float {
val parsed = parseRepeatSpeed(repeatSpeedInput.text.toString()) ?: lastValidRepeatSpeed
val normalized = settingsStore.sanitizeSideArrowRepeatSpeed(parsed)
applyRepeatSpeed(normalized, persist = true)
return normalized
}
private fun resolveRepeatSpeedForUse(): Float {
return normalizeTypedRepeatSpeed()
}
private fun applyRepeatSpeed(stepsPerSecond: Float, persist: Boolean) {
val sanitized = settingsStore.sanitizeSideArrowRepeatSpeed(stepsPerSecond)
val text = formatRepeatSpeed(sanitized)
updatingRepeatSpeedText = true
if (repeatSpeedInput.text.toString() != text) {
repeatSpeedInput.setText(text)
repeatSpeedInput.setSelection(repeatSpeedInput.text.length)
}
updatingRepeatSpeedText = false
lastValidRepeatSpeed = sanitized
if (persist) {
settingsStore.setSideArrowRepeatStepsPerSecond(sanitized)
}
}
private fun parseRepeatSpeed(text: String?): Float? {
return text
?.trim()
?.replace(',', '.')
?.toFloatOrNull()
}
private fun formatRepeatSpeed(stepsPerSecond: Float): String {
val raw = if (stepsPerSecond == stepsPerSecond.toInt().toFloat()) {
stepsPerSecond.toInt().toString()
} else {
String.format(Locale.US, "%.2f", stepsPerSecond)
.trimEnd('0')
.trimEnd('.')
}
return raw.replace('.', decimalSeparator)
}
private fun adjustDuration(deltaMs: Int) {
val current = resolveDurationForUse()
val updated = (current + deltaMs).coerceIn(
NavButtonSettingsStore.MIN_DURATION_MS,
NavButtonSettingsStore.MAX_DURATION_MS,
)
applyDuration(updated, persist = true)
}
private fun normalizeTypedDuration(): Int {
val parsed = durationInput.text.toString().toIntOrNull() ?: lastValidDurationMs
val normalized = settingsStore.sanitizeDuration(parsed)
applyDuration(normalized, persist = true)
return normalized
}
private fun resolveDurationForUse(): Int {
return normalizeTypedDuration()
}
private fun applyDuration(durationMs: Int, persist: Boolean) {
val sanitized = settingsStore.sanitizeDuration(durationMs)
val text = sanitized.toString()
updatingDurationText = true
if (durationInput.text.toString() != text) {
durationInput.setText(text)
durationInput.setSelection(durationInput.text.length)
}
updatingDurationText = false
lastValidDurationMs = sanitized
if (persist) {
settingsStore.setSideArrowLongPressDurationMs(sanitized)
}
}
}
} }
@@ -29,15 +29,6 @@
android:textSize="30sp" android:textSize="30sp"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:lineSpacingExtra="3dp"
android:text="@string/settings_summary"
android:textColor="@color/settings_text_secondary"
android:textSize="15sp" />
<LinearLayout <LinearLayout
android:id="@+id/button_sections_container" android:id="@+id/button_sections_container"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_button_section"
android:elevation="2dp"
android:orientation="vertical"
android:padding="18dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/side_arrows_section_title"
android:textColor="@color/settings_text_primary"
android:textSize="22sp"
android:textStyle="bold" />
<CheckBox
android:id="@+id/side_arrows_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="14dp"
android:buttonTint="@color/settings_label_text"
android:saveEnabled="false"
android:text="@string/side_arrows_enable_label"
android:textColor="@color/settings_text_primary"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/side_arrows_options_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/long_press_action_label"
android:textColor="@color/settings_label_text"
android:textSize="13sp"
android:textStyle="bold" />
<Spinner
android:id="@+id/side_arrow_long_press_action_spinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:background="@drawable/bg_spinner_field"
android:dropDownWidth="match_parent"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp"
android:popupBackground="@drawable/bg_spinner_popup"
android:saveEnabled="false" />
<LinearLayout
android:id="@+id/side_arrow_repeat_speed_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/side_arrow_repeat_speed_label"
android:textColor="@color/settings_label_text"
android:textSize="13sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/side_arrow_speed_minus_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="48dp"
android:text="@string/minus_button_label"
android:textAllCaps="false"
android:textSize="22sp" />
<EditText
android:id="@+id/side_arrow_repeat_speed_input"
android:layout_width="88dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:gravity="center"
android:importantForAutofill="no"
android:inputType="numberDecimal"
android:maxLength="6"
android:saveEnabled="false"
android:singleLine="true"
android:textColor="@color/settings_input_text"
android:textSize="18sp" />
<Button
android:id="@+id/side_arrow_speed_plus_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:minWidth="48dp"
android:text="@string/plus_button_label"
android:textAllCaps="false"
android:textSize="20sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/long_press_duration_label"
android:textColor="@color/settings_label_text"
android:textSize="13sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<Button
android:id="@+id/side_arrow_duration_minus_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="48dp"
android:text="@string/minus_button_label"
android:textAllCaps="false"
android:textSize="22sp" />
<EditText
android:id="@+id/side_arrow_long_press_duration_input"
android:layout_width="88dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:gravity="center"
android:importantForAutofill="no"
android:inputType="number"
android:maxLength="5"
android:saveEnabled="false"
android:singleLine="true"
android:textColor="@color/settings_input_text"
android:textSize="18sp" />
<Button
android:id="@+id/side_arrow_duration_plus_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:minWidth="48dp"
android:text="@string/plus_button_label"
android:textAllCaps="false"
android:textSize="20sp" />
</LinearLayout>
<Button
android:id="@+id/side_arrow_test_button"
android:layout_width="wrap_content"
android:layout_height="54dp"
android:layout_marginTop="14dp"
android:minWidth="160dp"
android:text="@string/test_button_label"
android:textAllCaps="false"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
+6 -1
View File
@@ -2,10 +2,15 @@
<string name="app_name">NavButtons</string> <string name="app_name">NavButtons</string>
<string name="xposed_desc">Customize navigation button press and long-press actions.</string> <string name="xposed_desc">Customize navigation button press and long-press actions.</string>
<string name="settings_title">Nav Button Settings</string> <string name="settings_title">Nav Button Settings</string>
<string name="settings_summary">Choose what each navigation button should do on press and long press, then tune how long a long press should feel.</string>
<string name="button_back">Back Button</string> <string name="button_back">Back Button</string>
<string name="button_home">Home Button</string> <string name="button_home">Home Button</string>
<string name="button_recents">Recents Button</string> <string name="button_recents">Recents Button</string>
<string name="side_arrows_section_title">Side arrows</string>
<string name="side_arrows_enable_label">Enable side arrows</string>
<string name="side_arrow_long_press_nothing">Nothing</string>
<string name="side_arrow_long_press_single_step">Single step</string>
<string name="side_arrow_long_press_multiple_steps">Multiple steps</string>
<string name="side_arrow_repeat_speed_label">Repeat speed (steps/sec)</string>
<string name="press_action_label">When pressed</string> <string name="press_action_label">When pressed</string>
<string name="long_press_action_label">When long-pressed</string> <string name="long_press_action_label">When long-pressed</string>
<string name="long_press_duration_label">Long-press duration (ms)</string> <string name="long_press_duration_label">Long-press duration (ms)</string>
@@ -1,3 +1,4 @@
android android
com.android.systemui com.android.systemui
com.sec.android.app.launcher com.sec.android.app.launcher
com.samsung.systemui.navillera
+1 -1
View File
@@ -1 +1 @@
version=1.0 version=2.0