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 caf0371..15b5817 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -33,7 +33,6 @@ 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 @@ -272,8 +271,8 @@ fun TimeUnitComposable( val currentDensity = LocalDensity.current val animatedColor by animateColorAsState( - targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 1f) else col, - animationSpec = tween(durationMillis = 100) // Très court pour rester réactif + targetValue = if (gridActive) MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f) else col, + animationSpec = tween(durationMillis = 150) // Très court pour rester réactif ) val focusRequesters = remember { List(4) { FocusRequester() } } @@ -565,6 +564,10 @@ 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) } @@ -589,80 +592,67 @@ 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 - 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() } + // Calcul asynchrone nombre de colonnes + LaunchedEffect(gridWidthDp, itemMinBaseWidth, columnGroup, gridCount, tuoList) { + if (gridWidthDp == 0.dp || tuoList.isEmpty()) return@LaunchedEffect - 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() + isCalculatingGrid = true + withContext(Dispatchers.Default) { + var calculatedCols = (gridWidthDp / (itemMinBaseWidth + 0.dp)).toInt() if (calculatedCols == 0) calculatedCols = columnGroup if (calculatedCols % columnGroup != 0) { calculatedCols = (calculatedCols / columnGroup) * columnGroup if (calculatedCols == 0) calculatedCols = columnGroup } - val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup) - actualColumnCount + val finalCols = calculatedCols.coerceAtLeast(columnGroup) + gridCount + val chunkedMeasures = tuoList.drop(1).chunked(finalCols) + + withContext(Dispatchers.Main) { + gridColumnCount = finalCols + computedMeasures = chunkedMeasures + isCalculatingGrid = false + } } - gridColumnCount += gridCount - val flowRowSize: Float = 0.98f / gridColumnCount + } - val currentStanza = viewModel.stanza + val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState() + val activeRowIndex by sharedScreenModel.activeIndex.collectAsState() - 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 } + 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) { @@ -671,417 +661,542 @@ fun LazyVerticalGridTUO( sharedScreenModel.updateAndFinalizeMidiData(metadataList) } } - LaunchedEffect(measures, sharedScreenModel.stanza.value) { + LaunchedEffect(computedMeasures, sharedScreenModel.stanza.value) { sharedScreenModel.updateSyllablesFromList( - measures = measures, + measures = computedMeasures, stanzaNumber = sharedScreenModel.stanza.value ) val totalSyllables = sharedScreenModel.synchronizedSyllables.value.size //println("Sync complète effectuée SUR stz: ${sharedScreenModel.stanza.value}! Total colonnes : $totalSyllables") } - Column( - modifier = Modifier.fillMaxWidth() - ){ - measures.forEachIndexed { measureIndex, measureTUOs -> - key(measureIndex) { - Column(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) { + Box( + modifier = modifier + .fillMaxWidth() + .onSizeChanged { size-> + onGridWidthMeasured(size.width) + } + .focusRequester(focusRequester) + .focusable(), + contentAlignment = Alignment.Center + ) { + LaunchedEffect(editMode) { + if (editMode) focusRequester.requestFocus() + } - 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()} + // 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) + ) - 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) + 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 + ) + } + } + } + } + } + } +} - ) - } - } - 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() -// } +@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 (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 (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 + } + } + } + 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) + ) { + val maxFontSize = 12.sp + val minFontSize = 10.sp + var currentFontSize by remember { mutableStateOf(maxFontSize) } + + TextButton( + onClick = { toggleWithDegreeAndChord1 = !toggleWithDegreeAndChord1 }, + contentPadding = PaddingValues(0.dp), + modifier = Modifier.height(24.dp).fillMaxWidth() ) { - 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() + BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { + val textToDisplay = + if (toggleWithDegreeAndChord1) (chordName + ?: "") else (degreeName + ?: "") - 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) - ) { - 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 - ) - ) + Text( + text = textToDisplay, + onTextLayout = { textLayoutResult -> + if (textLayoutResult.hasVisualOverflow && currentFontSize > minFontSize) { + currentFontSize = (currentFontSize.value * 0.9f).sp } - } - } - } - } - } - 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 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, + }, + 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 ), - onTextLayout = { txtLayoutRes -> - textWidth = with(density) { txtLayoutRes.size.width.toDp() } - val textWidthPx = txtLayoutRes.size.width - val noteWidthPx = with(density) { noteWidth.toPx() } - } + 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 == selectedGridIndex1) + + 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) { + selectedGridIndex1 = globalIndex + menuOffset = offset + selectedTUO1 = oneTUO + selectedIndex1 = globalIndex + showContextualMenu1 = 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 (showContextualMenu1 && selectedIndex1 == globalIndex) { + Popup( + alignment = Alignment.TopStart, + offset = IntOffset( + menuOffset.x.roundToInt(), + menuOffset.y.roundToInt() + ), + onDismissRequest = { showContextualMenu1 = false } + ) { + ContextualMenu(onMenuItemClick = { item -> + println("Clicked in TUO $globalIndex: $item") + showContextualMenu1 = false + when (item) { + "Modifier" -> { + showEditDialog1 = true + } + + "Ajouter" -> { + showAddDialog1 = true + } + + "Liste" -> { + showDetailDialog1 = true + } + + else -> { + showDetailDialog1 = false + } + } + }) + } + } + + 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() } + } + ) + } + } } } - } } }