Add '<' to drop note, shift left notes & '>' to add note, shift right notes on EditingDialog

This commit is contained in:
Hasinjato 2026-06-17 13:29:27 +03:00
parent 335b9b9edc
commit 13154d6acd
2 changed files with 235 additions and 72 deletions

View file

@ -8,6 +8,8 @@ import mg.dot.feufaro.getGlobalTemplate
import mg.dot.feufaro.launchFilePicker
import mg.dot.feufaro.midi.MidiPitch
import mg.dot.feufaro.midi.MidiWriterKotlin
import mg.dot.feufaro.transformLyricsInput
import mg.dot.feufaro.viewmodel.PartitionMetadata
import java.io.File
import kotlin.math.min
@ -56,6 +58,8 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val REGEX_PAREN_RECURSIVE = Regex("(\\([^\\(\\)]*)\\(([^\\)]*)\\)")
val REGEX_COMMENT = Regex("\\$\\{[^\\}]*\\}")
val REGEX_STRIP_DC = Regex("\\$\\{D[^:]*:[^\\}]*\\}")
val REGEX_NOTE_TOADD = Regex("([drmfsltz][ia]?|[―])([₄₃₂₁¹²³⁴',]*)>([drmfsltz][ia]?|[―])([₄₃₂₁¹²³⁴',]*)")
val REGEX_NOTE_TODROP = Regex("([drmfsltz][ia]?|[―])([₄₃₂₁¹²³⁴',]*)<")
}
var nextTIndex: Int = -1
@ -465,7 +469,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val finalString = updatedSource.joinToString("\n")
val tempDir = System.getProperty("java.io.tmpdir")
val tempDir = System.getProperty("java.io.tmpdir")?: ""
val fileName = filePath.substringAfterLast('/')
val currentTempFile = File(tempDir, fileName)
currentTempFile?.deleteOnExit()
@ -665,6 +669,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val resultTokens = mutableListOf<String>()
var currentLogicalIdx = 0
var i = 0
val consumedTokenIndices = mutableSetOf<Int>()
// println("\nSuivi pour la voix $linePrefix (Pointeur cible: $targetPointer)")
// println(
@ -678,6 +683,14 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
// )
// )
while (i < allTokens.size) {
if (consumedTokenIndices.contains(i)) {
val token = allTokens[i]
if (token.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) {
currentLogicalIdx++
}
i++
continue
}
val token = allTokens[i]
val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")"
@ -713,12 +726,36 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
val oldTokensForThisFragment = regex.findAll(revertMusicalInput(oldNot))
.map { it.value }
.filter { it.matches(Regex("[drmfsltDRFSTzw].*|[-―]")) }
.filter { it.matches(Regex("[drmfsltDRFSTzw].*")) }
.toList()
val dynamicDeleteCount = oldTokensForThisFragment.size
newUserTokens.forEach { ut ->
val dropRegex = Regex("${Regex.escape(ut)}<")
val isThisNoteDropped = dropRegex.containsMatchIn(revertMusicalInput(newNot))
if (isThisNoteDropped) {
if (ut == "z" || ut == "-" || ut == "") {
// println("Suppression de résonance/silence ($ut)...")
var lookAheadIdx = i
var foundDashIdx = -1
while (lookAheadIdx < allTokens.size) {
val lat = allTokens[lookAheadIdx]
if ((lat.matches(Regex("[-―]")) || lat.startsWith("z")) && !consumedTokenIndices.contains(lookAheadIdx)) {
foundDashIdx = lookAheadIdx
break
}
lookAheadIdx++
}
if (foundDashIdx != -1) {
consumedTokenIndices.add(foundDashIdx)
}
} else {
// println("Suppression de la note $ut (gérée par le saut du fragment d'origine)")
}
} else {
val processedToken = if (ut.matches(Regex("[drmfsltDRFSTzw].*"))) {
val (fileBase, fileLvl) = parseNoteAndOctave(allTokens[i])
val (uBase, _) = parseNoteAndOctave(ut)
@ -731,18 +768,29 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
resultTokens.add(processedToken)
if (processedToken.matches(Regex("[drmfsltDRFSTzw].*|[-―]"))) {
// println(String.format("NEW | %-10s | INSERT | %-12d | REPLACE", processedToken, currentLogicalIdx))
// println(
// String.format(
// "NEW | %-10s | INSERT | %-12d | REPLACE",
// processedToken,
// currentLogicalIdx
// )
// )
currentLogicalIdx++
}
}
}
var skipped = 0
while (skipped < dynamicDeleteCount && i < allTokens.size) {
if (consumedTokenIndices.contains(i)) {
i++
continue
}
val nextT = allTokens[i]
if (!nextT.contains("#") && nextT != "/" && nextT != "(" && nextT != ")") {
skipped++
} else if (hasAnchor) {
resultTokens.add(nextT) // Garder les ancres si on est en mode ancre
resultTokens.add(nextT)
}
i++
}
@ -779,9 +827,6 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
initMarkerValue: String,
editState: TUOEditState
): String {
/* On ne modifie pas le template tant que les notes ne sont pas modifiées
* Et tant qu'un marker est ajouter
* Ou une séparateur de mesure est ajouter */
val notesByVoice = editState.notesByVoice
val originalNotes = editState.originalNotes
val separat = editState.sep

View file

@ -19,14 +19,17 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.TUOEditState
import mg.dot.feufaro.solfa.TimeUnitObject
@ -349,6 +352,36 @@ fun TUODetailDialog(
Spacer(modifier = Modifier.height(8.dp))
val activeMessages = (0..3).mapNotNull { voice ->
val currentNote = notes[voice] ?: ""
val validation = validateMusicalInput(currentNote, templateFragment)
if (validation.message.isNotEmpty() && currentNote.isNotEmpty()) validation else null
}
if (activeMessages.isNotEmpty()) {
val firstValidation = activeMessages.first()
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 12.dp),
shape = RoundedCornerShape(8.dp),
color = if (firstValidation.isValid) Color(0xFF10B981) else Color(0xFFF59E0B),
shadowElevation = 2.dp
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = firstValidation.message,
color = if (firstValidation.isValid) Color.White else Color.Black,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
)
}
}
}
// --- SECTION NOTES ---
Column(
modifier = Modifier.fillMaxWidth(),
@ -356,23 +389,8 @@ fun TUODetailDialog(
) {
(0..3).forEach { voice ->
val currentNote = notes[voice] ?: ""
val isValid = validateMusicalInput(currentNote, templateFragment)
val tooltipState = rememberTooltipState(isPersistent = false)
Row(
) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
if (!isValid) {
PlainTooltip(
containerColor = Color(0xFFF59E0B),
contentColor = Color.Black
) {
Text("Veuillez suivre ce format: $templateFragment")
}
}
},
state = tooltipState
val validation = validateMusicalInput(currentNote, templateFragment)
Box(modifier = Modifier.wrapContentSize()
) {
MyTextEditField(
value = currentNote ?: "",
@ -382,7 +400,7 @@ fun TUODetailDialog(
customBrush = SolidColor(Color.White),
isEditable = isEditable,
isAddable = canAdd,
isWarn = !isValid,
isWarn = !validation.isValid,
funTransform = ::transformMusicalInput,
onValueChng = { newValue ->
notes[voice] = newValue
@ -390,14 +408,6 @@ fun TUODetailDialog(
)
}
}
LaunchedEffect(isValid, currentNote) {
if (!isValid && currentNote.isNotEmpty()) {
tooltipState.show()
} else {
tooltipState.dismiss()
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
@ -495,13 +505,17 @@ private fun transformMusicalInput(input: String): String {
.replace("¹'", "²")
.replace("²¹", "³")
.replace("²'", "³")
.replace("³'", "")
.replace("³¹", "")
.replace("₁,", "")
.replace("₁₁", "")
.replace("₂,", "")
.replace("₂₁", "")
.replace("₃,", "")
.replace("₃₁", "")
.replace("'", "¹")
}
fun transformMarkerInput(input: String): String {
private fun transformMarkerInput(input: String): String {
return input
.replace("dc", "DC")
.replace("ds", "DS")
@ -522,14 +536,38 @@ fun MyTextEditField(
funTransform: ((String) -> String)? = null,
onValueChng: (String) -> Unit
) {
val textToShow = if (isAddable && value == "_") "" else value
var textFieldValueState by remember {
mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length)))
}
LaunchedEffect(value) {
if (textFieldValueState.text != value) {
val newSelection = if (textFieldValueState.selection.start <= value.length) {
textFieldValueState.selection
} else {
TextRange(value.length)
}
textFieldValueState = TextFieldValue(text = value, selection = newSelection)
}
}
val textToShow = if (isAddable && value == "_") "" else textFieldValueState.text
BasicTextField(
value = textToShow,
value = textFieldValueState,
onValueChange = { newValue ->
if (isEditable || isAddable) {
val processedVal = funTransform?.invoke(newValue) ?: newValue
onValueChng(processedVal)
val transformedText = funTransform?.invoke(newValue.text) ?: newValue.text
val finalSelection = if (transformedText.length != newValue.text.length) {
if (newValue.selection.start <= transformedText.length) newValue.selection else TextRange(transformedText.length)
} else {
newValue.selection
}
textFieldValueState = TextFieldValue(
text = transformedText,
selection = finalSelection
)
onValueChng(transformedText)
}
},
keyboardOptions = KeyboardOptions(
@ -555,17 +593,60 @@ fun MyTextEditField(
)
}
fun validateMusicalInput(input: String, template: String): Boolean {
if (template.isEmpty()) return true
data class ValidNoteResult(
val isValid: Boolean,
val message: String
)
val cleanInput = input.replace("(", "").replace(")", "").replace(" ", "")
private fun validateMusicalInput(input: String, template: String): ValidNoteResult {
if (template.isEmpty()) return ValidNoteResult(false, "")
/* Ajout > */
val matchAddNote = Solfa.REGEX_NOTE_TOADD.find(input)
if (matchAddNote != null) {
val addedNote = matchAddNote.value.replace(">", "")
val inserted = matchAddNote.groupValues[1] + matchAddNote.groupValues[2]
val shifted = matchAddNote.groupValues[3] + matchAddNote.groupValues[4]
return ValidNoteResult(
isValid = true,
message = "+ $inserted$shifted"
)
}
/* Suppress < */
if (input.contains("<")) {
val matchDropNote = Solfa.REGEX_NOTE_TODROP.find(input)
val isValidDrop = matchDropNote != null/* && input.endsWith("<")*/
if (isValidDrop) {
val noteBase: String = matchDropNote.groupValues[1]
val octave: String = matchDropNote.groupValues[2]
val droppedNote = noteBase + octave
return ValidNoteResult(
isValid = true,
message = "$droppedNote ⌫ ?"
)
} else {
return ValidNoteResult(
isValid = false,
message = "< incorrect"
)
}
}
val cleanInput = input
.replace("(", "")
.replace(")", "")
.replace(" ", "")
.replace(Solfa.REGEX_NOTE_TOADD, "$3$4")
.replace(Solfa.REGEX_NOTE_TODROP, "$1$2")
val cleanTemplate = template.replace("(", "").replace(")", "")
val regexPattern = buildString {
cleanTemplate.forEachIndexed { index, char ->
when (char) {
in 'A'..'Y', in 'a'..'y' -> {
append("(?:―|di|ri|fi|si|ta|[drmfsltz])[¹²³₁₂₃]*")
append("(?:―|di|ri|fi|si|ta|[drmfsltz])[₄₃₂₁¹²³⁴]*")
}
'z' -> {
append(" ?")
@ -602,15 +683,53 @@ fun validateMusicalInput(input: String, template: String): Boolean {
""".trimIndent())*/
return isMatched
return if (isMatched) {
ValidNoteResult(isValid = true, message = "")
} else {
ValidNoteResult(isValid = false, message = "Erreur")
}
}
private fun validateLyricsInput(cleanInput: String, template: String): Boolean {
val vowels = "aeiouyòàéìỳAEIOUY"
val expectedSyllableCount = template.count { it.lowercaseChar() != 'z' && it.isLetter() }
val syllableRegex = Regex("[^aeiouyòàéìỳAEIOUY]*[aeiouyòàéìỳAEIOUY]+[^aeiouyòàéìỳAEIOUY]*", RegexOption.IGNORE_CASE)
val foundSyllables = syllableRegex.findAll(cleanInput).toList()
val rawSyllableCount = foundSyllables.size
val backslashCount = cleanInput.count { it == '\\' }
val actualSyllableCount = if (backslashCount > 0) {
(rawSyllableCount - backslashCount).coerceAtLeast(1)
} else {
rawSyllableCount
}
/*println("""
VALIDATION LYRICS
Template : "$template"
Input : "$cleanInput"
Attendu : $expectedSyllableCount syllabes
Trouvé : $actualSyllableCount syllabes
""".trimIndent())*/
return actualSyllableCount == expectedSyllableCount
}
public fun getGlobalTemplate(editState: TUOEditState): String {
// println("\n--- DÉBOGAGE getGlobalTemplate ---")
val voiceTemplates = (0..3).map { voiceIdx ->
val notes = editState.notesByVoice[voiceIdx] ?: ""
val tpl = notesToTemplate(notes)
// println("Voix $voiceIdx | Notes: '$notes' -> Template: '$tpl'")
val filteredNotes = notes
.replace(Solfa.REGEX_NOTE_TOADD, "$3$4")
.replace("<", "")
val tpl = notesToTemplate(filteredNotes)
// println("Voix $voiceIdx | Notes: '$filteredNotes' -> Template: '$tpl'")
tpl
}
@ -673,7 +792,6 @@ public fun getGlobalTemplate(editState: TUOEditState): String {
previousChars.contains('.') && chars.contains(' ') -> 'z'
else -> ' '
}
// Supprimer espace avant une virgule
if (combinedChar == ',' && result.isNotEmpty() && result.last() == ' ') {
result.deleteCharAt(result.length - 1)
}