Spruce up LoRaConfigScreen (#3224)

This commit is contained in:
Phil Oliver 2025-09-28 12:52:42 -04:00 committed by GitHub
parent 8c16052229
commit 3951ebb375
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 332 additions and 239 deletions

View file

@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.protobuf.ProtocolMessageEnum
@ -90,18 +91,29 @@ fun <T> DropDownPreference(
expanded = !expanded
}
},
modifier = modifier.padding(vertical = 8.dp),
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth().menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, enabled),
readOnly = true,
value = items.firstOrNull { it.first == selectedItem }?.second ?: "",
value = "",
onValueChange = {},
label = { Text(title) },
prefix = { Text(title) },
suffix = { Text(items.firstOrNull { it.first == selectedItem }?.second ?: "") },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors(),
colors =
ExposedDropdownMenuDefaults.outlinedTextFieldColors(
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
),
enabled = enabled,
supportingText = { if (summary != null) Text(text = summary) },
supportingText =
if (summary != null) {
{ Text(text = summary, modifier = Modifier.padding(bottom = 8.dp)) }
} else {
null
},
)
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
items

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.common.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PreferenceDivider() {
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
}

View file

@ -17,31 +17,36 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import com.geeksville.mesh.ConfigProtos.Config.LoRaConfig
import com.geeksville.mesh.config
import com.geeksville.mesh.copy
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.PreferenceDivider
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
import org.meshtastic.core.model.Channel
import org.meshtastic.core.model.ChannelOption
import org.meshtastic.core.model.RegionInfo
import org.meshtastic.core.model.numChannels
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.EditTextPreference
import org.meshtastic.core.ui.component.PreferenceCategory
import org.meshtastic.core.ui.component.SignedIntegerEditTextPreference
import org.meshtastic.core.ui.component.SwitchPreference
import org.meshtastic.core.ui.component.TitledCard
@Composable
fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewModel = hiltViewModel()) {
@ -65,184 +70,182 @@ fun LoRaConfigScreen(navController: NavController, viewModel: RadioConfigViewMod
viewModel.setConfig(config)
},
) {
item { PreferenceCategory(text = stringResource(R.string.options)) }
item {
DropDownPreference(
title = stringResource(R.string.region_frequency_plan),
summary = stringResource(id = R.string.config_lora_region_summary),
enabled = state.connected,
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = formState.value.region,
onItemSelected = { formState.value = formState.value.copy { region = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.use_modem_preset),
checked = formState.value.usePreset,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
)
}
item { HorizontalDivider() }
if (formState.value.usePreset) {
item {
TitledCard(title = stringResource(R.string.options)) {
DropDownPreference(
title = stringResource(R.string.modem_preset),
summary = stringResource(id = R.string.config_lora_modem_preset_summary),
enabled = state.connected && formState.value.usePreset,
items =
LoRaConfig.ModemPreset.entries
.filter { it != LoRaConfig.ModemPreset.UNRECOGNIZED }
.map { it to it.name },
selectedItem = formState.value.modemPreset,
onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
)
}
item { HorizontalDivider() }
} else {
item {
EditTextPreference(
title = stringResource(R.string.bandwidth),
value = formState.value.bandwidth,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.spread_factor),
value = formState.value.spreadFactor,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
)
}
item {
EditTextPreference(
title = stringResource(R.string.coding_rate),
value = formState.value.codingRate,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
)
}
}
item { PreferenceCategory(text = stringResource(R.string.advanced)) }
item {
SwitchPreference(
title = stringResource(R.string.ignore_mqtt),
checked = formState.value.ignoreMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.ok_to_mqtt),
checked = formState.value.configOkToMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.tx_enabled),
checked = formState.value.txEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
)
}
item { HorizontalDivider() }
item {
EditTextPreference(
title = stringResource(R.string.hop_limit),
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
value = formState.value.hopLimit,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
)
}
item { HorizontalDivider() }
item {
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.frequency_slot),
summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
value =
if (isFocused || formState.value.channelNum != 0) {
formState.value.channelNum
} else {
primaryChannel.channelNum
},
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocused = it.isFocused },
onValueChanged = {
if (it <= formState.value.numChannels) { // total num of LoRa channels
formState.value = formState.value.copy { channelNum = it }
}
},
)
}
item { HorizontalDivider() }
item {
SwitchPreference(
title = stringResource(R.string.sx126x_rx_boosted_gain),
checked = formState.value.sx126XRxBoostedGain,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
)
}
item { HorizontalDivider() }
item {
var isFocused by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.override_frequency_mhz),
value =
if (isFocused || formState.value.overrideFrequency != 0f) {
formState.value.overrideFrequency
} else {
primaryChannel.radioFreq
},
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocused = it.isFocused },
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
)
}
item { HorizontalDivider() }
item {
SignedIntegerEditTextPreference(
title = stringResource(R.string.tx_power_dbm),
value = formState.value.txPower,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
)
}
if (viewModel.hasPaFan) {
item {
SwitchPreference(
title = stringResource(R.string.pa_fan_disabled),
checked = formState.value.paFanDisabled,
title = stringResource(R.string.region_frequency_plan),
summary = stringResource(id = R.string.config_lora_region_summary),
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
items = RegionInfo.entries.map { it.regionCode to it.description },
selectedItem = formState.value.region,
onItemSelected = { formState.value = formState.value.copy { region = it } },
)
PreferenceDivider()
SwitchPreference(
title = stringResource(R.string.use_modem_preset),
checked = formState.value.usePreset,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { usePreset = it } },
containerColor = Color.Transparent,
)
PreferenceDivider()
if (formState.value.usePreset) {
DropDownPreference(
title = stringResource(R.string.modem_preset),
summary = stringResource(id = R.string.config_lora_modem_preset_summary),
enabled = state.connected && formState.value.usePreset,
items = ChannelOption.entries.map { it.modemPreset to stringResource(it.labelRes) },
selectedItem = formState.value.modemPreset,
onItemSelected = { formState.value = formState.value.copy { modemPreset = it } },
)
} else {
EditTextPreference(
title = stringResource(R.string.bandwidth),
value = formState.value.bandwidth,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { bandwidth = it } },
)
PreferenceDivider()
EditTextPreference(
title = stringResource(R.string.spread_factor),
value = formState.value.spreadFactor,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { spreadFactor = it } },
)
PreferenceDivider()
EditTextPreference(
title = stringResource(R.string.coding_rate),
value = formState.value.codingRate,
enabled = state.connected && !formState.value.usePreset,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { codingRate = it } },
)
}
}
}
item { Spacer(modifier = Modifier.height(16.dp)) }
item {
TitledCard(title = stringResource(R.string.advanced)) {
SwitchPreference(
title = stringResource(R.string.ignore_mqtt),
checked = formState.value.ignoreMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { ignoreMqtt = it } },
containerColor = Color.Transparent,
)
PreferenceDivider()
SwitchPreference(
title = stringResource(R.string.ok_to_mqtt),
checked = formState.value.configOkToMqtt,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { configOkToMqtt = it } },
containerColor = Color.Transparent,
)
PreferenceDivider()
SwitchPreference(
title = stringResource(R.string.tx_enabled),
checked = formState.value.txEnabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { txEnabled = it } },
containerColor = Color.Transparent,
)
PreferenceDivider()
EditTextPreference(
title = stringResource(R.string.hop_limit),
summary = stringResource(id = R.string.config_lora_hop_limit_summary),
value = formState.value.hopLimit,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { hopLimit = it } },
)
PreferenceDivider()
var isFocusedSlot by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.frequency_slot),
summary = stringResource(id = R.string.config_lora_frequency_slot_summary),
value =
if (isFocusedSlot || formState.value.channelNum != 0) {
formState.value.channelNum
} else {
primaryChannel.channelNum
},
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocusedSlot = it.isFocused },
onValueChanged = {
if (it <= formState.value.numChannels) { // total num of LoRa channels
formState.value = formState.value.copy { channelNum = it }
}
},
)
PreferenceDivider()
SwitchPreference(
title = stringResource(R.string.sx126x_rx_boosted_gain),
checked = formState.value.sx126XRxBoostedGain,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { sx126XRxBoostedGain = it } },
containerColor = Color.Transparent,
)
PreferenceDivider()
var isFocusedOverride by remember { mutableStateOf(false) }
EditTextPreference(
title = stringResource(R.string.override_frequency_mhz),
value =
if (isFocusedOverride || formState.value.overrideFrequency != 0f) {
formState.value.overrideFrequency
} else {
primaryChannel.radioFreq
},
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onFocusChanged = { isFocusedOverride = it.isFocused },
onValueChanged = { formState.value = formState.value.copy { overrideFrequency = it } },
)
PreferenceDivider()
SignedIntegerEditTextPreference(
title = stringResource(R.string.tx_power_dbm),
value = formState.value.txPower,
enabled = state.connected,
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }),
onValueChanged = { formState.value = formState.value.copy { txPower = it } },
)
if (viewModel.hasPaFan) {
SwitchPreference(
title = stringResource(R.string.pa_fan_disabled),
checked = formState.value.paFanDisabled,
enabled = state.connected,
onCheckedChange = { formState.value = formState.value.copy { paFanDisabled = it } },
containerColor = Color.Transparent,
)
}
}
item { HorizontalDivider() }
}
}
}

View file

@ -17,6 +17,8 @@
package com.geeksville.mesh.ui.settings.radio.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -25,9 +27,12 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.geeksville.mesh.ui.common.components.MainAppBar
import com.geeksville.mesh.ui.settings.radio.ResponseState
import com.google.protobuf.MessageLite
import org.meshtastic.core.strings.R
import org.meshtastic.core.ui.component.PreferenceFooter
@Composable
@ -61,21 +66,24 @@ fun <T : MessageLite> RadioConfigScreenList(
)
},
) { innerPadding ->
LazyColumn(modifier = Modifier.fillMaxSize().padding(innerPadding)) {
content()
item {
PreferenceFooter(
enabled = enabled && configState.isDirty,
onCancelClicked = {
focusManager.clearFocus()
configState.reset()
},
onSaveClicked = {
focusManager.clearFocus()
onSave(configState.value)
},
)
Column(modifier = Modifier.padding(innerPadding)) {
LazyColumn(modifier = Modifier.fillMaxSize().weight(1f), contentPadding = PaddingValues(16.dp)) {
content()
}
PreferenceFooter(
enabled = enabled && configState.isDirty,
negativeText = stringResource(R.string.discard_changes),
onNegativeClicked = {
focusManager.clearFocus()
configState.reset()
},
positiveText = stringResource(R.string.save_changes),
onPositiveClicked = {
focusManager.clearFocus()
onSave(configState.value)
},
)
}
}
}