From f6366cfd3f467785f75bf6ba88f6fd3b5ffb7801 Mon Sep 17 00:00:00 2001 From: Hasinjato Date: Tue, 7 Jul 2026 10:47:26 +0300 Subject: [PATCH] Revert "Extract MeasureRow into a compos to fix layout lag on Android" This reverts commit 96eb4dbbf02d3f2967d549a0e38553dc5a7e87b0. --- .../mg/dot/feufaro/solfa/TimeUnitObject.kt | 999 ++++++++---------- 1 file changed, 442 insertions(+), 557 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 15b5817..caf0371 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -271,8 +272,8 @@ fun TimeUnitComposable( val currentDensity = LocalDensity.current val animatedColor by animateColorAsState( - targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f) else col, - animationSpec = tween(durationMillis = 150) // Très court pour rester réactif + targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 1f) else col, + animationSpec = tween(durationMillis = 100) // Très court pour rester réactif ) val focusRequesters = remember { List(4) { FocusRequester() } } @@ -564,10 +565,6 @@ fun LazyVerticalGridTUO( val tuoList = viewModel.tuoList key(tuoList) { - var isCalculatingGrid by remember { mutableStateOf(true) } - var gridColumnCount by remember { mutableStateOf(0) } - var computedMeasures by remember { mutableStateOf>>(emptyList()) } - var menuPosition by remember { mutableStateOf(Offset.Zero) } var showAddDialog by remember { mutableStateOf(false) } var showDetailDialog by remember { mutableStateOf(false) } @@ -592,67 +589,80 @@ fun LazyVerticalGridTUO( val editMode by sharedScreenModel.modeEditor.collectAsState() val showFullChord by sharedScreenModel.harmonyView.collectAsState() var toggleWithDegreeAndChord by remember { mutableStateOf (false) } - val density = LocalDensity.current - val columnGroupString = regexMeasure?.groupValues?.get(1) ?: "1" - val columnGroup = columnGroupString.toInt().coerceAtLeast(1) - val itemMinBaseWidth = bestTUOWidth(tuoList) - val gridWidthDp = with(density) { gridWidthPx.toDp() } - val gridCount by sharedScreenModel.gridCount.collectAsState() - val currentStanza = viewModel.stanza - // Calcul asynchrone nombre de colonnes - LaunchedEffect(gridWidthDp, itemMinBaseWidth, columnGroup, gridCount, tuoList) { - if (gridWidthDp == 0.dp || tuoList.isEmpty()) return@LaunchedEffect + val focusManager = LocalFocusManager.current + Column( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { size -> + onGridWidthMeasured(size.width) + } + .focusRequester(focusRequester) + .focusable() + ) { + LaunchedEffect(editMode) { + if (editMode) { + focusRequester.requestFocus() + } + } + val density = LocalDensity.current + val horizontalArrangementSpacing: Dp = 0.dp + val columnGroupString = regexMeasure?.groupValues?.get(1) ?: "1" + val columnGroup = columnGroupString.toInt().coerceAtLeast(1) + val itemMinBaseWidth = bestTUOWidth(tuoList) + val gridWidthDp = with(density) { gridWidthPx.toDp() } - isCalculatingGrid = true - withContext(Dispatchers.Default) { - var calculatedCols = (gridWidthDp / (itemMinBaseWidth + 0.dp)).toInt() + val gridCount by sharedScreenModel.gridCount.collectAsState() + + var gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) { + if (gridWidthDp == 0.dp) return@remember columnGroup + var calculatedCols = + (gridWidthDp / (itemMinBaseWidth + horizontalArrangementSpacing)).toInt() if (calculatedCols == 0) calculatedCols = columnGroup if (calculatedCols % columnGroup != 0) { calculatedCols = (calculatedCols / columnGroup) * columnGroup if (calculatedCols == 0) calculatedCols = columnGroup } - val finalCols = calculatedCols.coerceAtLeast(columnGroup) + gridCount - val chunkedMeasures = tuoList.drop(1).chunked(finalCols) - - withContext(Dispatchers.Main) { - gridColumnCount = finalCols - computedMeasures = chunkedMeasures - isCalculatingGrid = false - } + val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup) + actualColumnCount } - } + gridColumnCount += gridCount + val flowRowSize: Float = 0.98f / gridColumnCount - val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() - val activeRowIndex by sharedScreenModel.activeIndex.collectAsState() + val currentStanza = viewModel.stanza - val metadataList = remember(tuoList) { - tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO -> - val markerText = oneTUO.pTemplate.markerToString() - val hairPin = oneTUO.hasHairPin() - val finalMarker = when { - hairPin != null && markerText.isBlank() -> hairPin.toString() - hairPin != null && markerText.isNotBlank() -> "${markerText.trim()}$hairPin" - markerText.isNotBlank() -> markerText - else -> null - } - if (finalMarker != null) { - val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } + val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() + val activeRowIndex by sharedScreenModel.activeIndex.collectAsState() + + val measures = tuoList.drop(1).chunked(gridColumnCount) + // Avant column affichage: + val metadataList = remember(tuoList) { + tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO -> + val markerText = oneTUO.pTemplate.markerToString() + val hairPin = oneTUO.hasHairPin() + val finalMarker = when { + hairPin != null && markerText.isBlank() -> hairPin.toString() + hairPin != null && markerText.isNotBlank() -> "${markerText.trim()}$hairPin" + markerText.isNotBlank() -> markerText + else -> null + } + if (finalMarker != null) { + val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } // println("MetaData[$globalIndex] marker='$finalMarker' hairpin=$hairPin") - MidiMarkers( - myTimestamp, - globalIndex, - oneTUO.pTemplate.template, - oneTUO.pTemplate.lastCalledMarker, - finalMarker, - oneTUO.prevTUO?.pTemplate?.template ?: "", - oneTUO.sep0, - oneTUO.tuNotes.getOrNull(1).toString() - ) + MidiMarkers( + myTimestamp, + globalIndex, + oneTUO.pTemplate.template, + oneTUO.pTemplate.lastCalledMarker, + finalMarker, + oneTUO.prevTUO?.pTemplate?.template ?: "", + oneTUO.sep0, + oneTUO.tuNotes.getOrNull(1).toString() + ) - } else null - }.distinctBy { it.gridIndex } - } + } else null + }.distinctBy { it.gridIndex } + } // Envoyer les données au ViewModel une seule fois LaunchedEffect(metadataList) { @@ -661,543 +671,418 @@ fun LazyVerticalGridTUO( sharedScreenModel.updateAndFinalizeMidiData(metadataList) } } - LaunchedEffect(computedMeasures, sharedScreenModel.stanza.value) { + LaunchedEffect(measures, sharedScreenModel.stanza.value) { sharedScreenModel.updateSyllablesFromList( - measures = computedMeasures, + measures = measures, stanzaNumber = sharedScreenModel.stanza.value ) val totalSyllables = sharedScreenModel.synchronizedSyllables.value.size //println("Sync complète effectuée SUR stz: ${sharedScreenModel.stanza.value}! Total colonnes : $totalSyllables") } - Box( - modifier = modifier - .fillMaxWidth() - .onSizeChanged { size-> - onGridWidthMeasured(size.width) - } - .focusRequester(focusRequester) - .focusable(), - contentAlignment = Alignment.Center - ) { - LaunchedEffect(editMode) { - if (editMode) focusRequester.requestFocus() - } + Column( + modifier = Modifier.fillMaxWidth() + ){ + measures.forEachIndexed { measureIndex, measureTUOs -> + key(measureIndex) { + Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { - // Chargement avant affichage - if (isCalculatingGrid || computedMeasures.isEmpty() || gridColumnCount == 0) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .padding(32.dp) - .size(100.dp) - ) { - CircularProgressIndicator( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.primary, - strokeWidth = 2.dp, - trackColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f) - ) - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "𝄞 ♫", - style = TextStyle( - fontSize = 28.sp, - color = MaterialTheme.colorScheme.primary - ) - ) - } - } - } else { - Column( - modifier = Modifier.fillMaxWidth() - ) { - computedMeasures.forEachIndexed { measureIndex, measureTUOs -> - key(measureIndex) { - MeasureRowItem( - measureTUOs, - gridWidthDp, - gridColumnCount, - showFullChord, - sharedScreenModel, - toggleWithDegreeAndChord, - measureIndex, - activeRowIndex, - selectedGridIndex, - editMode, - selectedTUO, - selectedIndex, - showContextualMenu, - currentStanza, - focusRequester, - showEditDialog, - showAddDialog, - showDetailDialog - ) - } - } - } - } - } - } -} - -@Composable -private fun MeasureRowItem( - measureTUOs: List, - gridWidthDp: Dp, - gridColumnCount: Int, - showFullChord: Boolean, - sharedScreenModel: SharedScreenModel, - toggleWithDegreeAndChord: Boolean, - measureIndex: Int, - activeRowIndex: Int, - selectedGridIndex: Int, - editMode: Boolean, - selectedTUO: TimeUnitObject?, - selectedIndex: Int, - showContextualMenu: Boolean, - currentStanza: Int, - focusRequester: FocusRequester, - showEditDialog: Boolean, - showAddDialog: Boolean, - showDetailDialog: Boolean -) { - var toggleWithDegreeAndChord1 = toggleWithDegreeAndChord - var selectedGridIndex1 = selectedGridIndex - var selectedTUO1 = selectedTUO - var selectedIndex1 = selectedIndex - var showContextualMenu1 = showContextualMenu - var showEditDialog1 = showEditDialog - var showAddDialog1 = showAddDialog - var showDetailDialog1 = showDetailDialog - Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.CenterVertically - ) { - measureTUOs.forEachIndexed { indexInMeasure, tuo -> - Column( - modifier = Modifier - .width(gridWidthDp / gridColumnCount) - ) { - 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(Regex("^\\s*(ppp|pp|p|mp|mf|f|ff|fff)\\s*$"))) { - fontWeight = FontWeight.Bold - } - Text( - text = text, - modifier = Modifier - .wrapContentSize( - unbounded = true, - align = Alignment.CenterStart - ), - softWrap = false, - maxLines = 1, - fontStyle = fontStyle, - fontWeight = fontWeight, - style = TextStyle( - color = MaterialTheme.colorScheme.onSecondary, - fontSize = MaterialTheme.typography.titleMedium.fontSize - ) - ) - } - } - } - } - if (showFullChord) { - 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) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically ) { - val maxFontSize = 12.sp - val minFontSize = 10.sp - var currentFontSize by remember { mutableStateOf(maxFontSize) } + measureTUOs.forEachIndexed { indexInMeasure, tuo -> + Column( + modifier = Modifier + .width(gridWidthDp / gridColumnCount) + ) { + 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()} - TextButton( - onClick = { toggleWithDegreeAndChord1 = !toggleWithDegreeAndChord1 }, - contentPadding = PaddingValues(0.dp), - modifier = Modifier.height(24.dp).fillMaxWidth() - ) { - BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { - val textToDisplay = - if (toggleWithDegreeAndChord1) (chordName - ?: "") else (degreeName - ?: "") + 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) - 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 (!toggleWithDegreeAndChord1) 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 + } + 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(Regex("^\\s*(ppp|pp|p|mp|mf|f|ff|fff)\\s*$"))) { + fontWeight = FontWeight.Bold + } + Text(text = text, + modifier = Modifier + .wrapContentSize(unbounded = true, align = Alignment.CenterStart,), + softWrap = false, + maxLines = 1, + fontStyle = fontStyle, + fontWeight = fontWeight, + style = TextStyle( + color = MaterialTheme.colorScheme.onSecondary, + fontSize = MaterialTheme.typography.titleMedium.fontSize + ) ) - ) + } } } } - } - } - } - Row(modifier = Modifier.fillMaxWidth()) { - measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> - val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure - val isActive = (globalIndex == activeRowIndex) - val isSelectedByKeyboard = (globalIndex == selectedGridIndex1) + if(showFullChord) { + 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 myTimestamp = - sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } - var menuOffset by remember { mutableStateOf(Offset.Zero) } - val interactionSource = remember { MutableInteractionSource() } - val isHovered by interactionSource.collectIsHoveredAsState() + 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 + } - key(oneTUO.numBlock) { - Box( - modifier = Modifier - .width(gridWidthDp / gridColumnCount) - .border( - width = if (isSelectedByKeyboard && editMode) 2.dp else 0.dp, - color = if (isSelectedByKeyboard && editMode) MaterialTheme.colorScheme.error else Color.Transparent, - shape = RoundedCornerShape(4.dp) - ) - .background( - if (isHovered) MaterialTheme.colorScheme.onSecondary.copy(alpha = 0.05f) else Color.Transparent, - RoundedCornerShape(4.dp) - ) - .combinedClickable( - interactionSource = interactionSource, - indication = LocalIndication.current, - onClick = { } - ) - .pointerInput(globalIndex) { - detectTapGestures( - onTap = { offset -> - if (editMode) { - selectedGridIndex1 = globalIndex - menuOffset = offset - selectedTUO1 = oneTUO - selectedIndex1 = globalIndex - showContextualMenu1 = true - } else { - sharedScreenModel.seekToGrid(globalIndex) + 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 + ) + ) } } - ) + } } + } + } + Row(modifier = Modifier.fillMaxWidth()) { + measureTUOs.forEachIndexed { indexInMeasure, oneTUO -> + val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure + val isActive = (globalIndex == activeRowIndex) + val isSelectedByKeyboard = (globalIndex == selectedGridIndex) + + val myTimestamp = sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L } + var menuOffset by remember { mutableStateOf(Offset.Zero) } + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + + key(oneTUO.numBlock) { + Box(modifier = Modifier + .width(gridWidthDp / gridColumnCount) + .border( + width = if (isSelectedByKeyboard && editMode) 2.dp else 0.dp, + color = if (isSelectedByKeyboard && editMode) MaterialTheme.colorScheme.error else Color.Transparent, + shape = RoundedCornerShape(4.dp) + ) + .background( + if (isHovered) MaterialTheme.colorScheme.onSecondary.copy(alpha = 0.05f) else Color.Transparent, + RoundedCornerShape(4.dp) + ) + .combinedClickable( + interactionSource = interactionSource, + indication = LocalIndication.current, + onClick = { } + ) + .pointerInput(globalIndex) { + detectTapGestures( + onTap = { offset -> + if(editMode) { + selectedGridIndex = globalIndex + menuOffset = offset + selectedTUO = oneTUO + selectedIndex = globalIndex + showContextualMenu = true + } else { + sharedScreenModel.seekToGrid(globalIndex) + } + } + ) + } + ) { + val currentInterval by sharedScreenModel.transpositionInterval.collectAsState() + TimeUnitComposable( + tuo = oneTUO, + stanzaNumber = currentStanza, + gridColumnCount = gridColumnCount, + gridActive = isActive, + isCurrentBlock = isSelectedByKeyboard && editMode, + parentFocusRequester = focusRequester, + transpositionInterval = currentInterval + ) + + if (showContextualMenu && selectedIndex == globalIndex) { + Popup( + alignment = Alignment.TopStart, + offset = IntOffset( + menuOffset.x.roundToInt(), + menuOffset.y.roundToInt() + ), + onDismissRequest = { showContextualMenu = false } + ) { + ContextualMenu(onMenuItemClick = { item -> + println("Clicked in TUO $globalIndex: $item") + showContextualMenu = false + when(item) { + "Modifier" -> { + showEditDialog = true + } + "Ajouter" -> { + showAddDialog = true + } + "Liste" -> { + showDetailDialog = true + } + else -> { + showDetailDialog = false + } + } + }) + } + } + + val currentSelected = selectedTUO + if ((showEditDialog || showAddDialog || 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 blankPrefixRegex =Regex("""z([0-9A-Z])[:]""") + val match = blankPrefixRegex.find(afterU0T0) + val blankPrefix = match?.groups?.get(1)?.value ?: "" + + 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() + ), + globalIndex = globalIndex, + isEditable = canEdit, + canAdd = canAdd, + onDismiss = { + showAddDialog = false + showDetailDialog = false + showEditDialog = false + }, + onSave = { newState -> + sharedScreenModel.openTUOEditor(newState) + showAddDialog = false + showEditDialog = false + }, + onIndexChanged = { newIndex -> + selectedIndex = newIndex + val globalList = sharedScreenModel.tuoList.value + val nextTUO = globalList.getOrNull(newIndex) + + if (nextTUO != null) { + selectedTUO = nextTUO + }else { + showAddDialog = false + showDetailDialog = false + showEditDialog = false + } + } + ) + } + } + } + } + } + + val allTemps = measureTUOs.map { it.getSingleSyllable(currentStanza) } + val maxLines = allTemps.maxOfOrNull { it.size } ?: 0 + + + val textMeasurer = rememberTextMeasurer() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start ) { - val currentInterval by sharedScreenModel.transpositionInterval.collectAsState() - TimeUnitComposable( - tuo = oneTUO, - stanzaNumber = currentStanza, - gridColumnCount = gridColumnCount, - gridActive = isActive, - isCurrentBlock = isSelectedByKeyboard && editMode, - parentFocusRequester = focusRequester, - transpositionInterval = currentInterval - ) - - if (showContextualMenu1 && selectedIndex1 == globalIndex) { - Popup( - alignment = Alignment.TopStart, - offset = IntOffset( - menuOffset.x.roundToInt(), - menuOffset.y.roundToInt() - ), - onDismissRequest = { showContextualMenu1 = false } + val columnWidthDp = gridWidthDp / gridColumnCount + val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+") + val cleanAllTemps = allTemps.map { column -> + column.map { syllable -> + syllable.replace(REGEX_CLEAN_PREFIX, "").trim() + } + } + allTemps.forEachIndexed { syls_i, syllables -> + /*val fragmentU0 = measureTUOs[syls_i].pTemplate.template + println("Fragment=[$fragmentU0] -> Syllabes=$syllables")*/ + Column( + modifier = Modifier + .width(columnWidthDp) ) { - ContextualMenu(onMenuItemClick = { item -> - println("Clicked in TUO $globalIndex: $item") - showContextualMenu1 = false - when (item) { - "Modifier" -> { - showEditDialog1 = true - } + val noteWidth = columnWidthDp + val density = LocalDensity.current + syllables.joinToString { " " } - "Ajouter" -> { - showAddDialog1 = true - } + val cleanSyllables = cleanAllTemps[syls_i] + cleanSyllables.forEachIndexed { index, syl -> + var textWidth by remember { mutableStateOf(0.dp) } - "Liste" -> { - showDetailDialog1 = true - } - else -> { - showDetailDialog1 = false + val textLayoutResult = textMeasurer.measure( + text = syl, + maxLines = 1, + softWrap = false + ) + val textWidthDp = with(density) { textLayoutResult.size.width.toDp() } + val containerWidthDp = gridWidthDp / gridColumnCount + val isTooLong = textWidthDp > containerWidthDp + + val spacer = makeSpaceBetweenSyllables(textMeasurer, columnWidthDp) + val (dynamicSpaceSyl, alignmentText) = spacer(syl, cleanAllTemps, syls_i, index) + Text( + text = dynamicSpaceSyl, + modifier = Modifier + .fillMaxWidth() + .padding(end = 4.dp) + .wrapContentSize(unbounded = true, align = alignmentText), + softWrap = false, + maxLines = 1, + overflow = TextOverflow.Visible, + style = TextStyle( + color = when { + (isTooLong) -> MaterialTheme.colorScheme.onBackground + (index == 0) -> MaterialTheme.colorScheme.onBackground + else -> MaterialTheme.colorScheme.onBackground + }, + fontSize = MaterialTheme.typography.titleMedium.fontSize, + ), + onTextLayout = { txtLayoutRes -> + textWidth = with(density) { txtLayoutRes.size.width.toDp() } + val textWidthPx = txtLayoutRes.size.width + val noteWidthPx = with(density) { noteWidth.toPx() } } - } - }) + ) + } } } - - val currentSelected = selectedTUO1 - if ((showEditDialog1 || showAddDialog1 || showDetailDialog1) && currentSelected != null && selectedIndex1 == globalIndex) { - val canAdd = showAddDialog1 - val canEdit = showEditDialog1 || 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 blankPrefixRegex = Regex("""z([0-9A-Z])[:]""") - val match = blankPrefixRegex.find(afterU0T0) - val blankPrefix = match?.groups?.get(1)?.value ?: "" - - 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() - ), - globalIndex = globalIndex, - isEditable = canEdit, - canAdd = canAdd, - onDismiss = { - showAddDialog1 = false - showDetailDialog1 = false - showEditDialog1 = false - }, - onSave = { newState -> - sharedScreenModel.openTUOEditor(newState) - showAddDialog1 = false - showEditDialog1 = false - }, - onIndexChanged = { newIndex -> - selectedIndex1 = newIndex - val globalList = sharedScreenModel.tuoList.value - val nextTUO = globalList.getOrNull(newIndex) - - if (nextTUO != null) { - selectedTUO1 = nextTUO - } else { - showAddDialog1 = false - showDetailDialog1 = false - showEditDialog1 = false - } - } - ) - } } } } - } - - val allTemps = measureTUOs.map { it.getSingleSyllable(currentStanza) } - val maxLines = allTemps.maxOfOrNull { it.size } ?: 0 - - - val textMeasurer = rememberTextMeasurer() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Start - ) { - val columnWidthDp = gridWidthDp / gridColumnCount - val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+") - val cleanAllTemps = allTemps.map { column -> - column.map { syllable -> - syllable.replace(REGEX_CLEAN_PREFIX, "").trim() - } - } - allTemps.forEachIndexed { syls_i, syllables -> - /*val fragmentU0 = measureTUOs[syls_i].pTemplate.template - println("Fragment=[$fragmentU0] -> Syllabes=$syllables")*/ - Column( - modifier = Modifier - .width(columnWidthDp) - ) { - val noteWidth = columnWidthDp - val density = LocalDensity.current - syllables.joinToString { " " } - - val cleanSyllables = cleanAllTemps[syls_i] - cleanSyllables.forEachIndexed { index, syl -> - var textWidth by remember { mutableStateOf(0.dp) } - - - val textLayoutResult = textMeasurer.measure( - text = syl, - maxLines = 1, - softWrap = false - ) - val textWidthDp = with(density) { textLayoutResult.size.width.toDp() } - val containerWidthDp = gridWidthDp / gridColumnCount - val isTooLong = textWidthDp > containerWidthDp - - val spacer = makeSpaceBetweenSyllables(textMeasurer, columnWidthDp) - val (dynamicSpaceSyl, alignmentText) = spacer( - syl, - cleanAllTemps, - syls_i, - index - ) - Text( - text = dynamicSpaceSyl, - modifier = Modifier - .fillMaxWidth() - .padding(end = 4.dp) - .wrapContentSize(unbounded = true, align = alignmentText), - softWrap = false, - maxLines = 1, - overflow = TextOverflow.Visible, - style = TextStyle( - color = when { - (isTooLong) -> MaterialTheme.colorScheme.onBackground - (index == 0) -> MaterialTheme.colorScheme.onBackground - else -> MaterialTheme.colorScheme.onBackground - }, - fontSize = MaterialTheme.typography.titleMedium.fontSize, - ), - onTextLayout = { txtLayoutRes -> - textWidth = with(density) { txtLayoutRes.size.width.toDp() } - val textWidthPx = txtLayoutRes.size.width - val noteWidthPx = with(density) { noteWidth.toPx() } - } - ) - } - } } } } + } } @Composable