From 1d7273a5316cb90960417b7f3ad61e3dd68514be Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Thu, 10 Sep 2026 14:09:07 +0300 Subject: [PATCH] Resolve crash in TUO by refactoring composables - Android --- .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 561 +++++++++--------- 1 file changed, 279 insertions(+), 282 deletions(-) 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 9699886..8a61996 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -320,84 +320,6 @@ fun TimeUnitComposable( modifier = Modifier .background(animatedColor) ) { - if (TimeUnitObject._hasMarker) { - val lineHeight = 20.sp - val density = LocalDensity.current - val lineHeightDp : Dp = with(density) { - lineHeight.toDp() - } - var fontStyle = FontStyle.Normal - var fontWeight = FontWeight.Normal - val hairPinSymbol = tuo.hasHairPin() - val yHeight = with(density) { lineHeightDp.toPx()} - - /*if (tuo.isTriolet()) { - Canvas(modifier = Modifier.fillMaxSize()) { - val arcWidth = with(density) { size.width * 0.75f} - drawArc( - color = FEUFAROO_TRIOLET_COLOR, - startAngle = 200f, - sweepAngle = 140f, - useCenter = false, - topLeft = Offset(0f, yHeight / 2), - size = Size(arcWidth, yHeight), - style = Stroke(width = 2f) - - ) - } - } - if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) { - println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}") - val hairPinStart = TimeUnitObject.lastHairPinStart - val lastHairPinSymbol = TimeUnitObject.lastHairPinSymbol - val hairPinStartLine: Int = (hairPinStart - 1) / gridColumnCount - val hairPinEndLine: Int = (tuo.numBlock - 1) / gridColumnCount - if (hairPinStartLine == hairPinEndLine) { - Canvas( - modifier = Modifier.fillMaxSize() - ) { - 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) - drawLine( - Color.DarkGray, - start = Offset(x=xStart, y=0f), - end = Offset(xEnd, yHeight/2) - ) - drawLine( - Color.DarkGray, - start = Offset(xStart, yHeight), - end = Offset(xEnd, yHeight/2) - ) - } - TimeUnitObject.endHairPin() - } - - } - if (hairPinSymbol != null && hairPinSymbol != '=') { - TimeUnitObject.startHairPin(hairPinSymbol, tuo.numBlock) - } - // @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne - // problème si template = $QD:,-$QD - tuo.pTemplate.resetCalledMarker() - val text = tuo.pTemplate.markerToString() - if (text.contains(Regex("^([mfp]+|[rR]it.*)$"))) { - fontStyle = FontStyle.Italic - } - if (text.contains("p")) { - fontWeight = FontWeight.Bold - }*/ - /*AutoResizingText( - text = text, - minFontSize = 18.sp, - maxFontSize = 18.sp, - maxLines = 2, - fontStyle = fontStyle, - fontWeight = fontWeight, - modifier = Modifier.fillMaxWidth() - .height(lineHeightDp))*/ - - - } Row (modifier = Modifier.height(IntrinsicSize.Min)){ if (tuo.sep0 == "/" || tuo.sep0 == "|") { Box( @@ -850,104 +772,18 @@ fun LazyVerticalGridTUO( } var fontStyle = FontStyle.Normal var fontWeight = FontWeight.Normal - val hairPinSymbol = tuo.hasHairPin() val yHeight = with(density) { lineHeightDp.toPx() } - if (tuo.isTriolet()) { - Canvas(modifier = Modifier.fillMaxSize()) { - val arcWidth = size.width * 0.65f - drawArc( - color = colorUnderline, - startAngle = 200f, - sweepAngle = 140f, - useCenter = false, - //topLeft = Offset(0f, yHeight/* / 2*/), - topLeft = Offset(size.width *0.05f, -yHeight * 0.1f), - size = Size(arcWidth, yHeight), - style = Stroke(width = 2f) - - ) - } - } - - val hairpinRange = hairpinByEndBlock[tuo.numBlock] - if (hairpinRange != null) { - Canvas( - modifier = Modifier.fillMaxSize() - ) { - val xStart = if (hairpinRange.symbol == '>') -size.width * (tuo.numBlock - hairpinRange.startBlock) else size.width / 2 - val xEnd = if (hairpinRange.symbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairpinRange.startBlock) - 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 = colorUnderline, - start = Offset(x = xStart, y = yTop), - end = Offset(xEnd, yCenter), - strokeWidth = 0.5f - ) - drawLine( - color = colorUnderline, - start = Offset(xStart, yBottom), - end = Offset(xEnd, yCenter), - strokeWidth = 0.5f - ) - } - } - - // @todo pTemplate.markerToString retourne les marqueurs comme une seule chaîne - // problème si template = $QD:,-$QD - tuo.pTemplate.resetCalledMarker() - - val markerList = tuo.pTemplate.markerToList() - Row( - modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart), - verticalAlignment = Alignment.CenterVertically - ) { - markerList.forEach { marker -> - Column { - var markerFontFamily: FontFamily = FontFamily.Default - var markerFontSize: Float = MaterialTheme.typography.titleMedium.fontSize.value - - when { - musicForPtSerifBIRegex.matches(marker) -> { - markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value - markerFontFamily = ptSerifBoldItalic - } - - musicForEmmentRegex.matches(marker) -> { - markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + 11 - markerFontFamily = emmentaler - } - - else -> { - markerFontFamily = ptSerif - } - } - Text( - text = marker, - modifier = Modifier - .wrapContentSize(unbounded = true, align = Alignment.CenterStart,), - softWrap = false, - maxLines = 1, - fontStyle = fontStyle, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.titleMedium.copy( - color = MaterialTheme.colorScheme.onSecondary, - fontSize = markerFontSize.sp, - fontFamily = markerFontFamily - ) - ) - if (marker != markerList.last()) { - Spacer(modifier = Modifier.width(2.dp)) - } - } - } - } + TuoHeaderDecorations( + tuo, + colorUnderline, + yHeight, + hairpinByEndBlock, + ptSerifBoldItalic, + emmentaler, + ptSerif, + fontStyle + ) } } } @@ -959,55 +795,17 @@ fun LazyVerticalGridTUO( 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 appliedInterval by sharedScreenModel.transpositionInterval.collectAsState() - val songKey = sharedScreenModel.songKey.value - val chordName = remember(chordMap, songKey, appliedInterval) { - if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, songKey, appliedInterval) else null - } - - Column( - modifier = Modifier - .width(gridWidthDp / gridColumnCount) - ) { - val maxFontSize = 12.sp - val minFontSize = 10.sp - var currentFontSize by remember { mutableStateOf(maxFontSize) } - - TextButton( - onClick = { toggleWithDegreeAndChord = !toggleWithDegreeAndChord }, - contentPadding = PaddingValues(0.dp), - modifier = Modifier.height(24.dp).fillMaxWidth() - ) { - BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { - val textToDisplay = if (toggleWithDegreeAndChord) (chordName ?: "") else (degreeName ?: "") - - Text( - text = textToDisplay, - onTextLayout = { textLayoutResult -> - if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) { - currentFontSize = (currentFontSize.value * 0.9f).sp - } - }, - softWrap = false, - maxLines = 1, - overflow = TextOverflow.Visible, - style = TextStyle( - color = if (!toggleWithDegreeAndChord) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary.copy(red = MaterialTheme.colorScheme.secondary.red * 0.4f, green = MaterialTheme.colorScheme.secondary.green * 0.4f, blue = MaterialTheme.colorScheme.secondary.blue * 0.4f ), - fontWeight = FontWeight.Bold, - fontSize = currentFontSize - ) - ) - } - } - } + TuoChordRowItem( + tuo, + sharedScreenModel, + gridWidthDp, + gridColumnCount, + toggleWithDegreeAndChord, + onToggleChange = { + toggleWithDegreeAndChord = it + }, + modifier = Modifier.height(24.dp).fillMaxWidth() + ) } } } @@ -1192,73 +990,21 @@ fun LazyVerticalGridTUO( val currentSelected = selectedTUO if ((showEditDialog || showDetailDialog) && currentSelected != null && selectedIndex == globalIndex) { - val canAdd = showAddDialog - val canEdit = showEditDialog || canAdd - - val template = oneTUO.pTemplate.template - val expectedTemplate = template.count { it.isLetter() } == 2 && template.contains(".") - val fileC = sharedScreenModel.fileContent.value ?: "" - val lines = fileC.split("\n").toMutableList() - val u0Idx = lines.indexOfFirst { it.startsWith("U0:") } - val t0Idx = lines.indexOfFirst { it.startsWith("T0:") } - - val fullLine = when { - t0Idx != -1 -> { - lines[t0Idx] - } - u0Idx != -1 -> { - lines[u0Idx] - } - else -> "" - } - val afterU0T0 = fullLine.substringAfter("0:") - val match = blankPrefixRegex.find(afterU0T0) - val blankPrefix = match?.groups?.get(1)?.value ?: "" - - val anchorsByVoice = (0..3).associate { voiceIdx -> - val linePrefix = "N${voiceIdx + 1}:" - val fullLine = lines.firstOrNull { it.startsWith(linePrefix) } ?: "" - voiceIdx to extractAnchorFromNoteLine(fullLine) - } - val editState = TUOEditState( - tuoIndex = oneTUO.firstTuoIndex, - notesByVoice = (0..3).associate { i -> - val rawNote = oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: "" - val fixedNote = autoFixNote(rawNote, template) - i to fixedNote - }, - lyricsByStanza = (1..currentStanza).associate { s -> - s to oneTUO.lyricsAsMultiString(currentStanza) - }.toMutableMap(), - templateFragment = oneTUO.pTemplate.template, - marker = listOfNotNull( - oneTUO.pTemplate.markerToString().takeIf { it.isNotBlank() }, - oneTUO.hasHairPin()?.toString()?.takeIf { it.isNotBlank() } - ).joinToString(" "), - silentDuration = blankPrefix, - sep = oneTUO.sep0 - ) - TUODetailDialog( - editState = editState, - tuo = currentSelected, - currentStanza = currentStanza, - menuPosition = IntOffset( - menuOffset.x.roundToInt(), - menuOffset.y.roundToInt() - ), + val lines = fileC.split("\n") + TuoEditDialogWrapper( + oneTUO = currentSelected, globalIndex = globalIndex, - isEditable = canEdit, - canAdd = canAdd, + currentStanza = currentStanza, + menuOffset = menuOffset, sharedScreenModel = sharedScreenModel, - anchorsByVoice = anchorsByVoice, + lines = lines, onDismiss = { showAddDialog = false showDetailDialog = false showEditDialog = false }, - onSave = { newState -> - sharedScreenModel.openTUOEditor(newState) + onSaveSuccess = { showAddDialog = false showEditDialog = false }, @@ -1378,6 +1124,257 @@ fun LazyVerticalGridTUO( } } +@Composable +private fun TuoChordRowItem( + tuo: TimeUnitObject, + sharedScreenModel: SharedScreenModel, + gridWidthDp: Dp, + gridColumnCount: Int, + toggleWithDegreeAndChord: Boolean, + onToggleChange: (Boolean) -> Unit, + modifier: Modifier +) { + 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 appliedInterval by sharedScreenModel.transpositionInterval.collectAsState() + val songKey = sharedScreenModel.songKey.value + val chordName = remember(chordMap, songKey, appliedInterval) { + if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, songKey, appliedInterval) else null + } + + Column( + modifier = Modifier + .width(gridWidthDp / gridColumnCount) + ) { + val maxFontSize = 12.sp + val minFontSize = 10.sp + var currentFontSize by remember { mutableStateOf(maxFontSize) } + + TextButton( + onClick = { onToggleChange(!toggleWithDegreeAndChord) }, + contentPadding = PaddingValues(0.dp), + modifier = modifier + ) { + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val textToDisplay = if (toggleWithDegreeAndChord) (chordName ?: "") else (degreeName ?: "") + + Text( + text = textToDisplay, + onTextLayout = { textLayoutResult -> + if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) { + currentFontSize = (currentFontSize.value * 0.9f).sp + } + }, + softWrap = false, + maxLines = 1, + overflow = TextOverflow.Visible, + style = TextStyle( + color = if (!toggleWithDegreeAndChord) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary.copy( + red = MaterialTheme.colorScheme.secondary.red * 0.4f, + green = MaterialTheme.colorScheme.secondary.green * 0.4f, + blue = MaterialTheme.colorScheme.secondary.blue * 0.4f + ), + fontWeight = FontWeight.Bold, + fontSize = currentFontSize + ) + ) + } + } + } +} + +@Composable +private fun TuoHeaderDecorations( + tuo: TimeUnitObject, + colorUnderline: Color, + yHeight: Float, + hairpinByEndBlock: Map, + ptSerifBoldItalic: FontFamily, + emmentaler: FontFamily, + ptSerif: FontFamily, + fontStyle: FontStyle +) { + if (tuo.isTriolet()) { + Canvas(modifier = Modifier.fillMaxSize()) { + val arcWidth = size.width * 0.65f + drawArc( + color = colorUnderline, + startAngle = 200f, + sweepAngle = 140f, + useCenter = false, + //topLeft = Offset(0f, yHeight/* / 2*/), + topLeft = Offset(size.width * 0.05f, -yHeight * 0.1f), + size = Size(arcWidth, yHeight), + style = Stroke(width = 2f) + + ) + } + } + + val hairpinRange = hairpinByEndBlock[tuo.numBlock] + if (hairpinRange != null) { + Canvas( + modifier = Modifier.fillMaxSize() + ) { + val xStart = + if (hairpinRange.symbol == '>') -size.width * (tuo.numBlock - hairpinRange.startBlock) else size.width / 2 + val xEnd = + if (hairpinRange.symbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairpinRange.startBlock) + 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 = colorUnderline, + start = Offset(x = xStart, y = yTop), + end = Offset(xEnd, yCenter), + strokeWidth = 0.5f + ) + drawLine( + color = colorUnderline, + start = Offset(xStart, yBottom), + end = Offset(xEnd, yCenter), + strokeWidth = 0.5f + ) + } + } + + // problème si template = $QD:,-$QD + tuo.pTemplate.resetCalledMarker() + + val markerList = tuo.pTemplate.markerToList() + Row( + modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart), + verticalAlignment = Alignment.CenterVertically + ) { + markerList.forEach { marker -> + Column { + var markerFontFamily: FontFamily = FontFamily.Default + var markerFontSize: Float = MaterialTheme.typography.titleMedium.fontSize.value + + when { + musicForPtSerifBIRegex.matches(marker) -> { + markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + markerFontFamily = ptSerifBoldItalic + } + + musicForEmmentRegex.matches(marker) -> { + markerFontSize = MaterialTheme.typography.titleMedium.fontSize.value + 11 + markerFontFamily = emmentaler + } + + else -> { + markerFontFamily = ptSerif + } + } + Text( + text = marker, + modifier = Modifier + .wrapContentSize(unbounded = true, align = Alignment.CenterStart), + softWrap = false, + maxLines = 1, + fontStyle = fontStyle, + fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium.copy( + color = MaterialTheme.colorScheme.onSecondary, + fontSize = markerFontSize.sp, + fontFamily = markerFontFamily + ) + ) + if (marker != markerList.last()) { + Spacer(modifier = Modifier.width(2.dp)) + } + } + } + } +} + +@Composable +private fun TuoEditDialogWrapper( + oneTUO: TimeUnitObject, + globalIndex: Int, + currentStanza: Int, + menuOffset: Offset, + sharedScreenModel: SharedScreenModel, + lines: List, + onDismiss: () -> Unit, + onSaveSuccess: () -> Unit, + onIndexChanged: (Int) -> Unit +) { + val template = oneTUO.pTemplate.template + + val u0Idx = lines.indexOfFirst { it.startsWith("U0:") } + val t0Idx = lines.indexOfFirst { it.startsWith("T0:") } + + val fullLine = when { + t0Idx != -1 -> { + lines[t0Idx] + } + + u0Idx != -1 -> { + lines[u0Idx] + } + + else -> "" + } + val afterU0T0 = fullLine.substringAfter("0:") + val match = blankPrefixRegex.find(afterU0T0) + val blankPrefix = match?.groups?.get(1)?.value ?: "" + + val anchorsByVoice = (0..3).associate { voiceIdx -> + val linePrefix = "N${voiceIdx + 1}:" + val fullLine = lines.firstOrNull { it.startsWith(linePrefix) } ?: "" + voiceIdx to extractAnchorFromNoteLine(fullLine) + } + val editState = TUOEditState( + tuoIndex = oneTUO.firstTuoIndex, + notesByVoice = (0..3).associate { i -> + val rawNote = oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: "" + val fixedNote = autoFixNote(rawNote, template) + i to fixedNote + }, + lyricsByStanza = (1..currentStanza).associate { s -> + s to oneTUO.lyricsAsMultiString(currentStanza) + }.toMutableMap(), + templateFragment = oneTUO.pTemplate.template, + marker = listOfNotNull( + oneTUO.pTemplate.markerToString().takeIf { it.isNotBlank() }, + oneTUO.hasHairPin()?.toString()?.takeIf { it.isNotBlank() } + ).joinToString(" "), + silentDuration = blankPrefix, + sep = oneTUO.sep0 + ) + TUODetailDialog( + editState = editState, + tuo = oneTUO, + currentStanza = currentStanza, + menuPosition = IntOffset( + menuOffset.x.roundToInt(), + menuOffset.y.roundToInt() + ), + globalIndex = globalIndex, + isEditable = true, + canAdd = false, + sharedScreenModel = sharedScreenModel, + anchorsByVoice = anchorsByVoice, + onDismiss = onDismiss, + onSave = { newState -> + sharedScreenModel.openTUOEditor(newState) + onSaveSuccess() + }, + onIndexChanged = onIndexChanged + ) +} + @Composable fun makeSpaceBetweenSyllables( textMeasurer: TextMeasurer,