Implement visual note Transposition by Clicking on SongKey

This commit is contained in:
Hasinjato 2026-06-04 09:23:19 +03:00
parent 9207be5387
commit 93797089d8
6 changed files with 194 additions and 27 deletions

View file

@ -247,7 +247,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
sharedScreenModel.setStanza(1)
}
}
fun justeBuild(sourceFile: String) {
fun justBuild(sourceFile: String, sourceContent: String) {
currentFile = sourceFile
val parseScope = CoroutineScope(Dispatchers.Default)
parseScope.launch {
@ -485,7 +485,7 @@ class Solfa(val sharedScreenModel: SharedScreenModel, private val fileRepository
currentTempFile?.writeBytes(finalString.toByteArray())
withContext(Dispatchers.Main) {
justeBuild(currentTempFile.absolutePath)
justBuild(currentTempFile.absolutePath, finalString)
}
}

View file

@ -3,12 +3,7 @@ package mg.dot.feufaro.solfa
import SharedScreenModel
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
@ -270,7 +265,8 @@ fun TimeUnitComposable(
gridColumnCount: Int,
gridActive: Boolean,
isCurrentBlock: Boolean = false,
parentFocusRequester: FocusRequester? = null
parentFocusRequester: FocusRequester? = null,
transpositionInterval: Int
) {
val col = if (tuo.getNum() % 2 == 0) Color(0xff, 0xfa, 0xf7) else Color(0xfb, 0xf3, 0xff)
val currentDensity = LocalDensity.current
@ -389,7 +385,8 @@ fun TimeUnitComposable(
tuo.annotate()
// Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à ""
val mutableNoteVersionX = if (tuo.mutableNoteVersion == -1) "7" else ""
val multiLineText = tuo.noteAsMultiString()
val multiLineNotes = tuo.noteAsMultiString()
val multiLineText = Transpose.transposeText(text = multiLineNotes, interval = transpositionInterval)
var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
Box (modifier = Modifier.fillMaxWidth()
.drawBehind {
@ -823,13 +820,15 @@ fun LazyVerticalGridTUO(
)
}
) {
val currentInterval by sharedScreenModel.transpositionInterval.collectAsState()
TimeUnitComposable(
tuo = oneTUO,
stanzaNumber = currentStanza,
gridColumnCount = gridColumnCount,
gridActive = isActive,
isCurrentBlock = isSelectedByKeyboard && editMode,
parentFocusRequester = focusRequester
parentFocusRequester = focusRequester,
transpositionInterval = currentInterval
)
if (showContextualMenu && selectedIndex == globalIndex) {
@ -1246,7 +1245,7 @@ fun EditSourceCompose(
}
withContext(Dispatchers.Main) {
solfaScreenModel.justeCompile(currentTempFile!!.absolutePath)
solfaScreenModel.justCompile(currentTempFile!!.absolutePath, expandedContent)
println("Compilation : ${currentTempFile?.absolutePath}")
currentTempFile?.deleteOnExit()

View file

@ -25,6 +25,59 @@ class Transpose {
val noteToNumber = listOf("d", "di", "r", "ri", "m", "f", "fi", "s", "si", "l", "ta", "t")
val keyToNumber = listOf("C", "Db", "D", "Eb", "E", "F", "Gb", "G", "Ab", "A", "Bb", "B" )
val keyToArmature = listOf(0, -7, 2, -3, 4, -1, -6, 1, -4, 3, -2, 5 )
fun transposeText(text: String, interval: Int): String {
if (text.isEmpty()) return text
var currentModulationInterval = 0
val MODULATION_REGEX = Regex(">([drmfslt][ia]?)([₄₃₂₁¹²³⁴',]*)>([drmfslt][ia]?)([₄₃₂₁¹²³⁴',]*)")
val NOTE_UNIT_REGEX = Regex("([drmfslt][ia]?)([₄₃₂₁¹²³⁴]*)")
val tokens = text.split(Regex("(?<=[:|•\\s])|(?=[:|•\\s])"))
val finalResult = tokens.joinToString("") { token ->
val trimmed = token.trim()
if (trimmed.isEmpty() || trimmed.matches(Regex("[:|•\\s]+"))) {
token
} else {
val modMatch = MODULATION_REGEX.matchEntire(trimmed)
if (modMatch != null) {
val oldNoteCore = modMatch.groupValues[1]
val oldOctave = modMatch.groupValues[2]
val newNoteCore = modMatch.groupValues[3]
val newOctave = modMatch.groupValues[4]
val oldIdx = noteToNumber.indexOf(oldNoteCore)
val newIdx = noteToNumber.indexOf(newNoteCore)
currentModulationInterval = newIdx - oldIdx
val transposedOld = transpose(note = oldNoteCore + oldOctave, fromKey = "C", toKey = "C", alter = interval)
val transposedNew = transpose(note = newNoteCore + newOctave, fromKey = "C", toKey = "C", alter = interval)
val modResult = ">$transposedOld>$transposedNew"
modResult
} else {
val separators = token.takeWhile { it == ',' || it == '.' }
val remaining = token.drop(separators.length)
val totalAlter = interval + currentModulationInterval
val transposedBody = NOTE_UNIT_REGEX.replace(remaining) { matchResult ->
val noteCore = matchResult.groupValues[1]
val octave = matchResult.groupValues[2]
val transposedCore = transpose(note = noteCore, fromKey = "C", toKey = "C", alter = totalAlter)
transposedCore + octave
}
val result = separators + transposedBody
result
}
}
}
return finalResult
}
fun compareNotes(note1: String, note2: String): Int {
val found1 = REGEX_ONE_NOTE.find(note1)
val found2 = REGEX_ONE_NOTE.find(note2)
@ -112,13 +165,13 @@ class Transpose {
var newNoteNum = noteNum - newKey + oldKey + alter
var suffix = ""
val noteSize = noteToNumber.size
if (newNoteNum < 0) {
while (newNoteNum < 0) {
newNoteNum += noteSize
suffix = ","
suffix += ","
}
if (newNoteNum >= noteSize) {
while (newNoteNum >= noteSize) {
newNoteNum -= noteSize
suffix = "'"
suffix += "'"
}
val newNote = noteToNumber[newNoteNum] + regexFound.groupValues[2] + suffix
return simplifyNote(newNote) + regexFound.groupValues[3]

View file

@ -2,14 +2,17 @@ package mg.dot.feufaro.ui
import SharedScreenModel
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@ -20,8 +23,8 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight
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.sp
@ -31,9 +34,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mg.dot.feufaro.pdf.PrintSettings
import mg.dot.feufaro.pdf.defaultPrintSettings
import mg.dot.feufaro.ui.PrintSettingsDialog
import mg.dot.feufaro.pdf.rememberPdfExportAction
import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.Transpose
import mg.dot.feufaro.viewmodel.SolfaScreenModel
import java.io.File
@ -226,15 +229,121 @@ fun MainScreenWithDrawer(
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
}
}, actions = {
Box(
var tempInterval by remember(fileContent) { mutableStateOf(0) }
var isEyeVisible by remember { mutableStateOf(false) }
val keysOrder = Transpose.keyToNumber
val songKeyIndex = keysOrder.indexOf(songKey).takeIf { it != -1 } ?: 0
val tempUiKey = keysOrder[(songKeyIndex + tempInterval + 24) % 12]
val appliedInterval by sharedScreenModel.transpositionInterval.collectAsState()
val isPendingChange = tempInterval != appliedInterval
val isCurrentlyTransposed = appliedInterval != 0
Row(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.Center
verticalAlignment = Alignment.CenterVertically
) {
AnimatedVisibility(
visible = isEyeVisible,
enter = expandHorizontally(expandFrom = Alignment.End) + fadeIn(tween(300)),
exit = shrinkHorizontally(shrinkTowards = Alignment.End) + fadeOut(tween(300))
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.padding(end = 8.dp)
.background(Color.White.copy(alpha = 0.15f), RoundedCornerShape(50))
.padding(horizontal = 4.dp)
) {
/* <*/
val currentIndex = keysOrder.indexOf(tempUiKey).takeIf { it != -1 } ?: 0
val targetKeyLeft = keysOrder[(currentIndex - 1 + 12) % 12]
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { Text(targetKeyLeft, fontSize = 12.sp) } },
state = rememberTooltipState()
) {
IconButton(
modifier = Modifier.size(36.dp),
onClick = { tempInterval-- }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
contentDescription = null,
tint = Color.White
)
}
}
val centralIcon = if (isPendingChange) Icons.Filled.Check else Icons.Filled.SwapHoriz
val centralTint = if (isPendingChange) Color(0xFFFFD700) else Color.White
val tooltipText = if (isPendingChange) "Transposer en $tempUiKey" else if (isCurrentlyTransposed) "Revenir au ($songKey)" else "Transposer"
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = {
PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { Text(text = tooltipText, fontSize = 12.sp) }
},
state = rememberTooltipState()
) {
IconButton(
modifier = Modifier.size(36.dp),
onClick = {
if (!isPendingChange && isCurrentlyTransposed) {
tempInterval = 0
sharedScreenModel.setTranspositionInterval(0)
} else {
sharedScreenModel.setTranspositionInterval(tempInterval)
}
solfaScreenModel.loadFromFile(sharedScreenModel.activeFilePath.value)
isEyeVisible = false
}
) {
Icon(
imageVector = centralIcon,
contentDescription = null,
tint = centralTint
)
}
}
/* > */
val targetKeyRight = keysOrder[(currentIndex + 1) % 12]
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
tooltip = { PlainTooltip(containerColor = Color.DarkGray, contentColor = Color.White) { Text(targetKeyRight, fontSize = 12.sp) } },
state = rememberTooltipState()
) {
IconButton(
modifier = Modifier.size(36.dp),
onClick = { tempInterval++ }
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = null,
tint = Color.White
)
}
}
}
}
Text(
text = songKey,
text = tempUiKey,
fontSize = 25.sp,
fontWeight = FontWeight.Black,
modifier = Modifier.padding(end = 16.dp)
textAlign = TextAlign.Center,
color = if (isCurrentlyTransposed && !isPendingChange) Color(0xFFFFD700) else Color.White,
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) {
isEyeVisible = !isEyeVisible
}
.padding(end = 16.dp)
.width(45.dp)
)
}
}, colors = TopAppBarColors(
@ -527,6 +636,7 @@ fun MainScreenWithDrawer(
solfaScreenModel.loadFromFile(item.path)
sharedScreenModel.stopMidi()
sharedScreenModel.seekTo(0f)
sharedScreenModel.setTranspositionInterval(0)
})
HorizontalDivider()
}

View file

@ -16,7 +16,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import mg.dot.feufaro.DeepLinkHandler
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.data.DrawerItem
import mg.dot.feufaro.data.GridTUOData
@ -227,6 +226,11 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
fun toggleEditorMode(enabled: Boolean) {
_modeEditor.value = enabled
}
val transpositionInterval = MutableStateFlow(0)
fun setTranspositionInterval(interval: Int) {
transpositionInterval.value = interval
}
private val _synchronizedSYllables = MutableStateFlow<List<String>>(emptyList())
val synchronizedSyllables: StateFlow<List<String>> = _synchronizedSYllables.asStateFlow()
@ -547,6 +551,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_dcDone.value = false
_dsDone.value = false
_editModeState.value = false
setTranspositionInterval(0)
try {
val midiFileName = fileRepository.getFileName(newMidiFile)
println("Opening xx129 $midiFileName")
@ -647,6 +652,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
_expandedFAB.value = false
_showSearchMenu.value = false
_editModeState.value = false
setTranspositionInterval(0)
_midiMarkersList.value = emptyList()
_tuoTimestamps.value = emptyList()
_searchTitle.value = ""

View file

@ -8,7 +8,6 @@ import mg.dot.feufaro.DeepLinkHandler
import mg.dot.feufaro.FileRepository
import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.TUOEditState
import java.io.File
class SolfaScreenModel(
val fileRepository: FileRepository,
@ -43,8 +42,8 @@ class SolfaScreenModel(
solfa.parse(path)
}
fun justeCompile(path:String) {
solfa.justeBuild(path)
fun justCompile(path: String, content: String) {
solfa.justBuild(path, content)
}
fun saveTUOEdit(editState: TUOEditState) {
screenModelScope.launch(Dispatchers.IO) {