Compare commits

...

2 commits

View file

@ -28,11 +28,13 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor
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 +273,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() } }
@ -475,13 +477,17 @@ fun TimeUnitComposable(
}
}
data class TUOWidthMeasure(val width: Dp, val isReady: Boolean)
@Composable
fun bestTUOWidth(items: List<TimeUnitObject>): Dp {
fun bestTUOWidth(items: List<TimeUnitObject>): TUOWidthMeasure {
val textMeasurer = rememberTextMeasurer()
val density = LocalDensity.current
var maxWidth by remember { mutableStateOf(0.dp) }
var maxWidth by remember(items) { mutableStateOf(0.dp) }
var isReady by remember(items) { mutableStateOf(false) }
LaunchedEffect(items) {
isReady = false
maxWidth = 0.dp
items.forEach {
val textLayoutResult: TextLayoutResult = textMeasurer.measure(
@ -495,8 +501,9 @@ fun bestTUOWidth(items: List<TimeUnitObject>): Dp {
maxWidth = textWidth
}
}
isReady = true
}
return maxWidth + 13.dp
return TUOWidthMeasure(maxWidth + 13.dp, isReady)
}
@Composable
fun AutoResizingText(
@ -564,10 +571,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<List<List<TimeUnitObject>>>(emptyList()) }
var menuPosition by remember { mutableStateOf(Offset.Zero) }
var showAddDialog by remember { mutableStateOf(false) }
var showDetailDialog by remember { mutableStateOf(false) }
@ -592,40 +595,57 @@ fun LazyVerticalGridTUO(
val editMode by sharedScreenModel.modeEditor.collectAsState()
val showFullChord by sharedScreenModel.harmonyView.collectAsState()
var toggleWithDegreeAndChord by remember { mutableStateOf (false) }
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 tuoWidthMeasure = bestTUOWidth(tuoList)
val itemMinBaseWidth = tuoWidthMeasure.width
val gridWidthDp = with(density) { gridWidthPx.toDp() }
// on affiche un loader pour éviter le "saut" visuel
val isLayoutReady = tuoWidthMeasure.isReady && gridWidthDp > 0.dp
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
isCalculatingGrid = true
withContext(Dispatchers.Default) {
var calculatedCols = (gridWidthDp / (itemMinBaseWidth + 0.dp)).toInt()
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)
val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup)
actualColumnCount
}
gridColumnCount += gridCount
val flowRowSize: Float = 0.98f / gridColumnCount
withContext(Dispatchers.Main) {
gridColumnCount = finalCols
computedMeasures = chunkedMeasures
isCalculatingGrid = false
}
}
}
val currentStanza = viewModel.stanza
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()
@ -661,48 +681,26 @@ 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")
}
if (!isLayoutReady) {
Box(
modifier = modifier
modifier = Modifier
.fillMaxWidth()
.onSizeChanged { size->
onGridWidthMeasured(size.width)
}
.focusRequester(focusRequester)
.focusable(),
.height(200.dp),
contentAlignment = Alignment.Center
) {
LaunchedEffect(editMode) {
if (editMode) focusRequester.requestFocus()
}
// 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)
modifier = Modifier.size(64.dp),
strokeWidth = 3.dp
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "𝄞 ♫",
style = TextStyle(
@ -711,71 +709,14 @@ fun LazyVerticalGridTUO(
)
)
}
}
} else {
Column(
modifier = Modifier.fillMaxWidth()
) {
computedMeasures.forEachIndexed { measureIndex, measureTUOs ->
){
measures.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<TimeUnitObject>,
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,
@ -789,17 +730,17 @@ private fun MeasureRowItem(
if (TimeUnitObject._hasMarker) {
val lineHeight = 20.sp
val density = LocalDensity.current
val lineHeightDp: Dp = with(density) {
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() }
val yHeight = with(density) { lineHeightDp.toPx()}
if (tuo.isTriolet()) {
Canvas(modifier = Modifier.fillMaxSize()) {
val arcWidth = with(density) { size.width * 0.75f }
val arcWidth = with(density) { size.width * 0.75f}
drawArc(
color = FEUFAROO_TRIOLET_COLOR,
startAngle = 200f,
@ -813,32 +754,30 @@ private fun MeasureRowItem(
}
}
if ((hairPinSymbol == '=') && (TimeUnitObject.lastHairPinSymbol != null)) {
// println("LastHairpin: ${TimeUnitObject.lastHairPinSymbol} ${TimeUnitObject.lastHairPinStart}")
// 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) {
// 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)
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)
start = Offset(x=xStart, y=0f),
end = Offset(xEnd, yHeight/2)
)
drawLine(
Color.DarkGray,
start = Offset(xStart, yHeight),
end = Offset(xEnd, yHeight / 2)
end = Offset(xEnd, yHeight/2)
)
}
TimeUnitObject.endHairPin()
// }
// }
}
if (hairPinSymbol != null && hairPinSymbol != '=') {
@ -854,13 +793,9 @@ private fun MeasureRowItem(
if (text.contains(Regex("^\\s*(ppp|pp|p|mp|mf|f|ff|fff)\\s*$"))) {
fontWeight = FontWeight.Bold
}
Text(
text = text,
Text(text = text,
modifier = Modifier
.wrapContentSize(
unbounded = true,
align = Alignment.CenterStart
),
.wrapContentSize(unbounded = true, align = Alignment.CenterStart,),
softWrap = false,
maxLines = 1,
fontStyle = fontStyle,
@ -874,7 +809,7 @@ private fun MeasureRowItem(
}
}
}
if (showFullChord) {
if(showFullChord) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
@ -890,10 +825,7 @@ private fun MeasureRowItem(
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeDegree(chordMap) else null
}
val chordName = remember(chordMap) {
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(
chordMap,
sharedScreenModel.songKey.value
) else null
if (chordMap.isNotEmpty()) HarmonicAnalyzer.analyzeChordName(chordMap, sharedScreenModel.songKey.value) else null
}
Column(
@ -905,15 +837,12 @@ private fun MeasureRowItem(
var currentFontSize by remember { mutableStateOf(maxFontSize) }
TextButton(
onClick = { toggleWithDegreeAndChord1 = !toggleWithDegreeAndChord1 },
onClick = { toggleWithDegreeAndChord = !toggleWithDegreeAndChord },
contentPadding = PaddingValues(0.dp),
modifier = Modifier.height(24.dp).fillMaxWidth()
) {
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
val textToDisplay =
if (toggleWithDegreeAndChord1) (chordName
?: "") else (degreeName
?: "")
val textToDisplay = if (toggleWithDegreeAndChord) (chordName ?: "") else (degreeName ?: "")
Text(
text = textToDisplay,
@ -926,11 +855,7 @@ private fun MeasureRowItem(
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
),
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
)
@ -945,17 +870,15 @@ private fun MeasureRowItem(
measureTUOs.forEachIndexed { indexInMeasure, oneTUO ->
val globalIndex = (measureIndex * gridColumnCount) + indexInMeasure
val isActive = (globalIndex == activeRowIndex)
val isSelectedByKeyboard = (globalIndex == selectedGridIndex1)
val isSelectedByKeyboard = (globalIndex == selectedGridIndex)
val myTimestamp =
sharedScreenModel.tuoTimestamps.value.getOrElse(globalIndex) { 0L }
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
Box(modifier = Modifier
.width(gridWidthDp / gridColumnCount)
.border(
width = if (isSelectedByKeyboard && editMode) 2.dp else 0.dp,
@ -974,12 +897,12 @@ private fun MeasureRowItem(
.pointerInput(globalIndex) {
detectTapGestures(
onTap = { offset ->
if (editMode) {
selectedGridIndex1 = globalIndex
if(editMode) {
selectedGridIndex = globalIndex
menuOffset = offset
selectedTUO1 = oneTUO
selectedIndex1 = globalIndex
showContextualMenu1 = true
selectedTUO = oneTUO
selectedIndex = globalIndex
showContextualMenu = true
} else {
sharedScreenModel.seekToGrid(globalIndex)
}
@ -998,47 +921,43 @@ private fun MeasureRowItem(
transpositionInterval = currentInterval
)
if (showContextualMenu1 && selectedIndex1 == globalIndex) {
if (showContextualMenu && selectedIndex == globalIndex) {
Popup(
alignment = Alignment.TopStart,
offset = IntOffset(
menuOffset.x.roundToInt(),
menuOffset.y.roundToInt()
),
onDismissRequest = { showContextualMenu1 = false }
onDismissRequest = { showContextualMenu = false }
) {
ContextualMenu(onMenuItemClick = { item ->
println("Clicked in TUO $globalIndex: $item")
showContextualMenu1 = false
when (item) {
showContextualMenu = false
when(item) {
"Modifier" -> {
showEditDialog1 = true
showEditDialog = true
}
"Ajouter" -> {
showAddDialog1 = true
showAddDialog = true
}
"Liste" -> {
showDetailDialog1 = true
showDetailDialog = true
}
else -> {
showDetailDialog1 = false
showDetailDialog = false
}
}
})
}
}
val currentSelected = selectedTUO1
if ((showEditDialog1 || showAddDialog1 || showDetailDialog1) && currentSelected != null && selectedIndex1 == globalIndex) {
val canAdd = showAddDialog1
val canEdit = showEditDialog1 || canAdd
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 expectedTemplate = template.count { it.isLetter() } == 2 && template.contains(".")
val fileC = sharedScreenModel.fileContent.value ?: ""
val lines = fileC.split("\n").toMutableList()
@ -1049,23 +968,20 @@ private fun MeasureRowItem(
t0Idx != -1 -> {
lines[t0Idx]
}
u0Idx != -1 -> {
lines[u0Idx]
}
else -> ""
}
val afterU0T0 = fullLine.substringAfter("0:")
val blankPrefixRegex = Regex("""z([0-9A-Z])[:]""")
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 rawNote = oneTUO.tuNotes.getOrNull(i + 1)?.toString() ?: ""
val fixedNote = autoFixNote(rawNote, template)
i to fixedNote
},
@ -1074,8 +990,7 @@ private fun MeasureRowItem(
}.toMutableMap(),
templateFragment = oneTUO.pTemplate.template,
marker = listOfNotNull(
oneTUO.pTemplate.markerToString()
.takeIf { it.isNotBlank() },
oneTUO.pTemplate.markerToString().takeIf { it.isNotBlank() },
oneTUO.hasHairPin()?.toString()?.takeIf { it.isNotBlank() }
).joinToString(" "),
silentDuration = blankPrefix,
@ -1093,26 +1008,26 @@ private fun MeasureRowItem(
isEditable = canEdit,
canAdd = canAdd,
onDismiss = {
showAddDialog1 = false
showDetailDialog1 = false
showEditDialog1 = false
showAddDialog = false
showDetailDialog = false
showEditDialog = false
},
onSave = { newState ->
sharedScreenModel.openTUOEditor(newState)
showAddDialog1 = false
showEditDialog1 = false
showAddDialog = false
showEditDialog = false
},
onIndexChanged = { newIndex ->
selectedIndex1 = newIndex
selectedIndex = newIndex
val globalList = sharedScreenModel.tuoList.value
val nextTUO = globalList.getOrNull(newIndex)
if (nextTUO != null) {
selectedTUO1 = nextTUO
} else {
showAddDialog1 = false
showDetailDialog1 = false
showEditDialog1 = false
selectedTUO = nextTUO
}else {
showAddDialog = false
showDetailDialog = false
showEditDialog = false
}
}
)
@ -1164,12 +1079,7 @@ private fun MeasureRowItem(
val isTooLong = textWidthDp > containerWidthDp
val spacer = makeSpaceBetweenSyllables(textMeasurer, columnWidthDp)
val (dynamicSpaceSyl, alignmentText) = spacer(
syl,
cleanAllTemps,
syls_i,
index
)
val (dynamicSpaceSyl, alignmentText) = spacer(syl, cleanAllTemps, syls_i, index)
Text(
text = dynamicSpaceSyl,
modifier = Modifier
@ -1198,6 +1108,12 @@ private fun MeasureRowItem(
}
}
}
}
}
}
}
}
}
}
@Composable