Add custom keyboards for search & edit notes

This commit is contained in:
Hasinjato 2026-08-03 11:25:36 +03:00
parent 9da97209c9
commit 340f72855f
8 changed files with 1613 additions and 835 deletions

View file

@ -15,12 +15,14 @@ import androidx.compose.material.icons.filled.RemoveCircleOutline
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
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.DialogProperties
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
@ -141,6 +143,7 @@ private val markerGroups = listOf(
@Composable
@OptIn(ExperimentalComposeUiApi::class)
fun MarkerPopup(
menuPosition: IntOffset,
onDismiss: () -> Unit,
@ -155,7 +158,8 @@ fun MarkerPopup(
properties = PopupProperties(
focusable = true,
dismissOnBackPress = true,
dismissOnClickOutside = true
dismissOnClickOutside = true,
usePlatformDefaultWidth = false
)
) {

View file

@ -1,14 +1,11 @@
package mg.dot.feufaro
import SharedScreenModel
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
@ -23,6 +20,7 @@ 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.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
@ -37,7 +35,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
import mg.dot.feufaro.ui.LocalSolfaKeyboardState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -61,6 +59,18 @@ fun TUODetailDialog(
putAll(editState.notesByVoice)
}
}
val isAndroid = remember { getPlatform().name.startsWith("Android") }
val keyboardState = LocalSolfaKeyboardState.current
val keyboardController = LocalSoftwareKeyboardController.current
var selectedVoiceIndex by remember { mutableStateOf(0) }
DisposableEffect(Unit) {
keyboardState.isDialogActive = true
onDispose {
keyboardState.hide()
keyboardState.isDialogActive = false
}
}
val originalNotes = mutableStateMapOf<Int, String>().apply {
putAll(editState.notesByVoice)
}
@ -114,20 +124,103 @@ fun TUODetailDialog(
var markerButtonPos by remember {
mutableStateOf(IntOffset.Zero)
}
LaunchedEffect(selectedVoiceIndex, notes[selectedVoiceIndex]) {
if (keyboardState.isVisible) {
val currentRawText = notes[selectedVoiceIndex] ?: ""
keyboardState.currentText = transformMusicalInput(currentRawText)
}
}
fun attachKeyboardToVoice(voiceIndex: Int) {
selectedVoiceIndex = voiceIndex
if(isAndroid) {
keyboardController?.hide()
val rawInitialText = notes[voiceIndex] ?: ""
val formattedInitialText = transformMusicalInput(rawInitialText)
keyboardState.currentText = formattedInitialText
keyboardState.show(
initialVoice = voiceIndex,
initialValue = formattedInitialText,
onNote = { noteToAdd ->
val currentText = notes[voiceIndex] ?: ""
val newText = currentText + noteToAdd
val formattedText = transformMusicalInput(newText)
notes[voiceIndex] = formattedText
keyboardState.currentText = formattedText
},
onDelete = {
val currentText = notes[voiceIndex] ?: ""
if (currentText.isNotEmpty()) {
val octaveRegex = Regex("['`,₁₂₃₄₅¹²³⁴⁵]+$")
val newText = if (octaveRegex.containsMatchIn(currentText)) {
currentText.replace(octaveRegex, "")
} else {
currentText.dropLast(1)
}
notes[voiceIndex] = transformMusicalInput(newText)
keyboardState.currentText = transformMusicalInput(newText)
}
},
onSymbol = { symbol ->
val currentText = notes[voiceIndex] ?: ""
val newText = currentText + symbol
notes[voiceIndex] = transformMusicalInput(newText)
keyboardState.currentText = transformMusicalInput(newText)
},
onClose = {
onDismiss()
},
onBuild = {
val finalLyricsMap = editState.lyricsByStanza.toMutableMap()
finalLyricsMap.putAll(editedLyricsMap)
if (!finalLyricsMap.containsKey(currentStanza)) {
finalLyricsMap[currentStanza] = lyricsLines.getOrNull(0)?.trim() ?: ""
}
val state = TUOEditState(
tuoIndex = globalIndex,
notesByVoice = notes.toMap(),
originalNotes = originalNotes.toMap(),
originalLyricsByStanza = originalLyricsByStz.toMutableMap(),
lyricsByStanza = finalLyricsMap,
templateFragment = templateFragment,
marker = if(marker.isEmpty()) "_" else marker,
originalSep = initialSep,
sep = newSep
)
onSave(state)
},
onTab = {
val nextVoice = (selectedVoiceIndex + 1) % 4
attachKeyboardToVoice(nextVoice)
}
)
}
}
Popup(
offset = menuPosition,
onDismissRequest = onDismiss,
onDismissRequest = {
if(!isAndroid && !keyboardState.isVisible) {
onDismiss()
}
},
properties = PopupProperties(
focusable = true,
dismissOnBackPress = true,
dismissOnClickOutside = true
focusable = if(isAndroid) false else true,
dismissOnBackPress = !isAndroid || !keyboardState.isVisible,
dismissOnClickOutside = !isAndroid || !keyboardState.isVisible
),
) {
Surface(
modifier = Modifier
.widthIn(max=125.dp)
.heightIn(max = 400.dp),
.heightIn(max = 400.dp)
.imePadding()
.navigationBarsPadding(),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.onSecondaryContainer.copy(0.75f),
) {
@ -500,9 +593,13 @@ fun TUODetailDialog(
val validation = validateMusicalInput(currentNote, templateFragment)
val voiceAnchor = anchorsByVoice[voice]?.replace("#","")
val hasAnchor = !voiceAnchor.isNullOrBlank()
val isSelected = selectedVoiceIndex == voice && keyboardState.isVisible
Box(modifier = Modifier.wrapContentSize()
Box(modifier = Modifier
//.wrapContentSize()
.fillMaxWidth()
) {
if(!isAndroid) {
MyTextEditField(
value = currentNote ?: "",
customFontSize = 14.sp,
@ -543,12 +640,37 @@ fun TUODetailDialog(
}
}
}
} else {
SolfaTextEditField(
value = currentNote ?: "",
customFontSize = 14.sp,
color = if (isSelected) MaterialTheme.colorScheme.tertiary else Color.White,
customPadding = 8.dp,
customBrush = SolidColor(Color.White),
isWarn = !validation.isValid,
readOnly = isAndroid,
funTransform = ::transformMusicalInput,
onValueChng = { newValue ->
notes[voice] = newValue
}
)
}
if(isAndroid) {
Box(
modifier = Modifier
.matchParentSize()
.clickable {
attachKeyboardToVoice(voice)
}
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
if(!isAndroid) {
// --- SECTION LYRICS ---
lyricsLines.forEachIndexed { index, line ->
val displayedLine = line
@ -594,10 +716,16 @@ fun TUODetailDialog(
isEditable = isEditable,
isAddable = canAdd,
isWarn = !isLyricsValid,
funTransform = { input -> transformLyricsInput(input, templateFragment) },
funTransform = { input ->
transformLyricsInput(
input,
templateFragment
)
},
onValueChng = { newValue ->
val trimmedValue = newValue.trimStart()
val dataToSave = if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue "
val dataToSave =
if (trimmedValue.endsWith(" ")) trimmedValue else "$trimmedValue "
lyricsLines[index] = trimmedValue
val updatedLyricsMap = editState.lyricsByStanza.toMutableMap()
editedLyricsMap[index + 1] = dataToSave
@ -617,7 +745,9 @@ fun TUODetailDialog(
}) {
Icon(
imageVector = if (index == 0) Icons.Default.Add else Icons.Default.Clear,
tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f),
tint = if (index == 0) MaterialTheme.colorScheme.secondary.copy(alpha = 1.5f) else MaterialTheme.colorScheme.tertiary.copy(
alpha = 1.5f
),
contentDescription = null
)
}
@ -626,7 +756,9 @@ fun TUODetailDialog(
}
Spacer(modifier = Modifier.height(10.dp))
}
}
if(!isAndroid) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
@ -672,6 +804,7 @@ fun TUODetailDialog(
}
}
}
}
if (showMarkerPopup) {
MarkerPopup(
menuPosition = IntOffset(
@ -907,6 +1040,75 @@ fun MyTextEditField(
)
}
@Composable
fun SolfaTextEditField(
value: String,
customFontSize: TextUnit,
font: FontFamily? = FontFamily.SansSerif,
color: Color,
customPadding: Dp,
customBrush: Brush,
isWarn: Boolean? = false,
readOnly: Boolean,
funTransform: ((String) -> String)? = null,
onValueChng: (String) -> Unit
) {
var textFieldValueState by remember {
mutableStateOf(TextFieldValue(text = value, selection = TextRange(value.length)))
}
LaunchedEffect(value) {
if (textFieldValueState.text != value) {
val newSelection = if (textFieldValueState.selection.start <= value.length) {
textFieldValueState.selection
} else {
TextRange(value.length)
}
textFieldValueState = TextFieldValue(text = value, selection = newSelection)
}
}
BasicTextField(
value = textFieldValueState,
onValueChange = { newValue ->
println("ça change $newValue => ${textFieldValueState.text}")
val transformedText = funTransform?.invoke(newValue.text) ?: newValue.text
val finalSelection = if (transformedText.length != newValue.text.length) {
if (newValue.selection.start <= transformedText.length) newValue.selection else TextRange(transformedText.length)
} else {
newValue.selection
}
textFieldValueState = TextFieldValue(
text = transformedText,
selection = finalSelection
)
println("et ça trnasforme en = $transformedText")
onValueChng(transformedText)
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Ascii
),
textStyle = TextStyle(
color = color,
fontSize = customFontSize,
fontFamily = font
),
readOnly = readOnly,
modifier = Modifier
.fillMaxWidth()
.background(Color.Black.copy(alpha = 0.3f), RoundedCornerShape(4.dp))
.border(
if (isWarn!!) 1.dp else 0.dp,
if (isWarn!!) MaterialTheme.colorScheme.tertiary.copy(alpha = 1.5f) else Color.Transparent,
shape = RoundedCornerShape(4.dp)
)
.padding(customPadding),
cursorBrush = customBrush
)
}
data class ValidNoteResult(
val isValid: Boolean,
val message: String

View file

@ -265,6 +265,13 @@ class TimeUnitObject (val pTemplate: PTemplate, val prevTUO: TimeUnitObject?, co
}
}
private val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
private val musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE)
private val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE)
private val blankPrefixRegex =Regex("""z([0-9A-Z])[:]""")
private val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*")
private val inclusionRegex = Regex("^I([0-9]):(.*)")
@Composable
fun TimeUnitComposable(
tuo: TimeUnitObject,
@ -273,17 +280,34 @@ fun TimeUnitComposable(
gridActive: Boolean,
isCurrentBlock: Boolean = false,
parentFocusRequester: FocusRequester? = null,
transpositionInterval: Int
transpositionInterval: Int,
hairpinRange: HairpinRange? = null
) {
val col = if (tuo.getNum() % 2 == 0) MaterialTheme.colorScheme.tertiary.copy(alpha = 0.05f) else MaterialTheme.colorScheme.error.copy(alpha = 0.05f)
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
)
val animatedColor = if (gridActive) {
animateColorAsState(
targetValue = MaterialTheme.colorScheme.secondary.copy(alpha = 1f),
animationSpec = tween(durationMillis = 100)
).value
} else {
col
}
val focusRequesters = remember { List(4) { FocusRequester() } }
// Mettre en cache les annotations et les chaînes transposées
val multiLineText = remember(tuo, transpositionInterval) {
val notes = tuo.noteAsMultiString()
Transpose.transposeText(text = notes, interval = transpositionInterval)
}
val annotationsCache = remember(tuo, transpositionInterval) {
tuo.annotate()
tuo.annotations()
}
Column(
modifier = Modifier
.background(animatedColor)
@ -389,15 +413,13 @@ fun TimeUnitComposable(
.fillMaxHeight()
)
}
tuo.annotate()
/*tuo.annotate()*/
// Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à ""
val mutableNoteVersionX = if (tuo.mutableNoteVersion == -1) "7" else ""
val multiLineNotes = tuo.noteAsMultiString()
val multiLineText = Transpose.transposeText(text = multiLineNotes, interval = transpositionInterval)
var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
Box (modifier = Modifier.fillMaxWidth()
.drawBehind {
tuo.annotations().map { ta ->
annotationsCache.map { ta ->
ta.underlineSpec.map { us ->
var xStart = 0f
val separatorLength = if (tuo.sep0 in listOf(":", "!")) 1 else 0
@ -435,7 +457,8 @@ fun TimeUnitComposable(
else -> Color.Black
}
val totalHeight = textLayoutResult?.size?.height ?: 0
val nbNotes = (multiLineText + mutableNoteVersionX).split("\n").size
// # FIX
val nbNotes = multiLineText.count { it == '\n' } +1
val yPos = ta.voiceNumber * totalHeight.toFloat() / nbNotes
drawLine(
colorUnderline,
@ -446,7 +469,8 @@ fun TimeUnitComposable(
}
}
){
val annotatedText = buildAnnotatedString {
val annotatedText = remember(multiLineText, MaterialTheme.colorScheme.primary) {
buildAnnotatedString {
multiLineText.split(">").mapIndexed { index, text ->
if (index %2 == 0) {
append(text)
@ -456,7 +480,7 @@ fun TimeUnitComposable(
fontSize = 10.sp,
baselineShift = BaselineShift.Superscript,
//fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.primary
/*color = MaterialTheme.colorScheme.primary*/
)) {
append(text+" ")
}
@ -464,6 +488,7 @@ fun TimeUnitComposable(
}
}
}
Text(
text = annotatedText,
style = MaterialTheme.typography.bodyLarge,
@ -485,6 +510,8 @@ fun TimeUnitComposable(
data class TUOWidthMeasure(val width: Dp, val isReady: Boolean)
data class HairpinRange(val startBlock: Int, val endBlock: Int, val symbol: Char)
@Composable
fun bestTUOWidth(items: List<TimeUnitObject>): TUOWidthMeasure {
val textMeasurer = rememberTextMeasurer()
@ -656,7 +683,9 @@ fun LazyVerticalGridTUO(
val tuoTimestamps by sharedScreenModel.tuoTimestamps.collectAsState()
val activeRowIndex by sharedScreenModel.activeIndex.collectAsStateWithLifecycle()
val measures = tuoList.drop(1).chunked(gridColumnCount)
val measures = remember(tuoList, gridColumnCount) {
tuoList.drop(1).chunked(gridColumnCount)
}
// Avant column affichage:
val metadataList = remember(tuoList) {
tuoList.drop(1).mapIndexedNotNull { globalIndex, oneTUO ->
@ -691,6 +720,32 @@ fun LazyVerticalGridTUO(
}.distinctBy { it.gridIndex }
}
val hairpinByEndBlock: Map<Int, HairpinRange> = remember(tuoList) {
val map = mutableMapOf<Int, HairpinRange>()
var pendingStartBlock: Int? = null
var pendingSymbol: Char? = null
tuoList.drop(1).forEach { oneTUO ->
when (oneTUO.hasHairPin()) {
'<', '>' -> {
pendingStartBlock = oneTUO.numBlock
pendingSymbol = oneTUO.hasHairPin()
}
'=' -> {
val startBlock = pendingStartBlock
val symbol = pendingSymbol
if (startBlock != null && symbol != null) {
map[oneTUO.numBlock] = HairpinRange(startBlock, oneTUO.numBlock, symbol)
}
pendingStartBlock = null
pendingSymbol = null
}
else -> Unit
}
}
map
}
// Envoyer les données au ViewModel une seule fois
LaunchedEffect(metadataList) {
if (metadataList.isNotEmpty()) {
@ -713,7 +768,6 @@ fun LazyVerticalGridTUO(
val textMeasurer = rememberTextMeasurer()
val containerWidthDp = gridWidthDp / gridColumnCount
val REGEX_CLEAN_PREFIX = Regex("(\\d+\\.)+")
val isAnySyllableTooLong = remember(currentStanza, gridWidthDp, gridColumnCount) {
val allMeasuresSyllables = tuoList.drop(1).chunked(gridColumnCount).map { measureTUOss ->
measureTUOss.map { it.getSingleSyllable(currentStanza) }.map { column ->
@ -786,7 +840,7 @@ fun LazyVerticalGridTUO(
if (tuo.isTriolet()) {
Canvas(modifier = Modifier.fillMaxSize()) {
val arcWidth = with(density) { size.width * 0.65f }
val arcWidth = size.width * 0.65f
drawArc(
color = FEUFAROO_TRIOLET_COLOR,
startAngle = 200f,
@ -800,18 +854,14 @@ fun LazyVerticalGridTUO(
)
}
}
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) {
val hairpinRange = hairpinByEndBlock[tuo.numBlock]
if (hairpinRange != null) {
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 (hairpinRange.symbol == '>') -size.width * (tuo.numBlock - hairpinRange.startBlock) else size.width / 2
val xEnd = if (hairpinRange.symbol == '>') size.width / 2 else -size.width * (tuo.numBlock - hairpinRange.startBlock)
val offsetUp = -yHeight * 0.30f
val hairpinHeight = yHeight * 0.70f
@ -832,20 +882,12 @@ fun LazyVerticalGridTUO(
strokeWidth = 0.5f
)
}
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 musicForPtSerifBIRegex = Regex("""^(Largh\.|Grave|Largo|Lento|Adagio|And\.|Andantino|Mod\.|Moderato|Alleg\.|All\.|Viv\.|Vivacissimo|Presto|Prestiss\.|accel\.|rit\.|rall\.|riten\.|string\.|allarg\.|a tempo|Tempo I|rubato|meno mosso|più mosso|cres\.|<|decresc\.|dim\.|>|rfz|fp|pf|sub\.p|sub\.f)""",RegexOption.IGNORE_CASE)
val musicForEmmentRegex = Regex("""^(ppp|pp|mp|mf|fff|ff|p|f|sfz|sf|fz|rfz|fp|pf)\b""",RegexOption.IGNORE_CASE)
val markerList = tuo.pTemplate.markerToList()
Row(
modifier = Modifier.wrapContentSize(unbounded = true, align = Alignment.CenterStart),
@ -1063,7 +1105,8 @@ fun LazyVerticalGridTUO(
gridActive = isActive,
isCurrentBlock = isSelectedByKeyboard && editMode,
parentFocusRequester = focusRequester,
transpositionInterval = currentInterval
transpositionInterval = currentInterval,
hairpinRange = hairpinByEndBlock[oneTUO.numBlock]
)
if (showContextualMenu && selectedIndex == globalIndex) {
@ -1119,7 +1162,6 @@ fun LazyVerticalGridTUO(
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 ?: ""
@ -1217,7 +1259,6 @@ fun LazyVerticalGridTUO(
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()
@ -1295,8 +1336,9 @@ fun makeSpaceBetweenSyllables(
val density = LocalDensity.current
val noteWidthPx = with(density) { noteWidthDp.toPx() }
val style = TextStyle(fontSize = fontSize)
val spaceWidthPx = textMeasurer.measure("\u00A0", style).size.width
val spaceWidthPx = remember(textMeasurer, style) {
textMeasurer.measure("\u00A0", style).size.width
}
return { currentSyl, allT, s_i, index ->
var textAlign = Alignment.Center
@ -1308,38 +1350,25 @@ fun makeSpaceBetweenSyllables(
val excessPx = currentWidth - noteWidthPx
val isExcess = (excessPx > 0)
val neededSpaces = if (isExcess) (excessPx / spaceWidthPx).toInt() + 1 else 0
val neededSpaces = if (isExcess && spaceWidthPx > 0) (excessPx / spaceWidthPx).toInt() + 1 else 0
val nextNextSyl = allT.getOrNull(s_i + 2)?.getOrNull(index) ?: ""
// println("801: prev: [$prev_syl]${prev_syl.length}:[${textMeasurer.measure(prev_syl, style).size.width}], current [$currentSyl]:[${textMeasurer.measure(currentSyl, style).size.width}], next [$next_syl]:[${textMeasurer.measure(next_syl, style).size.width}], isExcess=$excessPx & need $neededSpaces \t spwp $spaceWidthPx\n")
val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt()
val paddingNeeded = totalSpacesPossible - (currentSyl.length+4)
val resultText = when {
!isExcess && (paddingNeeded > 0) -> {
!isExcess && (noteWidthPx > 0 && spaceWidthPx > 0) -> {
val totalSpacesPossible = (noteWidthPx / spaceWidthPx).toInt()
val paddingNeeded = totalSpacesPossible - (currentSyl.length + 4)
if (paddingNeeded > 0) {
textAlign = Alignment.TopStart
currentSyl + "\u00A0".repeat(paddingNeeded)
}
isExcess && next_syl.length < prev_syl.length -> {
"\u00A0".repeat(neededSpaces) + currentSyl
}
isExcess && next_syl.length >= prev_syl.length -> {
currentSyl + "\u00A0".repeat(neededSpaces)
}
isExcess && next_syl.isNotEmpty() -> {
val nextWidth = textMeasurer.measure(next_syl, style).size.width
val nextExcess = nextWidth - noteWidthPx
if (nextExcess > 0 && (allT.getOrNull(s_i + 2)?.getOrNull(index)?.length ?: 0) > next_syl.length) {
// Si le mot d'après va se décaler à gauche, moi je me décale aussi à gauche
val compensationSpaces = ((nextExcess / spaceWidthPx) / 2).toInt() + 1
"\u00A0".repeat(compensationSpaces) + currentSyl
} else {
currentSyl
}
}
isExcess && spaceWidthPx > 0 && next_syl.length < prev_syl.length -> {
"\u00A0".repeat(neededSpaces) + currentSyl
}
isExcess && spaceWidthPx > 0 -> {
currentSyl + "\u00A0".repeat(neededSpaces)
}
else -> currentSyl
}
resultText to textAlign
@ -1356,7 +1385,6 @@ private suspend fun expandInclusions(
val lastSlash = currentFilePath.lastIndexOf('/')
val directory = if (lastSlash != -1) currentFilePath.substring(0, lastSlash + 1) else ""
val inclusionRegex = Regex("^I([0-9]):(.*)")
lines.forEach { line ->
val match = inclusionRegex.find(line)
@ -1592,7 +1620,6 @@ fun autoFixNote(rawNote: String, template: String): String {
// println("==================================================")
if (rawNote == "_") return rawNote
val noteRegex = Regex("([drmfsltDRFSTw][ia]?|―)[0-9'¹²³⁴₁₂₃₄,]*")
val actualNotes = if (rawNote.isEmpty()) emptyList() else noteRegex.findAll(rawNote).map { it.value }.toList()
// println(" • Notes détectées dans RawNote : $actualNotes (Taille: ${actualNotes.size})")
/*if (rawNote.contains("") || rawNote.contains("") || rawNote.contains(",")) {

View file

@ -0,0 +1,123 @@
package mg.dot.feufaro.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@Composable
fun CustomKeyboard(
onDigitClick: (String) -> Unit,
onDeleteClick: () -> Unit,
onClearClick: () -> Unit,
onPrefixClick: (String) -> Unit,
isFullScreen: Boolean,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier
.fillMaxWidth(),
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.75f),
tonalElevation = 0.dp,
shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)
) {
Column(
modifier = Modifier
.padding(horizontal = 12.dp, vertical = 10.dp)
.then(
if (isFullScreen) Modifier.navigationBarsPadding() else Modifier
),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val prefixes = listOf("EWS", "FFPM", "FF", "Antema", "Tsanta", "Salamo")
LazyRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 1.dp)
) {
items(prefixes) { prefix ->
Button(
onClick = { onPrefixClick("$prefix ") },
modifier = Modifier.height(38.dp),
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.15f),
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
),
contentPadding = PaddingValues(horizontal = 14.dp, vertical = 0.dp)
) {
Text(
text = prefix,
fontSize = MaterialTheme.typography.titleLarge.fontSize,
fontWeight = FontWeight.Bold
)
}
}
}
val keys = listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("", "0", "")
)
for (row in keys) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
for (key in row) {
val isActionKey = key == "" || key == ""
Button(
onClick = {
when (key) {
"" -> onDeleteClick()
"" -> onClearClick()
else -> onDigitClick(key)
}
},
modifier = Modifier
.weight(1f)
.height(44.dp),
shape = RoundedCornerShape(10.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isActionKey)
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.6f)
else
MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f),
contentColor = if (isActionKey)
MaterialTheme.colorScheme.onErrorContainer
else
Color.White
)
) {
Text(
text = key,
fontSize = MaterialTheme.typography.headlineLarge.fontSize,
fontWeight = FontWeight.Bold
)
}
}
}
}
}
}
}

View file

@ -30,11 +30,16 @@ import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -182,11 +187,15 @@ fun MainScreenWithDrawer(
val isQrVisible = sharedScreenModel.isQRCodeVisible.value
val sharedInteractionSource = remember { MutableInteractionSource() }
LaunchedEffect(drawerState.isOpen) {
if (drawerState.isOpen) {
sharedScreenModel.loadItems()
}
}
val density = LocalDensity.current
var keyboardHeightDp by remember { mutableStateOf(0.dp) }
ModalNavigationDrawer(drawerState = drawerState, drawerContent = {
SimpleDrawerContent(
@ -253,6 +262,22 @@ fun MainScreenWithDrawer(
)
}
val favoritePaths by sharedScreenModel.playlistItems.collectAsState()
val keyboardController = LocalSoftwareKeyboardController.current
var isNumericKeyboard by remember { mutableStateOf(true) }
LaunchedEffect(isNumericKeyboard, isSearchActive) {
if (isSearchActive) {
if (isAndroid && isNumericKeyboard) {
keyboardController?.hide()
} else {
keyboardController?.show()
}
}
}
val solfaKeyboardState = remember { SolfaKeyboardState() }
CompositionLocalProvider(LocalSolfaKeyboardState provides solfaKeyboardState) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val isLandscape = maxWidth > maxHeight
val topAppBarHeight = if (isAndroid) {
@ -265,11 +290,12 @@ fun MainScreenWithDrawer(
50.dp
}
CompositionLocalProvider(LocalSolfaKeyboardState provides solfaKeyboardState) {
Scaffold(
contentWindowInsets = if (isFullScreenEnabled) {
WindowInsets(0, 0, 0, 0)
} else {
if(!isLandscape) {
if (!isLandscape) {
WindowInsets.safeDrawing
} else {
WindowInsets(0, 0, 0, 0)
@ -278,9 +304,10 @@ fun MainScreenWithDrawer(
topBar = {
TopAppBar(
modifier = Modifier.height(topAppBarHeight),
windowInsets = if(!isLandscape) {
windowInsets = if (!isLandscape) {
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal + WindowInsetsSides.Top)
WindowInsetsSides.Horizontal + WindowInsetsSides.Top
)
} else {
WindowInsets(0, 0, 0, 0)
},
@ -300,12 +327,16 @@ fun MainScreenWithDrawer(
textInput = newValue
sharedScreenModel.updateSearchTxt(newValue)
},
readOnly = isAndroid && isNumericKeyboard,
textStyle = LocalTextStyle.current.copy(
color = Color.White,
fontSize = 16.sp
),
singleLine = true,
cursorBrush = SolidColor(Color.White),
keyboardOptions = KeyboardOptions(
keyboardType = if (isAndroid && isNumericKeyboard) KeyboardType.Text else KeyboardType.Number
),
modifier = Modifier
.fillMaxWidth()
.height(38.dp)
@ -321,7 +352,7 @@ fun MainScreenWithDrawer(
enabled = true,
singleLine = true,
visualTransformation = VisualTransformation.None,
interactionSource = remember { MutableInteractionSource() },
interactionSource = sharedInteractionSource,
placeholder = {
Text(
text = "FFPM 1 Andriananahary ...",
@ -338,7 +369,26 @@ fun MainScreenWithDrawer(
)
},
trailingIcon = {
if (textInput.isNotEmpty()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
if (isAndroid) {
IconButton(
onClick = {
isNumericKeyboard = !isNumericKeyboard
},
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = if (isNumericKeyboard) Icons.Default.Abc else Icons.Filled._123,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(22.dp)
)
}
}
IconButton(
onClick = {
textInput = ""
@ -348,14 +398,17 @@ fun MainScreenWithDrawer(
) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = "Effacer",
contentDescription = null,
tint = Color.White.copy(alpha = 0.8f),
modifier = Modifier.size(18.dp)
)
}
}
},
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp),
contentPadding = PaddingValues(
horizontal = 10.dp,
vertical = 0.dp
),
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
@ -385,7 +438,7 @@ fun MainScreenWithDrawer(
}
}
}, navigationIcon = {
if(!isQrVisible) {
if (!isQrVisible) {
IconButton(onClick = {
scope.launch { drawerState.open() }
}) {
@ -419,7 +472,7 @@ fun MainScreenWithDrawer(
color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
interactionSource = sharedInteractionSource,
indication = null
) {
isEyeVisible = !isEyeVisible
@ -471,9 +524,12 @@ fun MainScreenWithDrawer(
}
}
val centralIcon = if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz
val centralTint = if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White
val tooltipText = if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer"
val centralIcon =
if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz
val centralTint =
if (isPendingChange) MaterialTheme.colorScheme.tertiary else Color.White
val tooltipText =
if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir en $songKey" else "Transposer"
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
@ -553,7 +609,10 @@ fun MainScreenWithDrawer(
},
modifier = Modifier.fillMaxWidth().height(36.dp),
shape = RoundedCornerShape(8.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)),
border = BorderStroke(
1.dp,
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f)
),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.tertiary
)
@ -580,8 +639,10 @@ fun MainScreenWithDrawer(
Row(
modifier = Modifier.fillMaxWidth()
) {
if (!solfaKeyboardState.isVisible) {
Column(
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
modifier = Modifier.fillMaxWidth().padding(5.dp),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(7.dp)
) {
if (isEditMode) {
@ -637,7 +698,7 @@ fun MainScreenWithDrawer(
icon = Icons.Default.Close
)
}
} else if(!isQrVisible) {
} else if (!isQrVisible) {
AnimatedVisibility(
visible = isExpanded and !showMidiCtrl,
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
@ -704,7 +765,7 @@ fun MainScreenWithDrawer(
icon = if (showMidiCtrl) Icons.Filled.StopCircle else Icons.Filled.PlayCircle
)
}
if (!showMidiCtrl) {
if (!showMidiCtrl && !isSearchActive) {
MyFAB(
onClick = {
sharedScreenModel.setExpandedFAB(!isExpanded)
@ -758,16 +819,22 @@ fun MainScreenWithDrawer(
}
}
}
}
}) { paddingValues ->
Box(
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
content(PaddingValues(0.dp))
AnimatedVisibility(
androidx.compose.animation.AnimatedVisibility(
visible = isQrVisible,
enter = slideInVertically(
initialOffsetY = { fullHeight -> fullHeight }
@ -782,13 +849,13 @@ fun MainScreenWithDrawer(
)
}
if(createMode) {
if (createMode) {
NewPartition(
onCreate = { partMtdata ->
solfaScreenModel.newSolfa(partMtdata)
sharedScreenModel.toggleCreateMode(!createMode)
sharedScreenModel.toggleEditorMode(createMode)
if(!isAndroid) {
if (!isAndroid) {
sharedScreenModel.toggleSourceMode()
}
},
@ -802,8 +869,8 @@ fun MainScreenWithDrawer(
var isHovered by remember { mutableStateOf(false) }
val offsetX by animateDpAsState(targetValue = if (isHovered || isSearchActive) 0.dp else 15.dp)
AnimatedVisibility(
visible = !isEditMode && !isQrVisible && !createMode,
androidx.compose.animation.AnimatedVisibility(
visible = !isEditMode && !isQrVisible && !createMode && !isExpanded,
modifier = Modifier
.align(Alignment.TopEnd)
.padding(top = 15.dp)
@ -850,7 +917,7 @@ fun MainScreenWithDrawer(
}
}
AnimatedVisibility(
androidx.compose.animation.AnimatedVisibility(
visible = isSearchActive && textInput.isNotEmpty(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically(),
@ -858,11 +925,13 @@ fun MainScreenWithDrawer(
.align(Alignment.TopCenter)
.fillMaxWidth(0.85f)
.padding(top = 8.dp)
.padding(bottom = if (isAndroid && isNumericKeyboard) keyboardHeightDp else 0.dp)
) {
Card(
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
modifier = Modifier.fillMaxHeight()
) {
val sortedSongs = remember(filteredSongs) {
filteredSongs.sortedBy { item ->
@ -918,7 +987,9 @@ fun MainScreenWithDrawer(
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = if (!isFavorite) Color.LightGray else Color(0xFFFFD700)
tint = if (!isFavorite) Color.LightGray else Color(
0xFFFFD700
)
)
}
},
@ -935,6 +1006,63 @@ fun MainScreenWithDrawer(
}
}
}
// --- CLAVIER NUMÉRIQUE PERSONNALISÉ (Sur couche supérieure) ---
androidx.compose.animation.AnimatedVisibility(
visible = isSearchActive && isAndroid && isNumericKeyboard,
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter)
) {
CustomKeyboard(
modifier = Modifier.onGloballyPositioned { coordinates ->
keyboardHeightDp = with(density) { coordinates.size.height.toDp() }
},
onDigitClick = { digit ->
textInput += digit
sharedScreenModel.updateSearchTxt(textInput)
},
onDeleteClick = {
if (textInput.isNotEmpty()) {
textInput = textInput.dropLast(1)
sharedScreenModel.updateSearchTxt(textInput)
}
},
onClearClick = {
textInput = ""
sharedScreenModel.updateSearchTxt("")
},
onPrefixClick = { prefix ->
sharedScreenModel.updateSearchTxt(prefix)
},
isFullScreen = isFullScreenEnabled
)
}
}
AnimatedVisibility(
visible = solfaKeyboardState.isVisible && solfaKeyboardState.isDialogActive && isAndroid,
enter = slideInVertically(initialOffsetY = { fullHeight -> fullHeight }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { fullHeight -> fullHeight }) + fadeOut()
) {
SolfaKeyboard(
currentVoiceIndex = solfaKeyboardState.activeVoiceIndex,
currentValue = solfaKeyboardState.currentText,
onNoteClick = { note -> solfaKeyboardState.onNoteInput?.invoke(note) },
onTabClick = { solfaKeyboardState.onTabNextVoice?.invoke() },
onBackspaceClick = { solfaKeyboardState.onBackspace?.invoke() },
onCloseClick = { solfaKeyboardState.onCloseDialog?.invoke() },
onBuildClick = { solfaKeyboardState.onBuildEdit?.invoke() },
onCustomSymbolClick = { symbol -> solfaKeyboardState.onCustomSymbol?.invoke(symbol) },
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.3f)
)
}
}
}
}
}
}

View file

@ -59,6 +59,7 @@ fun SimpleDrawerContent(
val internalList by sharedScreenModel.internalItems.collectAsState()
val externalList by sharedScreenModel.externalItems.collectAsState()
val playList by sharedScreenModel.playlistItems.collectAsState()
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
val listState = rememberLazyListState()
var internalExpanded by remember { mutableStateOf(false) }
@ -154,7 +155,6 @@ fun SimpleDrawerContent(
if (internalExpanded) {
items(internalList) { item ->
val isSelected = item.path == activePath
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
val isFavorite = favoriteLists.contains(item)
NavigationDrawerItem(
@ -207,7 +207,6 @@ fun SimpleDrawerContent(
if (externalExpanded) {
items(externalList) { item ->
val isSelected = item.path == activePath
val favoriteLists by sharedScreenModel.playlistItems.collectAsState()
val isFavorite = favoriteLists.contains(item)
if (item.path != "") {

View file

@ -0,0 +1,296 @@
package mg.dot.feufaro.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardBackspace
import androidx.compose.material.icons.automirrored.filled.KeyboardTab
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
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.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mg.dot.feufaro.solfa.Transpose
enum class OctaveMode {
LOWER, NORMAL, UPPER
}
val SOLFA_NOTES = Transpose.noteToNumber
@Composable
fun SolfaKeyboard(
currentVoiceIndex: Int,
currentValue: String = "",
onNoteClick: (String) -> Unit,
onTabClick: () -> Unit,
onBackspaceClick: () -> Unit,
onBuildClick: () -> Unit,
onCloseClick: () -> Unit,
onCustomSymbolClick: (String) -> Unit,
modifier: Modifier = Modifier
) {
var selectedOctave by remember { mutableStateOf("") }
val voiceLabels = listOf("S", "A", "T", "B")
Surface(
modifier = modifier
.fillMaxWidth()
.background(Color(0xFF232323)),
color = Color(0xFF232323),
tonalElevation = 8.dp
) {
Row(
modifier = Modifier
.padding(6.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(5.dp)
) {
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(5.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = onTabClick,
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
shape = RoundedCornerShape(4.dp),
contentPadding = PaddingValues(horizontal = 6.dp, vertical = 2.dp),
modifier = Modifier.height(36.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardTab,
contentDescription = null,
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(2.dp))
Text(
text = voiceLabels.getOrElse(currentVoiceIndex) { "S" },
fontSize = 11.sp,
fontWeight = FontWeight.Bold
)
}
BasicTextField(
value = currentValue,
onValueChange = {},
readOnly = true,
textStyle = TextStyle(
color = Color.White,
fontSize = 13.sp,
fontWeight = FontWeight.Medium
),
modifier = Modifier
.weight(1.5f)
.height(36.dp)
.background(Color.Black.copy(alpha = 0.4f), RoundedCornerShape(4.dp))
.padding(horizontal = 8.dp, vertical = 8.dp)
)
OutlinedButton(
onClick = {
onNoteClick(",")
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF383838)
),
modifier = Modifier.height(36.dp).weight(0.7f)
) {
Text(",", fontSize = 14.sp, fontWeight = FontWeight.Bold)
}
OutlinedButton(
onClick = {
onNoteClick("'")
},
colors = ButtonDefaults.buttonColors(
containerColor = Color(0xFF383838)
),
modifier = Modifier.height(36.dp)
) {
Text("'", fontSize = 14.sp, fontWeight = FontWeight.Bold)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Spacer(modifier = Modifier.weight(0.5f))
KeyButton("di", Modifier.weight(1f)) { onNoteClick("di") }
KeyButton("ri", Modifier.weight(1f)) { onNoteClick("ri") }
Spacer(modifier = Modifier.weight(1.3f))
KeyButton("fi", Modifier.weight(1f)) { onNoteClick("fi") }
KeyButton("si", Modifier.weight(1f)) { onNoteClick("si") }
KeyButton("ta", Modifier.weight(1f)) { onNoteClick("ta") }
Spacer(modifier = Modifier.weight(0.5f))
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
val diatonicNotes = listOf("d", "r", "m", "f", "s", "l", "t")
for (note in diatonicNotes) {
KeyButton(note, Modifier.weight(1f)) { onNoteClick(note) }
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
KeyButton("", Modifier.weight(1f)) { onCustomSymbolClick("-") }
KeyButton("", Modifier.weight(1f)) { onCustomSymbolClick(";") }
KeyButton("", Modifier.weight(0.5f)) { onCustomSymbolClick(".") }
KeyButton("", Modifier.weight(0.5f)) { onCustomSymbolClick(" ,") }
KeyButton("Espace", Modifier.weight(1.6f)) { onCustomSymbolClick(" ") }
KeyButton("(", Modifier.weight(0.6f)) { onCustomSymbolClick("(") }
KeyButton(")", Modifier.weight(0.6f)) { onCustomSymbolClick(")") }
KeyButton("<", Modifier.weight(0.5f)) { onCustomSymbolClick("<") }
KeyButton(">", Modifier.weight(0.5f)) { onCustomSymbolClick(">") }
KeyButton("z", Modifier.weight(1f)) { onNoteClick("z") }
}
}
Column(
modifier = Modifier
.width(48.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Button(
onClick = onCloseClick,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.errorContainer),
contentPadding = PaddingValues(0.dp)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer
)
}
Button(
onClick = onBackspaceClick,
modifier = Modifier
.fillMaxWidth()
.weight(1f),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF4A4A4A)),
contentPadding = PaddingValues(0.dp)
) {
Icon(
imageVector = Icons.AutoMirrored.Default.KeyboardBackspace,
contentDescription = null,
tint = Color.White
)
}
Button(
onClick = onBuildClick,
modifier = Modifier
.fillMaxWidth()
.weight(1.2f),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary),
contentPadding = PaddingValues(0.dp)
) {
Icon(
imageVector = Icons.Default.Save,
contentDescription = null,
tint = Color.White
)
}
}
}
}
}
@Composable
private fun KeyButton(
text: String,
modifier: Modifier = Modifier,
containerColor: Color = Color(0xFF383838),
onClick: () -> Unit
) {
Button(
onClick = onClick,
modifier = modifier.height(49.dp),
shape = RoundedCornerShape(4.dp),
colors = ButtonDefaults.buttonColors(containerColor = containerColor),
contentPadding = PaddingValues(0.dp)
) {
Text(
text = text,
color = Color.White,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
}
}
class SolfaKeyboardState {
var isVisible by mutableStateOf(false)
private set
var isDialogActive by mutableStateOf(false)
var activeVoiceIndex by mutableStateOf(0)
private set
var currentText by mutableStateOf("")
var onNoteInput: ((String) -> Unit)? = null
var onBackspace: (() -> Unit)? = null
var onCloseDialog: (() -> Unit)? = null
var onBuildEdit: (() -> Unit)? = null
var onCustomSymbol: ((String) -> Unit)? = null
var onTabNextVoice: (() -> Unit)? = null
var onMarker: (() -> Unit)? = null
fun show(
initialVoice: Int = 0,
initialValue: String = "",
onNote: (String) -> Unit,
onDelete: () -> Unit,
onSymbol: (String) -> Unit,
onBuild: () -> Unit,
onClose: () -> Unit,
onTab: () -> Unit
) {
if (!isDialogActive) return
activeVoiceIndex = initialVoice
onNoteInput = onNote
onBackspace = onDelete
onCustomSymbol = onSymbol
onCloseDialog = onClose
onBuildEdit = onBuild
onTabNextVoice = onTab
isVisible = true
}
fun hide() {
isVisible = false
}
}
@Composable
fun rememberSolfaKeyboardState() = remember { SolfaKeyboardState() }
val LocalSolfaKeyboardState = staticCompositionLocalOf <SolfaKeyboardState> {
error("Aucun SolfaKeyboardState fourni")
}

View file

@ -612,7 +612,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_dcDone.value = false
_dsDone.value = false
_sourceModeState.value = false
setTranspositionInterval(0)
try {
val midiFileName = fileRepository.getFileName(newMidiFile)
println("Opening xx129 $midiFileName")