diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt index 3cd5af1..53cd1b4 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/Solfa.kt @@ -827,6 +827,13 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository initMarkerValue: String, editState: TUOEditState ): String { + + /* Set Pending Hairpin */ + when (initMarkerValue) { + "\${>}", "\${<}" -> sharedScreenModel.setPendingHairPin(true, targetPointer) + "\${=}" -> sharedScreenModel.setPendingHairPin(false) + } + val notesByVoice = editState.notesByVoice val originalNotes = editState.originalNotes val separat = editState.sep 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 1348a69..6f65539 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TUODetailDialog.kt @@ -37,6 +37,7 @@ import mg.dot.feufaro.solfa.MarkerPopup import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.TUOEditState import mg.dot.feufaro.solfa.TimeUnitObject +import mg.dot.feufaro.solfa.extractAnchorFromNoteLine @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -49,6 +50,7 @@ fun TUODetailDialog( isEditable: Boolean, canAdd: Boolean, sharedScreenModel: SharedScreenModel, + anchorsByVoice: Map, onIndexChanged: (Int) -> Unit, onDismiss: () -> Unit, onSave: (TUOEditState) -> Unit @@ -348,10 +350,9 @@ fun TUODetailDialog( if (!templateFragment.isNullOrBlank() && templateFragment != "-") { val hasMarker = marker.isNotEmpty() + val hairPinState by sharedScreenModel.isPendingHairPinOnEdit.collectAsState() Row(verticalAlignment = Alignment.CenterVertically) { - - // 1. Bouton "Remove" (affiché uniquement si hasMarker est true) if (hasMarker) { TooltipBox( positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), @@ -363,7 +364,7 @@ fun TUODetailDialog( state = rememberTooltipState() ) { IconButton( - modifier = Modifier.size(25.dp), + modifier = Modifier.size(22.dp), onClick = { val state = TUOEditState( tuoIndex = globalIndex, @@ -376,6 +377,9 @@ fun TUODetailDialog( originalSep = initialSep, sep = newSep ) + if(hairPinState.gridIndex == globalIndex) { + sharedScreenModel.setPendingHairPin(false) + } onSave(state) } ) { @@ -388,32 +392,65 @@ fun TUODetailDialog( } } - TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), - tooltip = { - PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { - Text( - text = "Ajouter un marqueur", - fontSize = MaterialTheme.typography.titleSmall.fontSize + if(hairPinState.isPending && hairPinState.gridIndex < globalIndex) { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { + Text( + text = "Complèter le marqueur", + fontSize = MaterialTheme.typography.titleSmall.fontSize + ) + } + }, + state = rememberTooltipState() + ) { + IconButton( + modifier = Modifier.size(22.dp), + onClick = { + marker = "=" + val state = TUOEditState( + tuoIndex = globalIndex, + marker = marker + ) + onSave(state) + } + ) { + Icon( + imageVector = Icons.Default.Check, + tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), + contentDescription = null ) } - }, - state = rememberTooltipState() - ) { - IconButton( - modifier = Modifier.size(25.dp) - .onGloballyPositioned { coordinates -> - markerButtonPos = coordinates.positionInParent().let { - IntOffset(it.x.toInt(), it.y.toInt()) - } - }, - onClick = { showMarkerPopup = true } + } + } else { + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { + Text( + text = "Ajouter un marqueur", + fontSize = MaterialTheme.typography.titleSmall.fontSize + ) + } + }, + state = rememberTooltipState() ) { - Icon( - imageVector = Icons.Default.Edit, - tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), - contentDescription = null - ) + IconButton( + modifier = Modifier.size(22.dp) + .onGloballyPositioned { coordinates -> + markerButtonPos = coordinates.positionInParent().let { + IntOffset(it.x.toInt(), it.y.toInt()) + } + }, + onClick = { showMarkerPopup = true } + ) { + Icon( + imageVector = Icons.Default.Edit, + tint = MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f), + contentDescription = null + ) + } } } } @@ -461,6 +498,9 @@ fun TUODetailDialog( (0..3).forEach { voice -> val currentNote = notes[voice] ?: "" val validation = validateMusicalInput(currentNote, templateFragment) + val voiceAnchor = anchorsByVoice[voice]?.replace("#","") + val hasAnchor = !voiceAnchor.isNullOrBlank() + Box(modifier = Modifier.wrapContentSize() ) { MyTextEditField( @@ -477,6 +517,32 @@ fun TUODetailDialog( notes[voice] = newValue } ) + if (hasAnchor) { + Spacer(modifier = Modifier.width(2.dp)) + + TooltipBox( + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), + tooltip = { + PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { + Text("Ancrée sur ${transformMusicalInput(voiceAnchor)}", fontSize = 11.sp) + } + }, + state = rememberTooltipState() + ) { + Surface( + shape = RoundedCornerShape(3.dp), + color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.8f), + modifier = Modifier.padding(start = 2.dp) + ) { + Text( + text = "⚓", + fontSize = 10.sp, + color = Color.Black, + modifier = Modifier.padding(horizontal = 2.dp, vertical = 1.dp) + ) + } + } + } } } } 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 057bec6..c4f7e25 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/solfa/TimeUnitObject.kt @@ -594,6 +594,7 @@ fun LazyVerticalGridTUO( showDetailDialog = false showAddDialog = false selectedIndex = -1 + selectedGridIndex = -1 selectedTUO = null } @@ -946,9 +947,21 @@ fun LazyVerticalGridTUO( val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() + val hairPinState by sharedScreenModel.isPendingHairPinOnEdit.collectAsState() + key(oneTUO.numBlock) { Box(modifier = Modifier .width(gridWidthDp / gridColumnCount) + .then( + if (hairPinState.isPending && hairPinState.gridIndex == globalIndex) { + Modifier.background( + color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f), + shape = RoundedCornerShape(4.dp) + ) + } else { + Modifier + } + ) .border( width = if (isSelectedByKeyboard && editMode) 2.dp else 0.dp, color = if (isSelectedByKeyboard && editMode) MaterialTheme.colorScheme.error else Color.Transparent, @@ -971,10 +984,19 @@ fun LazyVerticalGridTUO( menuOffset = offset selectedTUO = oneTUO selectedIndex = globalIndex - showContextualMenu = true + showEditDialog = true } else { sharedScreenModel.seekToGrid(globalIndex) } + }, + onDoubleTap = { offset -> + if(editMode) { + selectedGridIndex = globalIndex + menuOffset = offset + selectedTUO = oneTUO + selectedIndex = globalIndex + showContextualMenu = true + } } ) } @@ -1021,7 +1043,7 @@ fun LazyVerticalGridTUO( } val currentSelected = selectedTUO - if ((showEditDialog || showAddDialog || showDetailDialog) && currentSelected != null && selectedIndex == globalIndex) { + if ((showEditDialog || showDetailDialog) && currentSelected != null && selectedIndex == globalIndex) { val canAdd = showAddDialog val canEdit = showEditDialog || canAdd @@ -1047,6 +1069,11 @@ fun LazyVerticalGridTUO( val match = blankPrefixRegex.find(afterU0T0) val blankPrefix = match?.groups?.get(1)?.value ?: "" + val anchorsByVoice = (0..3).associate { voiceIdx -> + val linePrefix = "N${voiceIdx + 1}:" + val fullLine = lines.firstOrNull { it.startsWith(linePrefix) } ?: "" + voiceIdx to extractAnchorFromNoteLine(fullLine) + } val editState = TUOEditState( tuoIndex = oneTUO.firstTuoIndex, notesByVoice = (0..3).associate { i -> @@ -1077,6 +1104,7 @@ fun LazyVerticalGridTUO( isEditable = canEdit, canAdd = canAdd, sharedScreenModel = sharedScreenModel, + anchorsByVoice = anchorsByVoice, onDismiss = { showAddDialog = false showDetailDialog = false @@ -1089,6 +1117,7 @@ fun LazyVerticalGridTUO( }, onIndexChanged = { newIndex -> selectedIndex = newIndex + selectedGridIndex = newIndex val globalList = sharedScreenModel.tuoList.value val nextTUO = globalList.getOrNull(newIndex) @@ -1627,4 +1656,9 @@ private fun transformMusicalInput(input: String): String { .replace("₂,", "₃") .replace("₂₁", "₃") .replace("'", "¹") +} + +fun extractAnchorFromNoteLine(noteLine: String): String? { + val match = Regex("#[drmfsltDRFSTz][',]*").find(noteLine) + return match?.value } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt index a8405d5..74d7a9a 100644 --- a/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt +++ b/composeApp/src/commonMain/kotlin/mg/dot/feufaro/viewmodel/SharedScreenModel.kt @@ -234,12 +234,23 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode private val _editModeState = mutableStateOf(false) val editModeState: State = _editModeState + data class HairPinEditState( + val isPending: Boolean = false, + val gridIndex: Int = 0 + ) + private val _isPendingHairPinOnEdit = MutableStateFlow(HairPinEditState()) + val isPendingHairPinOnEdit: StateFlow = _isPendingHairPinOnEdit.asStateFlow() + fun setPendingHairPin(isPending: Boolean, gridIdx: Int = 0) { + _isPendingHairPinOnEdit.value = HairPinEditState(isPending, gridIdx) + } + private val _modeEditor = MutableStateFlow(false) val modeEditor: StateFlow = _modeEditor fun toggleEditorMode(enabled: Boolean) { _modeEditor.value = enabled } + private val _harmonyView = MutableStateFlow(false) val harmonyView: StateFlow = _harmonyView fun toggleHarmonyMode(enabled: Boolean) { @@ -499,7 +510,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode var newGridIndex = marker.gridIndex ?: 0 val newMarkerList = mutableListOf() - originalMarker.forEach { markerText-> + originalMarker.forEach { markerText -> val index = marker.gridIndex ?: 0 // On utilise l'index passé par l'UI val isNearEnd = index >= (tuos.size - 2) // println("MARKERS:> $markerText\n") @@ -701,6 +712,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode _midiMarkersList.value = emptyList() _tuoTimestamps.value = emptyList() _searchTitle.value = "" + _isPendingHairPinOnEdit.value = HairPinEditState(false, 0) tempTimeUnitObjectList.clear() resetGridCount() }