diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt new file mode 100644 index 0000000..74b971a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/MarkersPopup.kt @@ -0,0 +1,415 @@ +package mg.dot.feufaro.solfa + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircleOutline +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.RemoveCircleOutline +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.PlainTooltip +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TooltipBox +import androidx.compose.material3.TooltipDefaults +import androidx.compose.material3.rememberTooltipState +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties + +data class Marker( + val abbr: String, + val description: String, + val canEditValue: Boolean = false, + val defaultValue: String = "", + val options: List = emptyList() +) + +data class MarkerGroup( + val title: String, + val markers: List +) + +private val markerGroups = listOf( + MarkerGroup( + title = "Nuances", + markers = listOf( + Marker("ppp", "Très très doux"), + Marker("pp", "Pianissimo"), + Marker("p", "Piano"), + Marker("mp", "Mezzo piano"), + Marker("mf", "Mezzo forte"), + Marker("f", "Forte"), + Marker("ff", "Fortissimo"), + Marker("fff", "Très très fort"), + + Marker("cres.", "Augmenter progressivement le volume"), + Marker("<", "Crescendo"), + Marker("decresc.", "Diminuer progressivement le volume"), + Marker("dim.", "Diminuendo"), + Marker(">", "Decrescendo"), + + Marker("sf", "Accent soudain"), + Marker("sfz", "Sforzato"), + Marker("fz", "Forzando"), + Marker("rfz", "Rinforzando"), + Marker("fp", "Forte puis immédiatement piano"), + Marker("pf", "Piano puis forte"), + Marker("sub.p", "Subito piano"), + Marker("sub.f", "Subito forte") + ) + ), + + MarkerGroup( + title = "Tempo", + markers = listOf( + Marker("𝄐", "Point d'orgue"), + Marker("Largh.", "Très très lent"), + Marker("Grave", "Très lent et solennel"), + Marker("Largo", "Large et lent"), + Marker("Lento", "Lent"), + Marker("Adagio", "Assez lent"), + Marker("And.", "Andante"), + Marker("Mod.", "Moderato"), + Marker("Alleg.", "Allegretto"), + Marker("All.", "Allegro"), + Marker("Viv.", "Vivace"), + Marker("Presto", "Très rapide"), + Marker("Prestiss.", "Extrêmement rapide"), + + Marker("accel.", "Accélérer progressivement"), + Marker("rit.", "Ralentir progressivement"), + Marker("rall.", "Ralentir"), + Marker("riten.", "Retenir le tempo"), + Marker("string.", "Presser le tempo"), + Marker("♩=", "Changement tempo", canEditValue = true, defaultValue = "60"), + Marker("allarg.", "Élargir le tempo"), + Marker("a tempo", "Retour au tempo initial"), + Marker("Tempo I", "Retour au premier tempo"), + Marker("rubato", "Tempo libre"), + Marker("meno mosso", "Moins rapide"), + Marker("più mosso", "Plus rapide") + ) + ), + + MarkerGroup( + title = "Reprises", + markers = listOf( + Marker("D.C.", "Da Capo"), + Marker("D.C. Fin", "Retour au début jusqu'à Fine"), + Marker("D.C. al Coda", "Retour au début puis aller à la Coda"), + Marker("D.S.", "Retour au Segno"), + Marker("End", "Fin"), + Marker("D.S. Fin", "Retour au Segno jusqu'à Fine"), + Marker("Farany", "Fin"), + Marker("$", "Segno"), + Marker("Fine", "Fin"), + Marker("𝄌", "Coda"), + Marker("To Coda", "Aller à la Coda"), + Marker("||:", "Début de reprise"), + Marker(":||", "Fin de reprise"), + Marker("Bis", "Rejouer") + ) + ), + + MarkerGroup( + title = "Tonalité", + markers = listOf( + Marker("Dô dia", "Modulation", canEditValue = true, defaultValue = "C", options = Transpose.keyToNumber) + ) + ) +) + + +@Composable +fun MarkerPopup( + menuPosition: IntOffset, + onDismiss: () -> Unit, + onMarkerSelected: (String) -> Unit +) { + var expandedGroupIndex by remember { + mutableStateOf(null) + } + Popup( + offset = menuPosition, + onDismissRequest = onDismiss, + properties = PopupProperties( + focusable = true, + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + + Surface ( + modifier = Modifier + .widthIn(max=250.dp) + .heightIn(max=450.dp), + shape = MaterialTheme.shapes.small, + color = MaterialTheme.colorScheme.onSecondary.copy(0.75f), + ) { + LazyColumn { + itemsIndexed(markerGroups) { index, group -> + ExpandableGroup( + group = group, + expanded = expandedGroupIndex == index, + onExpand = { + expandedGroupIndex = if(expandedGroupIndex == index) null else index + }, + onMarkerSelected = { + onMarkerSelected(it) + onDismiss() + } + ) + } + } + } + } +} + +@OptIn( + ExperimentalFoundationApi::class, + ExperimentalMaterial3Api::class, + ExperimentalLayoutApi::class +) +@Composable +private fun ExpandableGroup( + group: MarkerGroup, + expanded: Boolean, + onExpand: () -> Unit, + onMarkerSelected: (String) -> Unit +) { + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onExpand() } + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = group.title, + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.headlineSmall.copy( + color = MaterialTheme.colorScheme.background + ) + ) + Icon( + imageVector = if (expanded) + Icons.Default.RemoveCircleOutline + else + Icons.Default.AddCircleOutline, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + + AnimatedVisibility( + visible = expanded, + enter = expandVertically( + animationSpec = tween(250) + ) + fadeIn( + animationSpec = tween(250) + ), + exit = shrinkVertically( + animationSpec = tween(200) + ) + fadeOut( + animationSpec = tween(200) + ) + ) { + Column { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 220.dp) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 2.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + group.markers.forEach { marker -> + if (marker.canEditValue) { + var value by remember { mutableStateOf(marker.defaultValue) } + + Row( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .background(Color.Gray) + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (marker.options.isNotEmpty()) { + Text( + marker.abbr, + style = MaterialTheme.typography.titleMedium + ) + var optionsExpanded by remember { mutableStateOf(false) } + Box(modifier = Modifier.fillMaxWidth(0.6f)) { + OutlinedButton( + onClick = { optionsExpanded = true }, + modifier = Modifier + .fillMaxWidth() + .padding(2.dp) + ) { + Text( + value, + style = MaterialTheme.typography.titleLarge.copy( + fontWeight = FontWeight.Bold + ), + color = MaterialTheme.colorScheme.secondary + ) + } + + DropdownMenu( + expanded = optionsExpanded, + onDismissRequest = { optionsExpanded = false } + ) { + marker.options.forEach { option -> + DropdownMenuItem( + onClick = { + value = option + optionsExpanded = false + }, + text = { + Text(option) + } + ) + } + } + } + } else { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + value = value, + onValueChange = { input -> + value = input.filter { it.isDigit() } + }, + modifier = Modifier.fillMaxWidth(0.70f), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number + ), + leadingIcon = { + Text( + text = marker.abbr, + fontSize = MaterialTheme.typography.titleMedium.fontSize, + color = MaterialTheme.colorScheme.onSecondary + ) + }, + trailingIcon = { + Text( + text = "bpm", + fontSize = MaterialTheme.typography.titleSmall.fontSize, + color = MaterialTheme.colorScheme.onSecondary + ) + } + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + IconButton( + onClick = { + val current = value.toIntOrNull() ?: 0 + value = (current + 1).toString() + }, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowUp, + contentDescription = "Augmenter le tempo", + tint = Color.White + ) + } + IconButton( + onClick = { + val current = value.toIntOrNull() ?: 0 + value = (current - 1).coerceAtLeast(0).toString() + }, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = "Diminuer le tempo", + tint = Color.White + ) + } + } + } + } + IconButton( + onClick = { + val result = marker.abbr + value + onMarkerSelected(result) + }, + modifier = Modifier.size(40.dp) + ) { + Text(text = "✓", color = Color.White) + } + } + } else { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip( + containerColor = Color.DarkGray, + contentColor = Color.White + ) { + Text( + text = marker.description, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + modifier = Modifier.padding(8.dp) + ) + } + }, + state = rememberTooltipState() + ) { + Box( + modifier = Modifier + .weight(1f, fill = true) + .padding(4.dp) + .background(Color.Gray) + .clickable { onMarkerSelected(marker.abbr) } + .padding(horizontal = 7.dp, vertical = 2.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = marker.abbr, + fontSize = MaterialTheme.typography.titleSmall.fontSize, + color = MaterialTheme.colorScheme.surface + ) + } + } + } + } + } + Spacer(Modifier.height(8.dp)) + } + } + HorizontalDivider() + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt index a8e9f78..ae77caa 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -16,9 +16,13 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInParent +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -29,6 +33,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties +import mg.dot.feufaro.solfa.MarkerPopup import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TimeUnitObject @@ -102,7 +107,11 @@ fun TUODetailDialog( } } - var canAddMark = mutableStateOf(false) + var showMarkerPopup by remember { mutableStateOf(false) } + var markerButtonPos by remember { + mutableStateOf(IntOffset.Zero) + } + Popup( offset = menuPosition, onDismissRequest = onDismiss, @@ -271,7 +280,18 @@ fun TUODetailDialog( IconButton( modifier = Modifier.size(20.dp), onClick = { - newSep = "" + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = mutableMapOf(), + templateFragment = templateFragment, + marker = if(marker.isEmpty()) "_" else marker, + originalSep = initialSep, + sep = "" + ) + onSave(state) }) { Text( text = "/", @@ -285,7 +305,19 @@ fun TUODetailDialog( IconButton( modifier = Modifier.size(20.dp), onClick = { - newSep = "/" + + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = mutableMapOf(), + templateFragment = templateFragment, + marker = if(marker.isEmpty()) "_" else marker, + originalSep = initialSep, + sep = "/" + ) + onSave(state) }) { Icon( imageVector = Icons.Default.Add, @@ -312,23 +344,13 @@ fun TUODetailDialog( ) } - if(!editState.marker.isNullOrEmpty() || canAddMark.value) { - Column( - modifier = Modifier.weight(1f) - ) { - MyTextEditField( - value = marker, - customFontSize = 14.sp, - color = MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f), - customPadding = 8.dp, - customBrush = SolidColor(Color.White), - isEditable = isEditable, - isAddable = canAdd, - funTransform = ::transformMarkerInput, - onValueChng = { marker = it } - ) + if(!templateFragment.isNullOrBlank() && templateFragment !="-") { + val hasMarker = marker.isNotEmpty() + val tooltipText = if (hasMarker) { + "Enlever la marqueur" + } else { + "Ajouter une marqueur" } - } else { TooltipBox( positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), tooltip = { @@ -336,18 +358,41 @@ fun TUODetailDialog( containerColor = Color.DarkGray, contentColor = Color.White ) { - Text(text = "Ajouter une marqueur", fontSize = 12.sp) + Text(text = tooltipText, fontSize = MaterialTheme.typography.titleSmall.fontSize) } }, state = rememberTooltipState() ) { IconButton( - modifier = Modifier.size(30.dp), + modifier = Modifier.size(30.dp) + .onGloballyPositioned { coordinates -> + markerButtonPos = coordinates.positionInParent() + .let { + IntOffset( + it.x.toInt(), + it.y.toInt() + ) + } + }, onClick = { - canAddMark.value = true + if(hasMarker) { + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = mutableMapOf(), + templateFragment = templateFragment, + marker = "_", + originalSep = initialSep, + sep = newSep + ) + onSave(state) + } + showMarkerPopup = true }) { Icon( - imageVector = Icons.Default.Add, + imageVector = if(hasMarker) Icons.Default.Remove else Icons.Default.Add, tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), contentDescription = null ) @@ -541,6 +586,33 @@ fun TUODetailDialog( } } } + if (showMarkerPopup) { + MarkerPopup( + menuPosition = IntOffset( + x = markerButtonPos.x + 180, + y = markerButtonPos.y + 100 + ), + onDismiss = { + showMarkerPopup = false + }, + onMarkerSelected = { marker -> + showMarkerPopup = false + + val state = TUOEditState( + tuoIndex = globalIndex, + notesByVoice = notes.toMap(), + originalNotes = originalNotes.toMap(), + originalLyricsByStanza = originalLyricsByStz.toMutableMap(), + lyricsByStanza = mutableMapOf(), + templateFragment = templateFragment, + marker = marker, + originalSep = initialSep, + sep = newSep + ) + onSave(state) + } + ) + } } private fun unpackLyrics(lyrics: String): String {