Add '<' to drop note, shift left notes & '>' to add note, shift right notes on EditingDialog
This commit is contained in:
parent
335b9b9edc
commit
13154d6acd
2 changed files with 235 additions and 72 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue