Add anchor indicator & pending state when hairPin added

This commit is contained in:
Hasinjato 2026-07-21 11:20:26 +03:00
parent 3bbe9367b7
commit 36a002256d
4 changed files with 148 additions and 29 deletions

View file

@ -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

View file

@ -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<Int, String?>,
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)
)
}
}
}
}
}
}

View file

@ -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
}

View file

@ -234,12 +234,23 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
private val _editModeState = mutableStateOf(false)
val editModeState: State<Boolean> = _editModeState
data class HairPinEditState(
val isPending: Boolean = false,
val gridIndex: Int = 0
)
private val _isPendingHairPinOnEdit = MutableStateFlow(HairPinEditState())
val isPendingHairPinOnEdit: StateFlow<HairPinEditState> = _isPendingHairPinOnEdit.asStateFlow()
fun setPendingHairPin(isPending: Boolean, gridIdx: Int = 0) {
_isPendingHairPinOnEdit.value = HairPinEditState(isPending, gridIdx)
}
private val _modeEditor = MutableStateFlow(false)
val modeEditor: StateFlow<Boolean> = _modeEditor
fun toggleEditorMode(enabled: Boolean) {
_modeEditor.value = enabled
}
private val _harmonyView = MutableStateFlow(false)
val harmonyView: StateFlow<Boolean> = _harmonyView
fun toggleHarmonyMode(enabled: Boolean) {
@ -499,7 +510,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
var newGridIndex = marker.gridIndex ?: 0
val newMarkerList = mutableListOf<String>()
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()
}