Warn user if notes was not correspond to template on edit notes & autocomplete note on ui

This commit is contained in:
Hasinjato 2026-05-12 15:29:52 +03:00
parent cadb2a6b3f
commit 68dd6216b2
3 changed files with 148 additions and 105 deletions

View file

@ -1,6 +1,7 @@
package mg.dot.feufaro package mg.dot.feufaro
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -11,14 +12,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.Savings
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
@ -26,17 +23,9 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.toUpperCase import androidx.compose.ui.unit.*
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties import androidx.compose.ui.window.PopupProperties
import mg.dot.feufaro.data.GridTUOData
import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.solfa.TimeUnitObject import mg.dot.feufaro.solfa.TimeUnitObject
@ -168,23 +157,47 @@ fun TUODetailDialog(
verticalArrangement = Arrangement.spacedBy(5.dp) verticalArrangement = Arrangement.spacedBy(5.dp)
) { ) {
(0..3).forEach { voice -> (0..3).forEach { voice ->
val currentNote = notes[voice] ?: ""
val isValid = validateMusicalInput(currentNote, templateFragment)
val tooltipState = rememberTooltipState(isPersistent = false)
Row( Row(
) { ) {
MyTextEditField( TooltipBox(
value = notes[voice] ?: "", positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
customFontSize = 14.sp, tooltip = {
color = Color.White, if (!isValid) {
customPadding = 8.dp, PlainTooltip(
customBrush = SolidColor(Color.White), containerColor = Color(0xFFF59E0B),
isEditable = isEditable, contentColor = Color.Black
isAddable = canAdd, ) {
funTransform = ::transformMusicalInput, Text("Veuillez suivre ce format: $templateFragment")
onValueChng = { newValue -> }
notes[voice] = newValue }
} },
) state = tooltipState
) {
MyTextEditField(
value = currentNote ?: "",
customFontSize = 14.sp,
color = Color.White,
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
isWarn = !isValid,
funTransform = ::transformMusicalInput,
onValueChng = { newValue ->
notes[voice] = newValue
}
)
}
}
LaunchedEffect(isValid, currentNote) {
if (!isValid && currentNote.isNotEmpty()) {
tooltipState.show()
} else {
tooltipState.dismiss()
}
} }
} }
} }
@ -302,6 +315,7 @@ fun MyTextEditField(
customBrush: Brush, customBrush: Brush,
isEditable: Boolean, isEditable: Boolean,
isAddable: Boolean, isAddable: Boolean,
isWarn: Boolean? = false,
funTransform: ((String) -> String)? = null, funTransform: ((String) -> String)? = null,
onValueChng: (String) -> Unit onValueChng: (String) -> Unit
) { ) {
@ -328,7 +342,62 @@ fun MyTextEditField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp)) .background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
.border(
if (isWarn!!) 1.dp else 0.dp,
if (isWarn!!) Color(0xFFF59E0B) else Color.Transparent,
shape = RoundedCornerShape(4.dp)
)
.padding(customPadding), .padding(customPadding),
cursorBrush = customBrush cursorBrush = customBrush
) )
}
fun validateMusicalInput(input: String, template: String): Boolean {
if (template.isEmpty()) return true
val cleanInput = input.replace("(", "").replace(")", "").replace(" ", "")
val cleanTemplate = template.replace("(", "").replace(")", "")
val regexPattern = buildString {
cleanTemplate.forEachIndexed { index, char ->
when (char) {
in 'A'..'Y' -> {
append("(?:―|di|ri|fi|si|ta|[drmfslt])[¹²³₁₂₃]*")
}
'z' -> {
append(" ?")
}
'.' -> append("")
',' -> append(",")
'-' -> {
val prev = if (index > 0) cleanTemplate[index - 1] else null
val next = if (index < cleanTemplate.lastIndex) cleanTemplate[index + 1] else null
if (prev == '.' && next == ',') {
append("?[\\s]*")
} else {
append("")
}
}
else -> append(Regex.escape(char.toString()))
}
}
}
val regex = Regex("^$regexPattern$")
val isMatched = regex.matches(cleanInput)
/*println("""
DÉBOGAGE VALIDATION
Template Origine : $template
Regex Générée : ^$regexPattern$
Input Reçu : "$input"
Résultat : ${if (isMatched) "VALIDE (OUI)" else "INVALIDE (NON)"}
""".trimIndent())*/
return isMatched
} }

View file

@ -15,7 +15,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBackIos
import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
@ -576,7 +575,6 @@ fun LazyVerticalGridTUO(
selectedTUO = null selectedTUO = null
} }
var showFullChord by remember { mutableStateOf(false) }
val editMode by sharedScreenModel.modeEditor.collectAsState() val editMode by sharedScreenModel.modeEditor.collectAsState()
Column( Column(
@ -760,63 +758,6 @@ fun LazyVerticalGridTUO(
} }
} }
} }
/*Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
measureTUOs.forEachIndexed { indexInMeasure, tuo ->
val chordMap = (1..4).mapNotNull { voice ->
val noteStr = tuo.tuNotes.getOrNull(voice)?.toString()
if (noteStr != null) voice to noteStr else null
}.toMap()
val degreeName = remember(chordMap) {
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeDegree(chordMap) else null
}
val chordName = remember(chordMap) {
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, sharedScreenModel.songKey.value) else null
}
Column(
modifier = Modifier
.width(gridWidthDp / gridColumnCount)
) {
// 1. Définir une plage de tailles de police
val maxFontSize = 13.sp
val minFontSize = 8.sp
var currentFontSize by remember { mutableStateOf(maxFontSize) }
TextButton(
onClick = { showFullChord = !showFullChord },
contentPadding = PaddingValues(0.dp),
modifier = Modifier.height(24.dp).fillMaxWidth() // fillMaxWidth pour occuper toute la colonne
) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val textToDisplay = if (showFullChord) (chordName ?: "") else (degreeName ?: "")
Text(
text = textToDisplay,
onTextLayout = { textLayoutResult ->
if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) {
currentFontSize = (currentFontSize.value * 0.9f).sp
}
},
softWrap = false, // Empêche le retour à la ligne
maxLines = 1,
overflow = TextOverflow.Visible,
style = TextStyle(
color = if (showFullChord) Color(0xFF1B5E20) else Color(0xFF0D47A1),
fontWeight = FontWeight.Bold,
fontSize = currentFontSize // Utilisation de la taille dynamique[cite: 1]
)
)
}
}
}
}
}*/
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
@ -837,11 +778,7 @@ fun LazyVerticalGridTUO(
.combinedClickable( .combinedClickable(
interactionSource = interactionSource, interactionSource = interactionSource,
indication = LocalIndication.current, indication = LocalIndication.current,
onClick = { onClick = { }
if(!editMode) {
sharedScreenModel.seekToGrid(globalIndex)
}
}
) )
.pointerInput(globalIndex) { .pointerInput(globalIndex) {
detectTapGestures( detectTapGestures(
@ -851,6 +788,8 @@ fun LazyVerticalGridTUO(
selectedTUO = oneTUO selectedTUO = oneTUO
selectedIndex = globalIndex selectedIndex = globalIndex
showContextualMenu = true showContextualMenu = true
} else {
sharedScreenModel.seekToGrid(globalIndex)
} }
} }
) )
@ -908,18 +847,7 @@ fun LazyVerticalGridTUO(
},*/ },*/
notesByVoice = (0..3).associate { i -> notesByVoice = (0..3).associate { i ->
val rawNote = oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: "" val rawNote = oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: ""
val fixedNote = val fixedNote = autoFixNote(rawNote, template)
if (!expectedTemplate) {
rawNote
} else {
val count = rawNote.count()
if (/*count < 2 && */!rawNote.contains("")) {
rawNote + "•―"
} else {
rawNote
}
}
i to fixedNote i to fixedNote
}, },
lyricsByStanza = (1..currentStanza).associate { s -> lyricsByStanza = (1..currentStanza).associate { s ->
@ -1299,4 +1227,50 @@ fun EditorActionButtons(onUndo: () -> Unit, onBuild: () -> Unit, onSave: () -> U
IconButton(onClick = onSave) { Icon(Icons.Default.Save, null, tint = Color(0xFF4CAF50)) } IconButton(onClick = onSave) { Icon(Icons.Default.Save, null, tint = Color(0xFF4CAF50)) }
IconButton(onClick = onClose) { Icon(Icons.Default.Close, null, tint = Color.Red) } IconButton(onClick = onClose) { Icon(Icons.Default.Close, null, tint = Color.Red) }
} }
}
fun autoFixNote(rawNote: String, template: String): String {
if (rawNote.isEmpty() || rawNote == "_") return rawNote
val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*")
val actualNotes = noteRegex.findAll(rawNote).map { it.value }.toList()
if (rawNote.contains("") || rawNote.contains("") || rawNote.contains(",")) {
val templateNotesCount = template.count { it in 'A'..'Y' }
if (actualNotes.size >= templateNotesCount) return rawNote
}
val result = StringBuilder()
var noteIdx = 0
for (i in template.indices) {
val char = template[i]
when {
char in 'A'..'Y' -> {
if (noteIdx < actualNotes.size) {
result.append(actualNotes[noteIdx])
noteIdx++
} else {
result.append("")
}
}
char == '-' -> {
val prev = if (i > 0) template[i - 1] else null
val next = if (i < template.lastIndex) template[i + 1] else null
if (prev == '.' && next == ',') result.append(" ") else result.append("")
}
char == 'z' -> result.append(" ")
char == '.' -> {
val currentText = result.toString()
if (!currentText.endsWith("")) result.append("")
}
char == ',' -> {
val currentText = result.toString()
if (!currentText.endsWith(",")) result.append(",")
}
char == '(' -> result.append("(")
char == ')' -> result.append(")")
}
}
return result.toString()
} }

View file

@ -649,7 +649,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_editModeState.value = false _editModeState.value = false
_midiMarkersList.value = emptyList() _midiMarkersList.value = emptyList()
_tuoTimestamps.value = emptyList() _tuoTimestamps.value = emptyList()
updateSearchTxt("") _searchTitle.value = ""
tempTimeUnitObjectList.clear() tempTimeUnitObjectList.clear()
resetGridCount() resetGridCount()
} }