Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9686f6cb3 | ||
|
|
d9b55c6811 | ||
|
|
0956bbc4d7 |
@@ -4,3 +4,4 @@ build/
|
||||
app/build/
|
||||
local/
|
||||
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(
|
||||
val pressAction: 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 {
|
||||
return sanitizeDuration(ViewConfiguration.getLongPressTimeout())
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -129,18 +211,26 @@ class NavButtonSettingsStore(context: Context) {
|
||||
companion object {
|
||||
private const val TAG = "NavButtons"
|
||||
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_LONG_PRESS_ACTION = "long_press_action"
|
||||
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"
|
||||
|
||||
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 MAX_DURATION_MS = 10_000
|
||||
const val DURATION_STEP_MS = 100
|
||||
const val HOLD_REPEAT_START_DELAY_MS = 700L
|
||||
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)
|
||||
|
||||
@@ -161,13 +251,15 @@ class NavButtonSettingsStore(context: Context) {
|
||||
pressActionKey(buttonId),
|
||||
longPressActionKey(buttonId),
|
||||
)
|
||||
}
|
||||
} + KEY_SIDE_ARROW_LONG_PRESS_ACTION
|
||||
}
|
||||
|
||||
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 {
|
||||
return "${buttonId.preferencePrefix}_$suffix"
|
||||
}
|
||||
@@ -220,7 +312,10 @@ class NavButtonSettingsStore(context: Context) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -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)) {
|
||||
editor.putLong(KEY_LAST_UPDATED_AT_MS, source.getLong(KEY_LAST_UPDATED_AT_MS, 0L))
|
||||
} else {
|
||||
@@ -252,7 +368,7 @@ class NavButtonSettingsStore(context: Context) {
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@@ -13,10 +14,14 @@ import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import java.text.DecimalFormatSymbols
|
||||
import java.util.Locale
|
||||
|
||||
class SettingsActivity : Activity() {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
@@ -61,6 +66,24 @@ class SettingsActivity : Activity() {
|
||||
false,
|
||||
)
|
||||
if (index > 0) {
|
||||
applySectionTopMargin(sectionView)
|
||||
}
|
||||
|
||||
NavButtonSectionController(sectionView, buttonId).bind()
|
||||
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) {
|
||||
@@ -75,11 +98,6 @@ class SettingsActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
NavButtonSectionController(sectionView, buttonId).bind()
|
||||
sectionsContainer.addView(sectionView)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startTestPreview(durationMs: Int) {
|
||||
stopTestPreview()
|
||||
previewScheduled = true
|
||||
@@ -96,6 +114,91 @@ class SettingsActivity : Activity() {
|
||||
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(
|
||||
sectionView: View,
|
||||
private val buttonId: NavButtonId,
|
||||
@@ -130,7 +233,7 @@ class SettingsActivity : Activity() {
|
||||
applyDuration(config.longPressDurationMs, persist = false)
|
||||
bindDurationInput()
|
||||
bindStepButtons()
|
||||
bindTestButton()
|
||||
attachTestButton(testButton, ::resolveDurationForUse)
|
||||
}
|
||||
|
||||
private fun bindActionSpinner(
|
||||
@@ -138,27 +241,12 @@ class SettingsActivity : Activity() {
|
||||
selectedAction: NavButtonAction,
|
||||
storeAction: (NavButtonId, NavButtonAction) -> Unit,
|
||||
) {
|
||||
val labels = NavButtonAction.entries.map { getString(it.labelRes) }
|
||||
val adapter = ArrayAdapter(
|
||||
this@SettingsActivity,
|
||||
R.layout.item_spinner_selected,
|
||||
labels,
|
||||
)
|
||||
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
|
||||
}
|
||||
bindSpinner(
|
||||
spinner,
|
||||
NavButtonAction.entries,
|
||||
selectedAction,
|
||||
NavButtonAction::labelRes,
|
||||
) { action -> storeAction(buttonId, action) }
|
||||
}
|
||||
|
||||
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) {
|
||||
val current = resolveDurationForUse()
|
||||
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: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
|
||||
android:id="@+id/button_sections_container"
|
||||
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>
|
||||
@@ -2,10 +2,15 @@
|
||||
<string name="app_name">NavButtons</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_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_home">Home 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="long_press_action_label">When long-pressed</string>
|
||||
<string name="long_press_duration_label">Long-press duration (ms)</string>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
android
|
||||
com.android.systemui
|
||||
com.sec.android.app.launcher
|
||||
com.samsung.systemui.navillera
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
version=1.0
|
||||
version=2.0
|
||||
|
||||
Reference in New Issue
Block a user