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

View file

@ -3,12 +3,7 @@ package mg.dot.feufaro.solfa
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas import androidx.compose.foundation.*
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.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
@ -270,7 +265,8 @@ fun TimeUnitComposable(
gridColumnCount: Int, gridColumnCount: Int,
gridActive: Boolean, gridActive: Boolean,
isCurrentBlock: Boolean = false, 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 col = if (tuo.getNum() % 2 == 0) Color(0xff, 0xfa, 0xf7) else Color(0xfb, 0xf3, 0xff)
val currentDensity = LocalDensity.current val currentDensity = LocalDensity.current
@ -389,7 +385,8 @@ fun TimeUnitComposable(
tuo.annotate() tuo.annotate()
// Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à "" // Utile pour que Compose recalcule la cellule. La valeur sera toutefois toujours égale à ""
val mutableNoteVersionX = if (tuo.mutableNoteVersion == -1) "7" else "" 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) } var textLayoutResult: TextLayoutResult? by remember { mutableStateOf(null) }
Box (modifier = Modifier.fillMaxWidth() Box (modifier = Modifier.fillMaxWidth()
.drawBehind { .drawBehind {
@ -823,13 +820,15 @@ fun LazyVerticalGridTUO(
) )
} }
) { ) {
val currentInterval by sharedScreenModel.transpositionInterval.collectAsState()
TimeUnitComposable( TimeUnitComposable(
tuo = oneTUO, tuo = oneTUO,
stanzaNumber = currentStanza, stanzaNumber = currentStanza,
gridColumnCount = gridColumnCount, gridColumnCount = gridColumnCount,
gridActive = isActive, gridActive = isActive,
isCurrentBlock = isSelectedByKeyboard && editMode, isCurrentBlock = isSelectedByKeyboard && editMode,
parentFocusRequester = focusRequester parentFocusRequester = focusRequester,
transpositionInterval = currentInterval
) )
if (showContextualMenu && selectedIndex == globalIndex) { if (showContextualMenu && selectedIndex == globalIndex) {
@ -1246,7 +1245,7 @@ fun EditSourceCompose(
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
solfaScreenModel.justeCompile(currentTempFile!!.absolutePath) solfaScreenModel.justCompile(currentTempFile!!.absolutePath, expandedContent)
println("Compilation : ${currentTempFile?.absolutePath}") println("Compilation : ${currentTempFile?.absolutePath}")
currentTempFile?.deleteOnExit() 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 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 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 ) 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 { fun compareNotes(note1: String, note2: String): Int {
val found1 = REGEX_ONE_NOTE.find(note1) val found1 = REGEX_ONE_NOTE.find(note1)
val found2 = REGEX_ONE_NOTE.find(note2) val found2 = REGEX_ONE_NOTE.find(note2)
@ -112,13 +165,13 @@ class Transpose {
var newNoteNum = noteNum - newKey + oldKey + alter var newNoteNum = noteNum - newKey + oldKey + alter
var suffix = "" var suffix = ""
val noteSize = noteToNumber.size val noteSize = noteToNumber.size
if (newNoteNum < 0) { while (newNoteNum < 0) {
newNoteNum += noteSize newNoteNum += noteSize
suffix = "," suffix += ","
} }
if (newNoteNum >= noteSize) { while (newNoteNum >= noteSize) {
newNoteNum -= noteSize newNoteNum -= noteSize
suffix = "'" suffix += "'"
} }
val newNote = noteToNumber[newNoteNum] + regexFound.groupValues[2] + suffix val newNote = noteToNumber[newNoteNum] + regexFound.groupValues[2] + suffix
return simplifyNote(newNote) + regexFound.groupValues[3] return simplifyNote(newNote) + regexFound.groupValues[3]

View file

@ -2,14 +2,17 @@ package mg.dot.feufaro.ui
import SharedScreenModel import SharedScreenModel
import androidx.compose.animation.* import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons 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.automirrored.filled.Undo
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* 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.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight 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.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.unit.sp
@ -31,9 +34,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mg.dot.feufaro.pdf.PrintSettings import mg.dot.feufaro.pdf.PrintSettings
import mg.dot.feufaro.pdf.defaultPrintSettings import mg.dot.feufaro.pdf.defaultPrintSettings
import mg.dot.feufaro.ui.PrintSettingsDialog
import mg.dot.feufaro.pdf.rememberPdfExportAction import mg.dot.feufaro.pdf.rememberPdfExportAction
import mg.dot.feufaro.solfa.Solfa import mg.dot.feufaro.solfa.Solfa
import mg.dot.feufaro.solfa.Transpose
import mg.dot.feufaro.viewmodel.SolfaScreenModel import mg.dot.feufaro.viewmodel.SolfaScreenModel
import java.io.File import java.io.File
@ -226,15 +229,121 @@ fun MainScreenWithDrawer(
Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu") Icon(Icons.Filled.Menu, contentDescription = "Ouvrir Menu")
} }
}, actions = { }, 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(), 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(
text = songKey, text = tempUiKey,
fontSize = 25.sp, fontSize = 25.sp,
fontWeight = FontWeight.Black, 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( }, colors = TopAppBarColors(
@ -527,6 +636,7 @@ fun MainScreenWithDrawer(
solfaScreenModel.loadFromFile(item.path) solfaScreenModel.loadFromFile(item.path)
sharedScreenModel.stopMidi() sharedScreenModel.stopMidi()
sharedScreenModel.seekTo(0f) sharedScreenModel.seekTo(0f)
sharedScreenModel.setTranspositionInterval(0)
}) })
HorizontalDivider() HorizontalDivider()
} }

View file

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

View file

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