Implement visual note Transposition by Clicking on SongKey
This commit is contained in:
parent
9207be5387
commit
93797089d8
6 changed files with 194 additions and 27 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue