This commit was merged in pull request #65.
This commit is contained in:
@@ -37,6 +37,20 @@ object TestTags {
|
||||
const val ATTRIBUTE_EDIT_DISMISS = "attribute_edit_dismiss"
|
||||
const val ATTRIBUTE_EDIT_ERROR = "attribute_edit_error"
|
||||
|
||||
// --- Character data edit dialog ---
|
||||
const val CHARACTER_DATA_EDIT_DIALOG = "character_data_edit_dialog"
|
||||
const val CHARACTER_DATA_EDIT_NAME = "character_data_edit_name"
|
||||
const val CHARACTER_DATA_EDIT_CONCEPT = "character_data_edit_concept"
|
||||
const val CHARACTER_DATA_EDIT_GENDER = "character_data_edit_gender"
|
||||
const val CHARACTER_DATA_EDIT_METATYPE = "character_data_edit_metatype"
|
||||
const val CHARACTER_DATA_EDIT_AGE = "character_data_edit_age"
|
||||
const val CHARACTER_DATA_EDIT_NUYEN = "character_data_edit_nuyen"
|
||||
const val CHARACTER_DATA_EDIT_ESSENCE = "character_data_edit_essence"
|
||||
const val CHARACTER_DATA_EDIT_TOTAL_KARMA = "character_data_edit_total_karma"
|
||||
const val CHARACTER_DATA_EDIT_CURRENT_KARMA = "character_data_edit_current_karma"
|
||||
const val CHARACTER_DATA_EDIT_CONFIRM = "character_data_edit_confirm"
|
||||
const val CHARACTER_DATA_EDIT_DISMISS = "character_data_edit_dismiss"
|
||||
|
||||
// --- Talent edit dialog ---
|
||||
const val TALENT_EDIT_DIALOG = "talent_edit_dialog"
|
||||
const val TALENT_EDIT_INPUT = "talent_edit_input"
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package org.shahondin1624.lib.components.charactermodel
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.shahondin1624.lib.components.TestTags
|
||||
import org.shahondin1624.model.characterdata.CharacterData
|
||||
import org.shahondin1624.model.characterdata.Metatype
|
||||
|
||||
/**
|
||||
* Dialog for editing character data fields: name, concept, metatype,
|
||||
* age, gender, nuyen, essence, karma (total and current).
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CharacterDataEditDialog(
|
||||
characterData: CharacterData,
|
||||
onConfirm: (CharacterData) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf(characterData.name) }
|
||||
var concept by remember { mutableStateOf(characterData.concept) }
|
||||
var gender by remember { mutableStateOf(characterData.gender) }
|
||||
var metatype by remember { mutableStateOf(characterData.metatype) }
|
||||
var ageText by remember { mutableStateOf(characterData.age.toString()) }
|
||||
var nuyenText by remember { mutableStateOf(characterData.nuyen.toString()) }
|
||||
var essenceText by remember { mutableStateOf(formatEssenceForEdit(characterData.essence)) }
|
||||
var totalKarmaText by remember { mutableStateOf(characterData.totalKarma.toString()) }
|
||||
var currentKarmaText by remember { mutableStateOf(characterData.currentKarma.toString()) }
|
||||
|
||||
var metatypeExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val age = ageText.toIntOrNull()
|
||||
val nuyen = nuyenText.toIntOrNull()
|
||||
val essence = essenceText.toFloatOrNull()
|
||||
val totalKarma = totalKarmaText.toIntOrNull()
|
||||
val currentKarma = currentKarmaText.toIntOrNull()
|
||||
|
||||
val isValid = name.isNotBlank() &&
|
||||
age != null && age >= 0 &&
|
||||
nuyen != null && nuyen >= 0 &&
|
||||
essence != null && essence in 0.0f..6.0f &&
|
||||
totalKarma != null && totalKarma >= 0 &&
|
||||
currentKarma != null && currentKarma >= 0 &&
|
||||
currentKarma <= totalKarma
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_DIALOG),
|
||||
title = { Text("Edit Character") },
|
||||
text = {
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Name
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("Name") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_NAME)
|
||||
)
|
||||
|
||||
// Concept
|
||||
OutlinedTextField(
|
||||
value = concept,
|
||||
onValueChange = { concept = it },
|
||||
label = { Text("Concept") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_CONCEPT)
|
||||
)
|
||||
|
||||
// Gender
|
||||
OutlinedTextField(
|
||||
value = gender,
|
||||
onValueChange = { gender = it },
|
||||
label = { Text("Gender") },
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_GENDER)
|
||||
)
|
||||
|
||||
// Metatype dropdown
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = metatypeExpanded,
|
||||
onExpandedChange = { metatypeExpanded = it },
|
||||
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_METATYPE)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = metatype.name,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Metatype") },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = metatypeExpanded) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(MenuAnchorType.PrimaryNotEditable)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = metatypeExpanded,
|
||||
onDismissRequest = { metatypeExpanded = false }
|
||||
) {
|
||||
Metatype.entries.forEach { mt ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(mt.name) },
|
||||
onClick = {
|
||||
metatype = mt
|
||||
metatypeExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Age
|
||||
OutlinedTextField(
|
||||
value = ageText,
|
||||
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) ageText = it },
|
||||
label = { Text("Age") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
isError = age == null && ageText.isNotEmpty(),
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_AGE)
|
||||
)
|
||||
|
||||
// Nuyen
|
||||
OutlinedTextField(
|
||||
value = nuyenText,
|
||||
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) nuyenText = it },
|
||||
label = { Text("Nuyen") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
isError = nuyen == null && nuyenText.isNotEmpty(),
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_NUYEN)
|
||||
)
|
||||
|
||||
// Essence
|
||||
OutlinedTextField(
|
||||
value = essenceText,
|
||||
onValueChange = { newVal ->
|
||||
if (newVal.isEmpty() || newVal.matches(Regex("^\\d*\\.?\\d*$"))) {
|
||||
essenceText = newVal
|
||||
}
|
||||
},
|
||||
label = { Text("Essence (0.0 - 6.0)") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
|
||||
isError = (essence == null || essence !in 0.0f..6.0f) && essenceText.isNotEmpty(),
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_ESSENCE)
|
||||
)
|
||||
|
||||
// Total Karma
|
||||
OutlinedTextField(
|
||||
value = totalKarmaText,
|
||||
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) totalKarmaText = it },
|
||||
label = { Text("Total Karma") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_TOTAL_KARMA)
|
||||
)
|
||||
|
||||
// Current Karma
|
||||
OutlinedTextField(
|
||||
value = currentKarmaText,
|
||||
onValueChange = { if (it.all { c -> c.isDigit() } || it.isEmpty()) currentKarmaText = it },
|
||||
label = { Text("Current Karma") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
isError = currentKarma != null && totalKarma != null && currentKarma > totalKarma,
|
||||
singleLine = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.CHARACTER_DATA_EDIT_CURRENT_KARMA)
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (isValid) {
|
||||
onConfirm(
|
||||
characterData.copy(
|
||||
name = name,
|
||||
concept = concept,
|
||||
gender = gender,
|
||||
metatype = metatype,
|
||||
age = age!!,
|
||||
nuyen = nuyen!!,
|
||||
essence = essence!!,
|
||||
totalKarma = totalKarma!!,
|
||||
currentKarma = currentKarma!!
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
enabled = isValid,
|
||||
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_CONFIRM)
|
||||
) {
|
||||
Text("Save")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.testTag(TestTags.CHARACTER_DATA_EDIT_DISMISS)
|
||||
) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format essence float for editing, showing one decimal place.
|
||||
*/
|
||||
private fun formatEssenceForEdit(essence: Float): String {
|
||||
val wholePart = essence.toInt()
|
||||
val decimalPart = ((essence - wholePart) * 10 + 0.5f).toInt()
|
||||
return "$wholePart.$decimalPart"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.shahondin1624.lib.components.charactermodel
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -19,7 +20,10 @@ import org.shahondin1624.theme.WindowSizeClass
|
||||
* Stacks vertically on Compact, horizontal row on Expanded.
|
||||
*/
|
||||
@Composable
|
||||
fun CharacterHeader(characterData: CharacterData) {
|
||||
fun CharacterHeader(
|
||||
characterData: CharacterData,
|
||||
onEdit: (() -> Unit)? = null
|
||||
) {
|
||||
val windowSizeClass = LocalWindowSizeClass.current
|
||||
val spacing = UiConstants.Spacing.medium(windowSizeClass)
|
||||
|
||||
@@ -27,6 +31,10 @@ fun CharacterHeader(characterData: CharacterData) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(TestTags.PANEL_CHARACTER_HEADER)
|
||||
.then(
|
||||
if (onEdit != null) Modifier.clickable { onEdit() }
|
||||
else Modifier
|
||||
)
|
||||
) {
|
||||
when (windowSizeClass) {
|
||||
WindowSizeClass.Compact -> CompactHeader(characterData, spacing)
|
||||
|
||||
@@ -59,6 +59,9 @@ fun CharacterSheetPage(
|
||||
// Talent edit dialog state
|
||||
var editingTalent by remember { mutableStateOf<TalentDefinition?>(null) }
|
||||
|
||||
// Character data edit dialog state
|
||||
var editingCharacterData by remember { mutableStateOf(false) }
|
||||
|
||||
// Show dice roll result dialog
|
||||
pendingRoll?.let { roll ->
|
||||
DiceRollResultDialog(
|
||||
@@ -99,6 +102,20 @@ fun CharacterSheetPage(
|
||||
)
|
||||
}
|
||||
|
||||
// Show character data edit dialog
|
||||
if (editingCharacterData) {
|
||||
CharacterDataEditDialog(
|
||||
characterData = character.characterData,
|
||||
onConfirm = { newData ->
|
||||
onUpdateCharacter { char ->
|
||||
char.copy(characterData = newData)
|
||||
}
|
||||
editingCharacterData = false
|
||||
},
|
||||
onDismiss = { editingCharacterData = false }
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
// Tab row
|
||||
TabRow(
|
||||
@@ -120,12 +137,15 @@ fun CharacterSheetPage(
|
||||
val onEditTalent: (TalentDefinition) -> Unit = { talent ->
|
||||
editingTalent = talent
|
||||
}
|
||||
val onEditCharacterData: () -> Unit = {
|
||||
editingCharacterData = true
|
||||
}
|
||||
|
||||
when (windowSizeClass) {
|
||||
WindowSizeClass.Expanded -> {
|
||||
// Expanded: two-column layout for Overview and Combat
|
||||
when (selectedTab) {
|
||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing)
|
||||
CharacterTab.Overview -> ExpandedOverviewContent(character, spacing, onEditCharacterData)
|
||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||
@@ -133,7 +153,7 @@ fun CharacterSheetPage(
|
||||
}
|
||||
else -> {
|
||||
when (selectedTab) {
|
||||
CharacterTab.Overview -> OverviewContent(character, spacing)
|
||||
CharacterTab.Overview -> OverviewContent(character, spacing, onEditCharacterData)
|
||||
CharacterTab.Attributes -> AttributesContent(character, spacing, onDiceRoll, onEditAttribute)
|
||||
CharacterTab.Talents -> TalentsContent(character, spacing, onDiceRoll, onEditTalent)
|
||||
CharacterTab.Combat -> CombatContent(character, spacing)
|
||||
@@ -144,7 +164,11 @@ fun CharacterSheetPage(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OverviewContent(character: ShadowrunCharacter, spacing: Dp) {
|
||||
private fun OverviewContent(
|
||||
character: ShadowrunCharacter,
|
||||
spacing: Dp,
|
||||
onEditCharacterData: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -152,14 +176,18 @@ private fun OverviewContent(character: ShadowrunCharacter, spacing: Dp) {
|
||||
.padding(spacing),
|
||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
||||
) {
|
||||
CharacterHeader(character.characterData)
|
||||
CharacterHeader(character.characterData, onEdit = onEditCharacterData)
|
||||
ResourcePanel(character.characterData, character.attributes.edge)
|
||||
DerivedAttributesPanel(character.attributes)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp) {
|
||||
private fun ExpandedOverviewContent(
|
||||
character: ShadowrunCharacter,
|
||||
spacing: Dp,
|
||||
onEditCharacterData: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
@@ -167,7 +195,7 @@ private fun ExpandedOverviewContent(character: ShadowrunCharacter, spacing: Dp)
|
||||
.padding(spacing),
|
||||
verticalArrangement = Arrangement.spacedBy(spacing)
|
||||
) {
|
||||
CharacterHeader(character.characterData)
|
||||
CharacterHeader(character.characterData, onEdit = onEditCharacterData)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing)
|
||||
|
||||
Reference in New Issue
Block a user