From 792efc5aeb097384dab658d26541f4e1f4947f10 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Tue, 21 Jul 2026 12:16:16 +0300 Subject: [PATCH] Add button 'Remove | add grid' on lastGrid TUO & reposition triolet,hairpin arc --- .../mg/dot/feufaro/solfa/GridActionMenu.kt | 53 ++++ .../kotlin/mg/dot/feufaro/solfa/Solfa.kt | 234 ++++++++++++++++-- .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 51 +++- 3 files changed, 309 insertions(+), 29 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt new file mode 100644 index 0000000..c5a3385 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/GridActionMenu.kt @@ -0,0 +1,53 @@ +package mg.dot.feufaro.solfa + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun GridActionMenu( + onAddMeasureRight: () -> Unit, + onDeleteMeasure: () -> Unit, +) { + Column { + Row { + FilledTonalButton( + onClick = { + onAddMeasureRight() + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null + ) + } + } + Row { + OutlinedButton( + onClick = { + onDeleteMeasure() + }, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.secondary + ), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt index 53cd1b4..b3a3417 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -22,7 +22,6 @@ import kotlin.math.min //@todo: bug ffpm-629.txt sssszd au lieu de ssss-d(-rrrrtdr) //@todo: bug ews-103.txt Triolet adds one blank //@todo: bug ffpm-167-1.txt shoud start with : and not | -//@todo: bug ffpm-491-2.txt ${x:} and ${DS: } not in sync class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository: FileRepository) { private val T: MutableList = mutableListOf() @@ -624,7 +623,12 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository fragments: List, targetIdx: Int, editState: TUOEditState - ) { + ) { + // Ajout/Enlèvement grille de fin + if (editState.tuoIndex == 9999) { + AddOrRemoveGridActionOnNotes(lines, editState) + return + } val notesByVoice = editState.notesByVoice val originalNotes = editState.originalNotes val templateFragment = editState.templateFragment @@ -821,12 +825,76 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository } } + private fun AddOrRemoveGridActionOnNotes( + lines: MutableList, + editState: TUOEditState + ) { + for (voiceNum in 0..3) { + val linePrefix = "N${voiceNum + 1}:" + val lineIdx = lines.indexOfFirst { it.startsWith(linePrefix) } + + if (lineIdx != -1) { + val currentLine = lines[lineIdx] + val prefix = currentLine.substringBefore(":") + ":" + val noteBody = currentLine.substringAfter(":") + + val regex = Regex("#\\S[',]*|\\s#\\S[',]*|/|\\(|\\)|[drmfsltDRFSTzw][0-9'¹²³⁴₁₂₃₄,]*|[-.―•]") + val allTokens = regex.findAll(expandNotes(noteBody.replace(Regex("\\s+"), ""))) + .map { it.value } + .filter { it != "." } + .toMutableList() + + /*if (editState.templateFragment == "-") { + var lastNoteIdx = -1 + for (idx in allTokens.indices.reversed()) { + val token = allTokens[idx] + val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")" + if (!isStructural) { + lastNoteIdx = idx + break + } + } + if (lastNoteIdx != -1) { + allTokens.removeAt(lastNoteIdx) + } + } else {*/ + // + + if (editState.templateFragment == "+") { + var lastNoteIdx = -1 + for (idx in allTokens.indices.reversed()) { + val token = allTokens[idx] + val isStructural = token.contains("#") || token == "/" || token == "(" || token == ")" + if (!isStructural) { + lastNoteIdx = idx + break + } + } + + if (lastNoteIdx != -1) { + allTokens.add(lastNoteIdx + 1, "d") + } else { + allTokens.add("d") + } + } + + lines[lineIdx] = prefix + allTokens.joinToString("") + } + } + return + } + private fun updateMarkerSource( templateLine: String, targetPointer: Int, initMarkerValue: String, editState: TUOEditState ): String { + /* Add or Remove lastGrid */ + if (editState.tuoIndex == 9999) { + val finalResult = AddOrRemoveGridActionOnTemplate(templateLine, editState) + return finalResult + } + /* Set Pending Hairpin */ when (initMarkerValue) { @@ -1060,6 +1128,126 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository return finalResult } + private fun AddOrRemoveGridActionOnTemplate(templateLine: String, editState: TUOEditState): String { + val prefix: String + val body: String + val suffix: String + + val isT0 = templateLine.startsWith("T0") + if (isT0) { + prefix = "T0:{" + body = templateLine.substringAfter("{").substringBeforeLast("}") + suffix = "}" + } else { + val match = Regex("^(.*?z\\d|.*?z.):").find(templateLine) + if (match != null) { + prefix = match.value + body = templateLine.substring(match.value.length) + suffix = "" + } else { + prefix = templateLine.substringBefore(":") + ":" + body = templateLine.substringAfter(":") + suffix = "" + } + } + + val separators = setOf(':', '|', '!', '/', ' ') + val tokens = mutableListOf() + val isSeparator = mutableListOf() + + var temp = StringBuilder() + for (char in body) { + if (char in separators) { + if (temp.isNotEmpty()) { + tokens.add(temp.toString()) + isSeparator.add(false) + temp = StringBuilder() + } + tokens.add(char.toString()) + isSeparator.add(true) + } else { + temp.append(char) + } + } + if (temp.isNotEmpty()) { + tokens.add(temp.toString()) + isSeparator.add(false) + } + + val updatedBody = if (editState.templateFragment == "-") { + //- + var lastNoteBlockIdx = -1 + for (idx in tokens.indices.reversed()) { + if (!isSeparator[idx]) { + lastNoteBlockIdx = idx + break + } + } + + if (lastNoteBlockIdx != -1) { + tokens.removeAt(lastNoteBlockIdx) + isSeparator.removeAt(lastNoteBlockIdx) + } + + val cleanTokens = mutableListOf() + var lastWasSeparator = false + for (i in tokens.indices) { + val token = tokens[i] + val isSep = isSeparator[i] + if (isSep) { + if (!lastWasSeparator) { + cleanTokens.add(token) + lastWasSeparator = true + } else { + val prevSep = cleanTokens.last() + if (prevSep == ":" && token != ":") { + cleanTokens[cleanTokens.lastIndex] = token + } + } + } else { + cleanTokens.add(token) + lastWasSeparator = false + } + } + cleanTokens.joinToString("") + } else { + // + + var lastNoteBlockIdx = -1 + for (idx in tokens.indices.reversed()) { + if (!isSeparator[idx]) { + lastNoteBlockIdx = idx + break + } + } + + if (lastNoteBlockIdx != -1) { + if (lastNoteBlockIdx < tokens.lastIndex && isSeparator[lastNoteBlockIdx + 1]) { + val nextSepIdx = lastNoteBlockIdx + 1 + val sep = tokens[nextSepIdx] + tokens.add(nextSepIdx + 1, "D") + isSeparator.add(nextSepIdx + 1, false) + } else { + tokens.add("!") + isSeparator.add(true) + tokens.add("D") + isSeparator.add(false) + } + } else { + tokens.add("D") + isSeparator.add(false) + } + tokens.joinToString("") + } + + val finalResult = "$prefix$updatedBody$suffix" + + // println("=".repeat(100)) + // println("EDITION FIN GRILLE (9999) : Action = ${editState.templateFragment}") + // println("RESULTAT COMPACTÉ (9999) : $finalResult") + // println("=".repeat(100)) + return finalResult + } + private fun parseNoteAndOctave(s: String): Pair { val base = s.replace(Regex("[^drmfsltDRFSTzw]"), "") var level = 0 @@ -1211,6 +1399,27 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository return templateArray } + + private fun encodeDuration(duration: Int): String { + return when (duration) { + 1 -> "1"; + 2 -> "2"; + 3 -> "3"; + 4 -> "4" + 6 -> "6"; + 8 -> "8"; + 10 -> "A"; + 11 -> "B"; + 12 -> "C"; + 14 -> "E"; + 16 -> "G"; + 20 -> "K"; + 24 -> "O"; + 28 -> "S"; + 32 -> "W" + else -> duration.toString() + } + } private fun reconstructIt(finalResult: String, measure: Int): String { if(finalResult.startsWith("T0:")) { /* T0 */ @@ -1312,27 +1521,6 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository } } - fun encodeDuration(duration: Int): String { - return when (duration) { - 1 -> "1"; - 2 -> "2"; - 3 -> "3"; - 4 -> "4" - 6 -> "6"; - 8 -> "8"; - 10 -> "A"; - 11 -> "B"; - 12 -> "C"; - 14 -> "E"; - 16 -> "G"; - 20 -> "K"; - 24 -> "O"; - 28 -> "S"; - 32 -> "W" - else -> duration.toString() - } - } - var k = 0 while (k < buff.size) { if (consumedBlocks.contains(k)) { diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt index c4f7e25..4230825 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -13,8 +13,10 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Undo +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Save import androidx.compose.material3.* @@ -780,13 +782,14 @@ fun LazyVerticalGridTUO( if (tuo.isTriolet()) { Canvas(modifier = Modifier.fillMaxSize()) { - val arcWidth = with(density) { size.width * 0.75f } + val arcWidth = with(density) { size.width * 0.65f } drawArc( color = FEUFAROO_TRIOLET_COLOR, startAngle = 200f, sweepAngle = 140f, useCenter = false, - topLeft = Offset(0f, yHeight / 2), + //topLeft = Offset(0f, yHeight/* / 2*/), + topLeft = Offset(size.width *0.05f, -yHeight * 0.1f), size = Size(arcWidth, yHeight), style = Stroke(width = 2f) @@ -805,15 +808,24 @@ fun LazyVerticalGridTUO( ) { val xStart = if (lastHairPinSymbol == '>') -size.width * (tuo.numBlock - hairPinStart) else size.width / 2 val xEnd = if (lastHairPinSymbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairPinStart) + val offsetUp = -yHeight * 0.30f + val hairpinHeight = yHeight * 0.70f + + val yTop = 0f + offsetUp + val yCenter = (hairpinHeight / 2) + offsetUp + val yBottom = hairpinHeight + offsetUp + drawLine( Color.DarkGray, - start = Offset(x = xStart, y = 0f), - end = Offset(xEnd, yHeight / 2) + start = Offset(x = xStart, y = yTop), + end = Offset(xEnd, yCenter), + strokeWidth = 0.5f ) drawLine( Color.DarkGray, - start = Offset(xStart, yHeight), - end = Offset(xEnd, yHeight / 2) + start = Offset(xStart, yBottom), + end = Offset(xEnd, yCenter), + strokeWidth = 0.5f ) } TimeUnitObject.endHairPin() @@ -942,6 +954,32 @@ fun LazyVerticalGridTUO( val isActive = (globalIndex == activeRowIndex) val isSelectedByKeyboard = (globalIndex == selectedGridIndex) + val isAbsoluteLastGrid = (globalIndex == tuoList.size - 2) + if (editMode && isAbsoluteLastGrid) { + Box( + modifier = Modifier + .width(gridWidthDp / gridColumnCount) + .fillMaxHeight(), + contentAlignment = Alignment.Center + ) { + GridActionMenu( + onAddMeasureRight = { + val state = TUOEditState( + tuoIndex = 9999, + templateFragment = "+" + ) + sharedScreenModel.openTUOEditor(state) + }, + onDeleteMeasure = { + val state = TUOEditState( + tuoIndex = 9999, + templateFragment = "-" + ) + sharedScreenModel.openTUOEditor(state) + } + ) + } + } else { val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } var menuOffset by remember { mutableStateOf(Offset.Zero) } val interactionSource = remember { MutableInteractionSource() } @@ -1133,6 +1171,7 @@ fun LazyVerticalGridTUO( } } } + } } }