Add btn to Export pdf & customize print settings
This commit is contained in:
parent
b3d7429b59
commit
1d35b9227d
10 changed files with 1394 additions and 347 deletions
|
|
@ -69,9 +69,9 @@ private fun measureNoteWidth(font: PDType0Font, fontSize: Float, text: String):
|
||||||
|
|
||||||
private fun bestColWidthPt(
|
private fun bestColWidthPt(
|
||||||
tuoList: List<TimeUnitObject>,
|
tuoList: List<TimeUnitObject>,
|
||||||
font: org.apache.pdfbox.pdmodel.font.PDType0Font,
|
font: PDType0Font,
|
||||||
noteFontSize: Float,
|
noteFontSize: Float,
|
||||||
lyricFont: org.apache.pdfbox.pdmodel.font.PDType0Font,
|
lyricFont:PDType0Font,
|
||||||
lyricFontSize: Float
|
lyricFontSize: Float
|
||||||
): Float {
|
): Float {
|
||||||
var maxNoteW = 0f
|
var maxNoteW = 0f
|
||||||
|
|
@ -269,8 +269,12 @@ private fun drawNoteWithSubscript(
|
||||||
// ── drawHeader ────────────────────────────────────────────────────────────────
|
// ── drawHeader ────────────────────────────────────────────────────────────────
|
||||||
private fun drawHeader(
|
private fun drawHeader(
|
||||||
cs: PDPageContentStream,
|
cs: PDPageContentStream,
|
||||||
noteFont: PDType0Font, lyricFont: PDType0Font?,
|
noteFont:PDType0Font,
|
||||||
pageWidth: Float, pageHeight: Float, marginX: Float, usableWidth: Float,
|
lyricFont:PDType0Font?,
|
||||||
|
bolderFont: PDType0Font?,
|
||||||
|
emmentalerfFont: PDType0Font?,
|
||||||
|
pageWidth: Float, pageHeight: Float, marginX: Float, marginY: Float,
|
||||||
|
usableWidth: Float,
|
||||||
songTitle: String, songAuthor: String, songRhythm: String,
|
songTitle: String, songAuthor: String, songRhythm: String,
|
||||||
songComposer: String, songKey: String, measure: String,
|
songComposer: String, songKey: String, measure: String,
|
||||||
stanza: Int, nbStanza: Int
|
stanza: Int, nbStanza: Int
|
||||||
|
|
@ -278,46 +282,100 @@ private fun drawHeader(
|
||||||
val titleFont = lyricFont ?: noteFont
|
val titleFont = lyricFont ?: noteFont
|
||||||
val normalFont = lyricFont ?: noteFont
|
val normalFont = lyricFont ?: noteFont
|
||||||
val smallFont = noteFont
|
val smallFont = noteFont
|
||||||
|
val emmentalerFont = emmentalerfFont ?: noteFont
|
||||||
|
val boldFont = bolderFont ?: noteFont
|
||||||
|
|
||||||
fun tw(f: PDType0Font, sz: Float, t: String) =
|
fun tw(f: PDType0Font, sz: Float, t: String) =
|
||||||
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
|
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
|
||||||
|
|
||||||
|
// ── Y calculés depuis marginY au lieu de valeurs fixes ────────────────────
|
||||||
|
val titleY = pageHeight - marginY - 5f
|
||||||
|
val line2Y = pageHeight - marginY - 21f
|
||||||
|
val line3Y = pageHeight - marginY - 33f
|
||||||
|
val sepY = line3Y - 5f
|
||||||
|
|
||||||
// Ligne 1 — Titre centré
|
// Ligne 1 — Titre centré
|
||||||
val titleSize = 15f
|
val titleSize = 15f
|
||||||
val titleW = tw(titleFont, titleSize, songTitle)
|
val titleW = tw(titleFont, titleSize, songTitle)
|
||||||
cs.beginText(); cs.setFont(titleFont, titleSize)
|
cs.beginText(); cs.setFont(titleFont, titleSize)
|
||||||
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, pageHeight - 48f)
|
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, titleY)
|
||||||
cs.showText(songTitle.sanitize()); cs.endText()
|
cs.showText(songTitle.sanitize())
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
// Ligne 2 — Auteur | Compositeur
|
// Ligne 2 — Author | Composer (droite)
|
||||||
val line2Y = pageHeight - 64f; val sz2 = 9f
|
val sz2 = 9f
|
||||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||||
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
|
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
|
||||||
|
|
||||||
val composerW = tw(normalFont, sz2, songComposer)
|
val composerW = tw(normalFont, sz2, songComposer)
|
||||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
cs.beginText();
|
||||||
|
cs.setFont(normalFont, sz2)
|
||||||
cs.newLineAtOffset(marginX + usableWidth - composerW, line2Y)
|
cs.newLineAtOffset(marginX + usableWidth - composerW, line2Y)
|
||||||
cs.showText(songComposer.sanitize()); cs.endText()
|
cs.showText(songComposer.sanitize());
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
// Ligne 3 — Key | Measure | Rythme
|
// Ligne 3 — Key | Measure | Rhythm
|
||||||
val line3Y = pageHeight - 76f; val sz3 = 8f
|
val sz3 = 10f
|
||||||
val keyTxt = "Dô dia ${songKey.sanitize()}"
|
val keyTxt = "Dô dia "
|
||||||
cs.beginText(); cs.setFont(smallFont, sz3)
|
cs.beginText();
|
||||||
cs.newLineAtOffset(marginX, line3Y); cs.showText(keyTxt); cs.endText()
|
cs.setFont(smallFont, sz3)
|
||||||
|
cs.newLineAtOffset(marginX, line3Y);
|
||||||
|
cs.showText(keyTxt);
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
val keyW = tw(smallFont, sz3, keyTxt)
|
val keyW_2 = tw(smallFont, sz3, keyTxt)
|
||||||
cs.beginText(); cs.setFont(smallFont, sz3)
|
val keyTxt2 = "${songKey.sanitize()}"
|
||||||
cs.newLineAtOffset(marginX + keyW + 10f, line3Y)
|
cs.beginText(); cs.setFont(boldFont, sz3+2f)
|
||||||
cs.showText(measure.sanitize()); cs.endText()
|
cs.newLineAtOffset(marginX + keyW_2 + 3f, line3Y);
|
||||||
|
cs.showText(keyTxt2);
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
|
val keyW = tw(boldFont, sz3, keyTxt+keyTxt2) + 10f
|
||||||
|
val sz32 = 14f
|
||||||
|
val splitMeaRgx = """(\d+/\d+)\s+(.*)""".toRegex()
|
||||||
|
val matchRes = splitMeaRgx.matchEntire(measure)
|
||||||
|
var timeSignature = ""
|
||||||
|
var titleSignature = ""
|
||||||
|
if(matchRes != null) {
|
||||||
|
timeSignature = matchRes.groupValues[1]
|
||||||
|
titleSignature = matchRes.groupValues[2]
|
||||||
|
}
|
||||||
|
val timesig = timeSignature.toCharArray()
|
||||||
|
|
||||||
|
var ksW = keyW
|
||||||
|
timesig.forEach { c ->
|
||||||
|
cs.beginText();
|
||||||
|
if(c == '/') {
|
||||||
|
cs.setFont(normalFont, sz32)
|
||||||
|
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||||
|
ksW += tw(normalFont, sz32, c.toString())
|
||||||
|
} else {
|
||||||
|
cs.setFont(emmentalerFont, sz32)
|
||||||
|
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||||
|
ksW += tw(emmentalerFont, sz32, c.toString())
|
||||||
|
}
|
||||||
|
cs.showText(c.toString());
|
||||||
|
cs.endText()
|
||||||
|
}
|
||||||
|
cs.beginText();
|
||||||
|
cs.setFont(smallFont, sz3)
|
||||||
|
cs.newLineAtOffset(marginX + ksW + 15f, line3Y)
|
||||||
|
cs.showText("${titleSignature}");
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
|
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
|
||||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
cs.beginText();
|
||||||
|
cs.setFont(normalFont, sz2)
|
||||||
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
|
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
|
||||||
cs.showText(songRhythm.sanitize()); cs.endText()
|
cs.showText(songRhythm.sanitize());
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
// Ligne de séparation
|
// Ligne de séparation
|
||||||
cs.setStrokingColor(0.6f, 0.6f, 0.6f); cs.setLineWidth(0.4f)
|
cs.setStrokingColor(0.6f, 0.6f, 0.6f);
|
||||||
cs.moveTo(marginX, line3Y - 5f); cs.lineTo(marginX + usableWidth, line3Y - 5f); cs.stroke()
|
cs.setLineWidth(0.4f)
|
||||||
|
cs.moveTo(marginX, sepY);
|
||||||
|
cs.lineTo(marginX + usableWidth, sepY);
|
||||||
|
cs.stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── actual fun ────────────────────────────────────────────────────────────────
|
// ── actual fun ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -333,8 +391,8 @@ actual fun rememberPdfExportAction(
|
||||||
songAut: String,
|
songAut: String,
|
||||||
songComp: String,
|
songComp: String,
|
||||||
songRythm: String
|
songRythm: String
|
||||||
): () -> Unit {
|
): (PrintSettings) -> Unit {
|
||||||
return {
|
return { settings: PrintSettings ->
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
val (pdfBytes, computedFileName) = generatePdfToBytes(
|
val (pdfBytes, computedFileName) = generatePdfToBytes(
|
||||||
gridData, songTitle, measure, stanza,
|
gridData, songTitle, measure, stanza,
|
||||||
|
|
@ -360,7 +418,8 @@ private suspend fun generatePdfToBytes(
|
||||||
songKey: String,
|
songKey: String,
|
||||||
songAut: String,
|
songAut: String,
|
||||||
songComp: String,
|
songComp: String,
|
||||||
songRythm: String
|
songRythm: String,
|
||||||
|
settings: PrintSettings = defaultPrintSettings()
|
||||||
): Pair<ByteArray, String> {
|
): Pair<ByteArray, String> {
|
||||||
|
|
||||||
val tuoList = gridData.tuoList.drop(1)
|
val tuoList = gridData.tuoList.drop(1)
|
||||||
|
|
@ -370,20 +429,21 @@ private suspend fun generatePdfToBytes(
|
||||||
fun getVoicesFromPrefix(syl: String): List<Int> =
|
fun getVoicesFromPrefix(syl: String): List<Int> =
|
||||||
REGEX_PREFIX_DIGITS.findAll(syl).map { it.groupValues[1].toInt() }.toList()
|
REGEX_PREFIX_DIGITS.findAll(syl).map { it.groupValues[1].toInt() }.toList()
|
||||||
|
|
||||||
val markerHeight = 16f
|
val markerHeight = settings.markerHeight
|
||||||
val noteFontSize = 12.5f
|
val noteFontSize = settings.noteFontSize
|
||||||
val lyricFontSize = 13.25f
|
val lyricFontSize = settings.lyricFontSize
|
||||||
val noteLineH = 15f
|
val noteLineH = settings.noteLineH
|
||||||
val lyricLineH = 13f
|
val lyricLineH = settings.lyricLineH
|
||||||
val extraSylLineH = 14f
|
val extraSylLineH = settings.extraSylLineH
|
||||||
val adjustedMargin = 14f
|
val adjustedMargin = settings.adjustedMargin
|
||||||
val interRowGap = 16f
|
val interRowGap = settings.interRowGap
|
||||||
val marginX = 40f
|
val marginX = settings.marginX
|
||||||
val marginY = 50f
|
val marginY = settings.marginY
|
||||||
val headerH = 55f
|
val headerH = settings.headerH
|
||||||
val pageWidth = PDRectangle.A4.width
|
val pageWidth = settings.pageWidth
|
||||||
val pageHeight = PDRectangle.A4.height
|
val pageHeight = settings.pageHeight
|
||||||
val usableWidth = pageWidth - 2 * marginX
|
val usableWidth = pageWidth - 2 * marginX
|
||||||
|
val customNbGrid = settings.nbGridOnMeasure
|
||||||
|
|
||||||
// ── Polices ────────────────────────────────────────────────────────────────
|
// ── Polices ────────────────────────────────────────────────────────────────
|
||||||
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
|
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
|
||||||
|
|
@ -402,8 +462,12 @@ private suspend fun generatePdfToBytes(
|
||||||
|
|
||||||
// ── Largeur optimale de colonne ────────────────────────────────────────
|
// ── Largeur optimale de colonne ────────────────────────────────────────
|
||||||
val bestColW = bestColWidthPt(tuoList, customFont, noteFontSize, lyricFont, lyricFontSize)
|
val bestColW = bestColWidthPt(tuoList, customFont, noteFontSize, lyricFont, lyricFontSize)
|
||||||
val colCount = ((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
val colCount = if (customNbGrid == 0) {
|
||||||
val colWidth = bestColW
|
((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
||||||
|
} else {
|
||||||
|
customNbGrid
|
||||||
|
}
|
||||||
|
val colWidth = usableWidth / colCount
|
||||||
|
|
||||||
val maxSylsSize = tuoList.maxOfOrNull { tuo ->
|
val maxSylsSize = tuoList.maxOfOrNull { tuo ->
|
||||||
(1..nbStanza).maxOfOrNull { sz -> tuo.getSingleSyllable(sz).size } ?: 1
|
(1..nbStanza).maxOfOrNull { sz -> tuo.getSingleSyllable(sz).size } ?: 1
|
||||||
|
|
@ -411,7 +475,7 @@ private suspend fun generatePdfToBytes(
|
||||||
|
|
||||||
// ── Fonctions de dessin locales ────────────────────────────────────────
|
// ── Fonctions de dessin locales ────────────────────────────────────────
|
||||||
fun newPage(): PDPageContentStream {
|
fun newPage(): PDPageContentStream {
|
||||||
val page = PDPage(PDRectangle.A4)
|
val page = PDPage(settings.pdRectangle)
|
||||||
doc.addPage(page)
|
doc.addPage(page)
|
||||||
return PDPageContentStream(doc, page)
|
return PDPageContentStream(doc, page)
|
||||||
}
|
}
|
||||||
|
|
@ -451,27 +515,78 @@ private suspend fun generatePdfToBytes(
|
||||||
cs.restoreGraphicsState()
|
cs.restoreGraphicsState()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Fermata (dessin vectoriel — remplace le rendu glyphe Emmentaler) ──
|
fun drawFermata(cs: PDPageContentStream, x: Float, y: Float, scale: Float = 1f) {
|
||||||
fun drawFermata(cs: PDPageContentStream, x: Float, y: Float, width: Float = 10f, height: Float = 12f) {
|
fun px(svgX: Float) = x + svgX * scale
|
||||||
cs.setStrokingColor(0f, 0f, 0f); cs.setNonStrokingColor(0f, 0f, 0f)
|
fun py(svgY: Float) = y + svgY * scale
|
||||||
val circleRadius = width * 0.22f
|
|
||||||
val circleX = x + width / 2
|
fun curveTo(
|
||||||
val circleY = y + height * 0.25f
|
cp1x: Float, cp1y: Float,
|
||||||
val k = 0.5522847498f
|
cp2x: Float, cp2y: Float,
|
||||||
cs.moveTo(circleX + circleRadius, circleY)
|
ex: Float, ey: Float
|
||||||
cs.curveTo(circleX + circleRadius, circleY + circleRadius * k, circleX + circleRadius * k, circleY + circleRadius, circleX, circleY + circleRadius)
|
) = cs.curveTo(px(cp1x), py(cp1y), px(cp2x), py(cp2y), px(ex), py(ey))
|
||||||
cs.curveTo(circleX - circleRadius * k, circleY + circleRadius, circleX - circleRadius, circleY + circleRadius * k, circleX - circleRadius, circleY)
|
|
||||||
cs.curveTo(circleX - circleRadius, circleY - circleRadius * k, circleX - circleRadius * k, circleY - circleRadius, circleX, circleY - circleRadius)
|
fun drawEllipse(cx: Float, cy: Float, rx: Float, ry: Float) {
|
||||||
cs.curveTo(circleX + circleRadius * k, circleY - circleRadius, circleX + circleRadius, circleY - circleRadius * k, circleX + circleRadius, circleY)
|
val k = 0.5523f
|
||||||
|
val left = cx - rx; val right = cx + rx
|
||||||
|
val bottom = cy - ry; val top = cy + ry
|
||||||
|
|
||||||
|
cs.moveTo(px(right), py(cy))
|
||||||
|
// quart haut-droit
|
||||||
|
cs.curveTo(
|
||||||
|
px(right), py(cy + ry * k),
|
||||||
|
px(cx + rx * k), py(top),
|
||||||
|
px(cx), py(top)
|
||||||
|
)
|
||||||
|
// quart haut-gauche
|
||||||
|
cs.curveTo(
|
||||||
|
px(cx - rx * k), py(top),
|
||||||
|
px(left), py(cy + ry * k),
|
||||||
|
px(left), py(cy)
|
||||||
|
)
|
||||||
|
// quart bas-gauche
|
||||||
|
cs.curveTo(
|
||||||
|
px(left), py(cy - ry * k),
|
||||||
|
px(cx - rx * k), py(bottom),
|
||||||
|
px(cx), py(bottom)
|
||||||
|
)
|
||||||
|
// quart bas-droit
|
||||||
|
cs.curveTo(
|
||||||
|
px(cx + rx * k), py(bottom),
|
||||||
|
px(right), py(cy - ry * k),
|
||||||
|
px(right), py(cy)
|
||||||
|
)
|
||||||
|
cs.closePath()
|
||||||
|
}
|
||||||
|
|
||||||
|
cs.setNonStrokingColor(0f, 0f, 0f) // RGB noir
|
||||||
|
cs.moveTo(px(0.3034f), py(0.6922f))
|
||||||
|
|
||||||
|
// Dôme extérieur — côté gauche
|
||||||
|
curveTo(2.6605f, 16.6051f, 13.8570f, 30.7498f, 28.0000f, 30.7498f)
|
||||||
|
// Dôme extérieur — côté droit
|
||||||
|
curveTo(42.1430f, 30.7498f, 53.3395f, 16.6051f, 55.6966f, 0.6922f)
|
||||||
|
|
||||||
|
// Épaisseur bord droit
|
||||||
|
curveTo(55.8145f, 0.1028f, 52.8680f, 0.1028f, 52.7502f, 0.6922f)
|
||||||
|
|
||||||
|
// Dôme intérieur — côté droit
|
||||||
|
curveTo(49.8037f, 11.8901f, 42.1430f, 22.4987f, 28.0000f, 22.4987f)
|
||||||
|
// Dôme intérieur — côté gauche
|
||||||
|
curveTo(13.8570f, 22.4987f, 6.1963f, 11.8901f, 3.2498f, 0.6922f)
|
||||||
|
|
||||||
|
// Épaisseur bord gauche
|
||||||
|
curveTo(3.1320f, 0.1028f, 0.1855f, 0.1028f, 0.3034f, 0.6922f)
|
||||||
|
|
||||||
|
cs.closePath()
|
||||||
|
cs.fill()
|
||||||
|
|
||||||
|
drawEllipse(
|
||||||
|
cx = 28.0000f,
|
||||||
|
cy = 5.8492f,
|
||||||
|
rx = 5.5983f,
|
||||||
|
ry = 5.5990f
|
||||||
|
)
|
||||||
cs.fill()
|
cs.fill()
|
||||||
cs.setLineWidth(1.5f)
|
|
||||||
val arcStartX = x + width * 0.2f; val arcStartY = y + height * 0.55f
|
|
||||||
val arcPeakX = x + width / 2; val arcPeakY = y + height * 0.85f
|
|
||||||
val arcEndX = x + width * 0.8f; val arcEndY = y + height * 0.55f
|
|
||||||
cs.moveTo(arcStartX, arcStartY)
|
|
||||||
cs.curveTo(arcStartX + width * 0.15f, arcPeakY, arcPeakX - width * 0.15f, arcPeakY, arcPeakX, arcPeakY)
|
|
||||||
cs.curveTo(arcPeakX + width * 0.15f, arcPeakY, arcEndX - width * 0.15f, arcStartY, arcEndX, arcStartY)
|
|
||||||
cs.stroke()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── drawMarkers ────────────────────────────────────────────────────────
|
// ── drawMarkers ────────────────────────────────────────────────────────
|
||||||
|
|
@ -493,7 +608,7 @@ private suspend fun generatePdfToBytes(
|
||||||
|
|
||||||
// Point d'orgue → drawFermata (pas de GeneralPath sur Android)
|
// Point d'orgue → drawFermata (pas de GeneralPath sur Android)
|
||||||
if (markerText.contains("𝄐") || tuo.pTemplate.template.contains("𝄐")) {
|
if (markerText.contains("𝄐") || tuo.pTemplate.template.contains("𝄐")) {
|
||||||
drawFermata(cs, colX + colWidth / 2 - 5f, markerY - 2f, 10f, 12f)
|
drawFermata(cs, colX + colWidth / 2 - 5f, markerY - 2f, 0.3f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marqueur texte
|
// Marqueur texte
|
||||||
|
|
@ -540,8 +655,8 @@ private suspend fun generatePdfToBytes(
|
||||||
var y = pageHeight - marginY - headerH
|
var y = pageHeight - marginY - headerH
|
||||||
|
|
||||||
drawHeader(
|
drawHeader(
|
||||||
cs, customFont, lyricFont,
|
cs, customFont, lyricFont, markerBoldFont, markerEmmetFont,
|
||||||
pageWidth, pageHeight, marginX, usableWidth,
|
pageWidth, pageHeight, marginX, marginY, usableWidth,
|
||||||
songTitle = songTitle,
|
songTitle = songTitle,
|
||||||
songAuthor = songAut,
|
songAuthor = songAut,
|
||||||
songRhythm = songRythm,
|
songRhythm = songRythm,
|
||||||
|
|
@ -812,7 +927,7 @@ private suspend fun generatePdfToBytes(
|
||||||
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||||
|
|
||||||
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
// val isAllZeroSpacing = customSpacings.all { it == 0 }
|
||||||
|
|
||||||
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) {
|
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) {
|
||||||
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0)
|
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package mg.dot.feufaro.pdf
|
||||||
|
|
||||||
|
import com.tom_roush.pdfbox.pdmodel.common.PDRectangle
|
||||||
|
|
||||||
|
actual data class PrintSettings actual constructor(
|
||||||
|
actual val pageSize : PageSize,
|
||||||
|
actual val orientation : PageOrientation,
|
||||||
|
actual val markerHeight : Float,
|
||||||
|
actual val noteFontSize : Float,
|
||||||
|
actual val lyricFontSize : Float,
|
||||||
|
actual val noteLineH : Float,
|
||||||
|
actual val lyricLineH : Float,
|
||||||
|
actual val extraSylLineH : Float,
|
||||||
|
actual val adjustedMargin : Float,
|
||||||
|
actual val interRowGap : Float,
|
||||||
|
actual val marginX : Float,
|
||||||
|
actual val marginY : Float,
|
||||||
|
actual val headerH : Float,
|
||||||
|
actual val nbGridOnMeasure: Int
|
||||||
|
) {
|
||||||
|
val pdRectangle: PDRectangle
|
||||||
|
get() {
|
||||||
|
val base = when (pageSize) {
|
||||||
|
PageSize.A4 -> PDRectangle.A4
|
||||||
|
PageSize.A3 -> PDRectangle.A3
|
||||||
|
PageSize.LETTER -> PDRectangle.LETTER
|
||||||
|
}
|
||||||
|
return if (orientation == PageOrientation.PORTRAIT) base
|
||||||
|
else PDRectangle(base.height, base.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val pageWidth : Float get() = pdRectangle.width
|
||||||
|
actual val pageHeight : Float get() = pdRectangle.height
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun defaultPrintSettings(): PrintSettings = PrintSettings(
|
||||||
|
pageSize = PageSize.A4,
|
||||||
|
orientation = PageOrientation.PORTRAIT,
|
||||||
|
markerHeight = 16f,
|
||||||
|
noteFontSize = 12f,
|
||||||
|
lyricFontSize = 12.5f,
|
||||||
|
noteLineH = 17f,
|
||||||
|
lyricLineH = 15f,
|
||||||
|
extraSylLineH = 15f,
|
||||||
|
adjustedMargin = 14f,
|
||||||
|
interRowGap = 22f,
|
||||||
|
marginX = 42.5f,
|
||||||
|
marginY = 42.5f,
|
||||||
|
headerH = 55f,
|
||||||
|
nbGridOnMeasure = 0
|
||||||
|
)
|
||||||
|
|
@ -16,4 +16,4 @@ expect fun rememberPdfExportAction(
|
||||||
songAut: String,
|
songAut: String,
|
||||||
songComp: String,
|
songComp: String,
|
||||||
songRythm: String
|
songRythm: String
|
||||||
): () -> Unit
|
): (PrintSettings) -> Unit
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
package mg.dot.feufaro.pdf
|
||||||
|
|
||||||
|
enum class PageSize(val label: String) {
|
||||||
|
A4("A4"),
|
||||||
|
A3("A3"),
|
||||||
|
LETTER("Letter")
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PageOrientation(val label: String) {
|
||||||
|
PORTRAIT("Portrait"),
|
||||||
|
LANDSCAPE("Paysage")
|
||||||
|
}
|
||||||
|
|
||||||
|
expect class PrintSettings(
|
||||||
|
pageSize : PageSize,
|
||||||
|
orientation : PageOrientation,
|
||||||
|
markerHeight : Float,
|
||||||
|
noteFontSize : Float,
|
||||||
|
lyricFontSize : Float,
|
||||||
|
noteLineH : Float,
|
||||||
|
lyricLineH : Float,
|
||||||
|
extraSylLineH : Float,
|
||||||
|
adjustedMargin : Float,
|
||||||
|
interRowGap : Float,
|
||||||
|
marginX : Float,
|
||||||
|
marginY : Float,
|
||||||
|
headerH : Float,
|
||||||
|
nbGridOnMeasure : Int
|
||||||
|
) {
|
||||||
|
val pageSize : PageSize
|
||||||
|
val orientation : PageOrientation
|
||||||
|
val markerHeight : Float
|
||||||
|
val noteFontSize : Float
|
||||||
|
val lyricFontSize : Float
|
||||||
|
val noteLineH : Float
|
||||||
|
val lyricLineH : Float
|
||||||
|
val extraSylLineH : Float
|
||||||
|
val adjustedMargin : Float
|
||||||
|
val interRowGap : Float
|
||||||
|
val marginX : Float
|
||||||
|
val marginY : Float
|
||||||
|
val headerH : Float
|
||||||
|
val nbGridOnMeasure : Int
|
||||||
|
val pageWidth : Float
|
||||||
|
val pageHeight : Float
|
||||||
|
}
|
||||||
|
|
||||||
|
expect fun defaultPrintSettings(): PrintSettings
|
||||||
|
|
@ -560,7 +560,9 @@ fun LazyVerticalGridTUO(
|
||||||
val itemMinBaseWidth = bestTUOWidth(tuoList)
|
val itemMinBaseWidth = bestTUOWidth(tuoList)
|
||||||
val gridWidthDp = with(density) { gridWidthPx.toDp() }
|
val gridWidthDp = with(density) { gridWidthPx.toDp() }
|
||||||
|
|
||||||
val gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) {
|
val gridCount by sharedScreenModel.gridCount.collectAsState()
|
||||||
|
|
||||||
|
var gridColumnCount: Int = remember(gridWidthDp, itemMinBaseWidth, columnGroup) {
|
||||||
if (gridWidthDp == 0.dp) return@remember columnGroup
|
if (gridWidthDp == 0.dp) return@remember columnGroup
|
||||||
var calculatedCols =
|
var calculatedCols =
|
||||||
(gridWidthDp / (itemMinBaseWidth + horizontalArrangementSpacing)).toInt()
|
(gridWidthDp / (itemMinBaseWidth + horizontalArrangementSpacing)).toInt()
|
||||||
|
|
@ -572,6 +574,7 @@ fun LazyVerticalGridTUO(
|
||||||
val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup)
|
val actualColumnCount: Int = calculatedCols.coerceAtLeast(columnGroup)
|
||||||
actualColumnCount
|
actualColumnCount
|
||||||
}
|
}
|
||||||
|
gridColumnCount += gridCount
|
||||||
val flowRowSize: Float = 0.98f / gridColumnCount
|
val flowRowSize: Float = 0.98f / gridColumnCount
|
||||||
|
|
||||||
val currentStanza = viewModel.stanza
|
val currentStanza = viewModel.stanza
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,17 @@ 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.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
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import mg.dot.feufaro.data.getDrawerItems
|
import mg.dot.feufaro.data.getDrawerItems
|
||||||
|
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.pdf.rememberPdfExportAction
|
||||||
import mg.dot.feufaro.solfa.Solfa
|
import mg.dot.feufaro.solfa.Solfa
|
||||||
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
import mg.dot.feufaro.viewmodel.SolfaScreenModel
|
||||||
|
|
@ -111,7 +114,10 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
)
|
)
|
||||||
}, content = {
|
}, content = {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val pdfExportAction: () -> Unit = rememberPdfExportAction(
|
var showPrintSettings by remember { mutableStateOf(false) }
|
||||||
|
var pendingPrintSettings by remember { mutableStateOf<PrintSettings?>(null) }
|
||||||
|
|
||||||
|
val pdfExportAction: (PrintSettings) -> Unit = rememberPdfExportAction(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
fileRepository = solfaScreenModel.fileRepository,
|
fileRepository = solfaScreenModel.fileRepository,
|
||||||
gridData = sharedScreenModel.currentGridData,
|
gridData = sharedScreenModel.currentGridData,
|
||||||
|
|
@ -124,6 +130,17 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
songComp = songComposer,
|
songComp = songComposer,
|
||||||
songRythm = songRhythm
|
songRythm = songRhythm
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (showPrintSettings) {
|
||||||
|
PrintSettingsDialog(
|
||||||
|
initialSettings = defaultPrintSettings(),
|
||||||
|
onDismiss = { showPrintSettings = false },
|
||||||
|
onConfirm = { settings ->
|
||||||
|
showPrintSettings = false
|
||||||
|
pdfExportAction(settings)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
|
Scaffold(contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
modifier = Modifier.height(55.dp).windowInsetsPadding(WindowInsets.statusBars), title = {
|
||||||
|
|
@ -171,6 +188,42 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
|
modifier = Modifier.fillMaxWidth().padding(5.dp), horizontalAlignment = Alignment.End,
|
||||||
verticalArrangement = Arrangement.spacedBy(7.dp)
|
verticalArrangement = Arrangement.spacedBy(7.dp)
|
||||||
) {
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded and !showMidiCtrl,
|
||||||
|
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||||
|
exit = fadeOut() + scaleOut() + slideOutVertically { it / 2 }
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Column {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
sharedScreenModel.descGridCount(1)
|
||||||
|
}, modifier = Modifier.size(30.dp).alpha(0.45f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Remove,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
sharedScreenModel.addGridCount(1)
|
||||||
|
}, modifier = Modifier.size(30.dp).alpha(0.45f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = Color.Blue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded and !showMidiCtrl,
|
visible = isExpanded and !showMidiCtrl,
|
||||||
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
enter = fadeIn() + scaleIn() + slideInVertically { it / 2 },
|
||||||
|
|
@ -198,7 +251,7 @@ LaunchedEffect(isPlay, isPos) {
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
pdfExportAction()
|
showPrintSettings = !showPrintSettings
|
||||||
}, modifier = Modifier.alpha(0.45f)
|
}, modifier = Modifier.alpha(0.45f)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,653 @@
|
||||||
|
package mg.dot.feufaro.ui
|
||||||
|
|
||||||
|
|
||||||
|
import androidx.compose.foundation.*
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.PictureAsPdf
|
||||||
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.graphics.vector.PathParser
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import mg.dot.feufaro.pdf.PageOrientation
|
||||||
|
import mg.dot.feufaro.pdf.PageSize
|
||||||
|
import mg.dot.feufaro.pdf.PrintSettings
|
||||||
|
import mg.dot.feufaro.pdf.defaultPrintSettings
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PrintSettingsDialog(
|
||||||
|
initialSettings: PrintSettings = defaultPrintSettings(),
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (PrintSettings) -> Unit,
|
||||||
|
) {
|
||||||
|
// ── État local (copie mutable des paramètres) ─────────────────────────────
|
||||||
|
var pageSize by remember { mutableStateOf(initialSettings.pageSize) }
|
||||||
|
var orientation by remember { mutableStateOf(initialSettings.orientation) }
|
||||||
|
var noteFontSize by remember { mutableStateOf(initialSettings.noteFontSize) }
|
||||||
|
var lyricFontSize by remember { mutableStateOf(initialSettings.lyricFontSize) }
|
||||||
|
var markerHeight by remember { mutableStateOf(initialSettings.markerHeight) }
|
||||||
|
var noteLineH by remember { mutableStateOf(initialSettings.noteLineH) }
|
||||||
|
var lyricLineH by remember { mutableStateOf(initialSettings.lyricLineH) }
|
||||||
|
var extraSylLineH by remember { mutableStateOf(initialSettings.extraSylLineH) }
|
||||||
|
var adjustedMargin by remember { mutableStateOf(initialSettings.adjustedMargin) }
|
||||||
|
var interRowGap by remember { mutableStateOf(initialSettings.interRowGap) }
|
||||||
|
var marginX by remember { mutableStateOf(initialSettings.marginX) }
|
||||||
|
var marginY by remember { mutableStateOf(initialSettings.marginY) }
|
||||||
|
var headerH by remember { mutableStateOf(initialSettings.headerH) }
|
||||||
|
var nbGridOnMeasure by remember { mutableStateOf(initialSettings.nbGridOnMeasure) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.92f)
|
||||||
|
.fillMaxHeight(0.92f),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
tonalElevation = 8.dp,
|
||||||
|
color = MaterialTheme.colorScheme.surface
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
||||||
|
// ── Barre de titre ────────────────────────────────────────────
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.primary)
|
||||||
|
.padding(horizontal = 20.dp, vertical = 14.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Paramètres d'impression",
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
IconButton(onClick = onDismiss) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.Close,
|
||||||
|
contentDescription = "Fermer",
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Corps scrollable en deux colonnes ─────────────────────────
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(24.dp)
|
||||||
|
) {
|
||||||
|
// ── Colonne gauche : Format papier + Preview ──────────────
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
SectionTitle("Format de page")
|
||||||
|
|
||||||
|
Row{
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(0.7f)
|
||||||
|
) {
|
||||||
|
LabelRow("Nombre de colonne d'une portée")
|
||||||
|
}
|
||||||
|
Column {
|
||||||
|
TextField(
|
||||||
|
value = if (nbGridOnMeasure == 0) "" else nbGridOnMeasure.toString(),
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
val number = newValue.toIntOrNull()?.coerceAtMost(25)
|
||||||
|
if (number != null) {
|
||||||
|
nbGridOnMeasure = number
|
||||||
|
} else if (newValue.isEmpty()) {
|
||||||
|
nbGridOnMeasure = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.height(50.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Taille papier
|
||||||
|
LabelRow("Taille du papier")
|
||||||
|
SegmentedRow(
|
||||||
|
options = PageSize.entries.map { it.label },
|
||||||
|
selected = pageSize.ordinal,
|
||||||
|
onSelect = { pageSize = PageSize.entries[it] }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Orientation
|
||||||
|
LabelRow("Orientation")
|
||||||
|
SegmentedRow(
|
||||||
|
options = PageOrientation.entries.map { it.label },
|
||||||
|
selected = orientation.ordinal,
|
||||||
|
onSelect = { orientation = PageOrientation.entries[it] }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// ── Preview miniature de la page ──────────────────────
|
||||||
|
SectionTitle("Aperçu")
|
||||||
|
val currentSettings = PrintSettings(
|
||||||
|
pageSize = pageSize,
|
||||||
|
orientation = orientation,
|
||||||
|
noteFontSize = noteFontSize,
|
||||||
|
lyricFontSize = lyricFontSize,
|
||||||
|
markerHeight = markerHeight,
|
||||||
|
noteLineH = noteLineH,
|
||||||
|
lyricLineH = lyricLineH,
|
||||||
|
extraSylLineH = extraSylLineH,
|
||||||
|
adjustedMargin = adjustedMargin,
|
||||||
|
interRowGap = interRowGap,
|
||||||
|
marginX = marginX,
|
||||||
|
marginY = marginY,
|
||||||
|
headerH = headerH,
|
||||||
|
nbGridOnMeasure = nbGridOnMeasure
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
PagePreview(
|
||||||
|
settings = currentSettings,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(250.dp) // Hauteur fixe demandée
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Colonne droite : Paramètres typographiques / marges ───
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
SectionTitle("Typographie")
|
||||||
|
FloatSlider(
|
||||||
|
label = "Taille fonte notes",
|
||||||
|
value = noteFontSize,
|
||||||
|
range = 8f..18f,
|
||||||
|
unit = "pt"
|
||||||
|
) { noteFontSize = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Taille fonte paroles",
|
||||||
|
value = lyricFontSize,
|
||||||
|
range = 8f..18f,
|
||||||
|
unit = "pt"
|
||||||
|
) { lyricFontSize = it }
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
SectionTitle("Espacements")
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Hauteur zone marqueurs",
|
||||||
|
value = markerHeight,
|
||||||
|
range = 8f..32f,
|
||||||
|
unit = "mm"
|
||||||
|
) { markerHeight = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Espace entre voix",
|
||||||
|
value = noteLineH,
|
||||||
|
range = 10f..34f,
|
||||||
|
unit = "mm"
|
||||||
|
) { noteLineH = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Espace lignes paroles restantes",
|
||||||
|
value = lyricLineH,
|
||||||
|
range = 10f..30f,
|
||||||
|
unit = "mm"
|
||||||
|
) { lyricLineH = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Espace syl0 / syl1+",
|
||||||
|
value = extraSylLineH,
|
||||||
|
range = 8f..30f,
|
||||||
|
unit = "mm"
|
||||||
|
) { extraSylLineH = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Marge ajustée syl0",
|
||||||
|
value = adjustedMargin,
|
||||||
|
range = 4f..30f,
|
||||||
|
unit = "mm"
|
||||||
|
) { adjustedMargin = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Écart entre portées",
|
||||||
|
value = interRowGap,
|
||||||
|
range = 10f..60f,
|
||||||
|
unit = "mm"
|
||||||
|
) { interRowGap = it }
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
SectionTitle("Marges")
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Marge gauche/droite",
|
||||||
|
value = marginX,
|
||||||
|
range = 15f..80f,
|
||||||
|
unit = "mm"
|
||||||
|
) { marginX = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Marge haut/bas",
|
||||||
|
value = marginY,
|
||||||
|
range = 15f..80f,
|
||||||
|
unit = "mm"
|
||||||
|
) { marginY = it }
|
||||||
|
|
||||||
|
FloatSlider(
|
||||||
|
label = "Hauteur en-tête",
|
||||||
|
value = headerH,
|
||||||
|
range = 49f..90f,
|
||||||
|
unit = "mm"
|
||||||
|
) { headerH = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Boutons bas ───────────────────────────────────────────────
|
||||||
|
HorizontalDivider()
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 24.dp, vertical = 14.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.End),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
// Réinitialiser
|
||||||
|
TextButton(onClick = {
|
||||||
|
val def = defaultPrintSettings()
|
||||||
|
pageSize = def.pageSize
|
||||||
|
orientation = def.orientation
|
||||||
|
noteFontSize = def.noteFontSize
|
||||||
|
lyricFontSize = def.lyricFontSize
|
||||||
|
markerHeight = def.markerHeight
|
||||||
|
noteLineH = def.noteLineH
|
||||||
|
lyricLineH = def.lyricLineH
|
||||||
|
extraSylLineH = def.extraSylLineH
|
||||||
|
adjustedMargin = def.adjustedMargin
|
||||||
|
interRowGap = def.interRowGap
|
||||||
|
marginX = def.marginX
|
||||||
|
marginY = def.marginY
|
||||||
|
headerH = def.headerH
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RestartAlt,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
OutlinedButton(onClick = onDismiss) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
onConfirm(
|
||||||
|
PrintSettings(
|
||||||
|
pageSize = pageSize,
|
||||||
|
orientation = orientation,
|
||||||
|
noteFontSize = noteFontSize,
|
||||||
|
lyricFontSize = lyricFontSize,
|
||||||
|
markerHeight = markerHeight,
|
||||||
|
noteLineH = noteLineH,
|
||||||
|
lyricLineH = lyricLineH,
|
||||||
|
extraSylLineH = extraSylLineH,
|
||||||
|
adjustedMargin = adjustedMargin,
|
||||||
|
interRowGap = interRowGap,
|
||||||
|
marginX = marginX,
|
||||||
|
marginY = marginY,
|
||||||
|
headerH = headerH,
|
||||||
|
nbGridOnMeasure = nbGridOnMeasure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PictureAsPdf,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Composants internes ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SectionTitle(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(bottom = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LabelRow(text: String) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SegmentedRow(
|
||||||
|
options: List<String>,
|
||||||
|
selected: Int,
|
||||||
|
onSelect: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
options.forEachIndexed { index, label ->
|
||||||
|
val isSelected = index == selected
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.background(
|
||||||
|
if (isSelected) MaterialTheme.colorScheme.primary
|
||||||
|
else Color.Transparent
|
||||||
|
)
|
||||||
|
.clickable { onSelect(index) }
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.onPrimary
|
||||||
|
else MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slider flottant avec label + valeur affichée.
|
||||||
|
*/
|
||||||
|
fun Float.toMm(): Float {
|
||||||
|
return this * 0.3528f
|
||||||
|
}
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun FloatSlider(
|
||||||
|
label: String,
|
||||||
|
value: Float,
|
||||||
|
range: ClosedFloatingPointRange<Float>,
|
||||||
|
unit: String,
|
||||||
|
onValueChange: (Float) -> Unit
|
||||||
|
) {
|
||||||
|
val displayValue = if (unit == "mm") value.toMm() else value
|
||||||
|
val roundedDisplayValue = (displayValue * 2).roundToInt() / 2f
|
||||||
|
val primaryTeal = Color(0xFF009688)
|
||||||
|
val lightTeal = Color(0xFFB2DFDB)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
// 1. Label de description fixe à gauche
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. Zone du Slider avec la valeur flottante au-dessus
|
||||||
|
BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
val widthPx = constraints.maxWidth.toFloat()
|
||||||
|
|
||||||
|
// Calcul de la position X du texte pour suivre le thumb
|
||||||
|
// On normalise la valeur entre 0.0 et 1.0
|
||||||
|
val fraction = (value - range.start) / (range.endInclusive - range.start)
|
||||||
|
val textOffset = (widthPx * fraction)
|
||||||
|
|
||||||
|
// Affichage de la valeur qui "flotte" au-dessus du thumb
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(20.dp) // Espace pour le texte
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "$roundedDisplayValue $unit",
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = primaryTeal,
|
||||||
|
modifier = Modifier.graphicsLayer() {
|
||||||
|
translationX = textOffset - (30.dp.toPx()) // Ajustement pour centrer le texte
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Slider(
|
||||||
|
value = value,
|
||||||
|
onValueChange = { onValueChange((it * 2).roundToInt() / 2f) },
|
||||||
|
valueRange = range,
|
||||||
|
steps = (((range.endInclusive - range.start) / 0.5f).toInt() - 1).coerceAtLeast(0),
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 15.dp), // Décale le slider sous le texte
|
||||||
|
thumb = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(15.dp)
|
||||||
|
.background(primaryTeal, CircleShape)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
track = { sliderState ->
|
||||||
|
SliderDefaults.Track(
|
||||||
|
sliderState = sliderState,
|
||||||
|
modifier = Modifier.height(8.dp),
|
||||||
|
colors = SliderDefaults.colors(
|
||||||
|
activeTrackColor = primaryTeal,
|
||||||
|
inactiveTrackColor = lightTeal
|
||||||
|
),
|
||||||
|
drawStopIndicator = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Mini preview de la page
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun PagePreview(
|
||||||
|
settings: PrintSettings,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val isLandscape = settings.orientation == PageOrientation.LANDSCAPE
|
||||||
|
val ratio = if (isLandscape) 1.414f else 0.707f
|
||||||
|
val baseScale = 200f / 595f // Échelle pour l'affichage
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.aspectRatio(ratio)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(Color.White)
|
||||||
|
.border(1.dp, Color.Gray.copy(alpha = 0.3f), RoundedCornerShape(4.dp)),
|
||||||
|
contentAlignment = Alignment.TopStart
|
||||||
|
) {
|
||||||
|
// Conversion des unités en DP
|
||||||
|
val mx = (settings.marginX * baseScale).dp
|
||||||
|
val my = (settings.marginY * baseScale).dp
|
||||||
|
val hH = (settings.headerH * baseScale).dp
|
||||||
|
val nH = (settings.noteLineH * baseScale).dp // Espace entre 2 voix
|
||||||
|
val iG = (settings.interRowGap * baseScale).dp // Entre portées
|
||||||
|
val mH = (settings.markerHeight * baseScale).dp // Zone orange
|
||||||
|
val aM = (settings.adjustedMargin * baseScale).dp // Marge syl0
|
||||||
|
val eS = (settings.extraSylLineH * baseScale).dp // Espace syl0/syl1
|
||||||
|
val pR = (settings.lyricLineH * baseScale).dp // Espace syl0/syl1
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = mx, vertical = my)
|
||||||
|
) {
|
||||||
|
// 1. HEADER
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(hH)
|
||||||
|
.border(0.5.dp, Color.Red.copy(alpha = 0.5f))
|
||||||
|
.background(Color.Red.copy(alpha = 0.05f))
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(iG / 2))
|
||||||
|
|
||||||
|
// 2. PORTÉES
|
||||||
|
val repeatedPort = if(settings.orientation == PageOrientation.LANDSCAPE) 2 else 3
|
||||||
|
repeat(repeatedPort) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(0.5.dp, Color(0xFF2ECC71).copy(alpha = 0.4f)) // Cadre Vert
|
||||||
|
.padding(2.dp)
|
||||||
|
) {
|
||||||
|
// Ligne des Marqueurs
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(mH / 2)
|
||||||
|
.border(0.5.dp, Color(0xFFE67E22)) // Orange
|
||||||
|
.background(Color(0xFFE67E22).copy(alpha = 0.1f))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lignes des Notes par voix
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
AccoladeShape(modifier = Modifier.width(6.dp).height(nH * 2.5f))
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f).padding(start = 2.dp)) {
|
||||||
|
repeat(4) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.9f)
|
||||||
|
.height(nH / 3)
|
||||||
|
.border(0.3.dp, Color(0xFF00BCD4)) // Bleu Ciel
|
||||||
|
.background(Color(0xFF00BCD4).copy(alpha = 0.1f))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(nH / 6))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paroles Syl0 et Syl1
|
||||||
|
Spacer(modifier = Modifier.height(aM / 2))
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(0.85f)
|
||||||
|
.border(0.5.dp, Color(0xFF9B59B6)) // Violet
|
||||||
|
.padding(1.dp)
|
||||||
|
) {
|
||||||
|
// Syl0
|
||||||
|
Box(modifier = Modifier.fillMaxWidth().height(2.dp).background(Color(0xFF9B59B6).copy(alpha = 0.3f)))
|
||||||
|
Spacer(modifier = Modifier.height(eS / 4))
|
||||||
|
// Syl1
|
||||||
|
Box(modifier = Modifier.fillMaxWidth(0.9f).height(2.dp).background(Color(0xFF9B59B6).copy(alpha = 0.3f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(iG))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. PAROLES RESTANTES
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.border(0.5.dp, Color.Red.copy(alpha = 0.5f))
|
||||||
|
.background(Color.Red.copy(alpha = 0.05f))
|
||||||
|
.padding(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
repeat(4) { index ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(if (index % 2 == 0) 0.95f else 0.85f)
|
||||||
|
.height(2.dp)
|
||||||
|
.background(Color.Red.copy(alpha = 0.2f))
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(pR / 4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccoladeShape(modifier: Modifier) {
|
||||||
|
Canvas(modifier = modifier) {
|
||||||
|
// Le path que tu as fourni (normalisé sur une grille de 24x24 environ)
|
||||||
|
val pathData = "M14 21C12.8954 21 12 20.1046 12 19V15.3255C12 14.8363 12 14.5917 11.9447 14.3615C11.8957 14.1575 11.8149 13.9624 11.7053 13.7834C11.5816 13.5816 11.4086 13.4086 11.0627 13.0627L10 12L11.0627 10.9373C11.4086 10.5914 11.5816 10.4184 11.7053 10.2166C11.8149 10.0376 11.8957 9.84254 11.9447 9.63846C12 9.40829 12 9.1637 12 8.67452V5C12 3.89543 12.8954 3 14 3"
|
||||||
|
|
||||||
|
val path = PathParser().parsePathString(pathData).toPath()
|
||||||
|
val pathBounds = path.getBounds()
|
||||||
|
val scaleX = size.width / pathBounds.width
|
||||||
|
val scaleY = size.height / pathBounds.height
|
||||||
|
|
||||||
|
drawContext.canvas.save()
|
||||||
|
drawContext.canvas.scale(scaleX, scaleY)
|
||||||
|
drawContext.canvas.translate(-pathBounds.left, -pathBounds.top)
|
||||||
|
|
||||||
|
drawPath(
|
||||||
|
path = path,
|
||||||
|
color = Color.Black,
|
||||||
|
style = androidx.compose.ui.graphics.drawscope.Stroke(
|
||||||
|
width = 0.552f,
|
||||||
|
cap = androidx.compose.ui.graphics.StrokeCap.Round,
|
||||||
|
join = androidx.compose.ui.graphics.StrokeJoin.Round
|
||||||
|
)
|
||||||
|
)
|
||||||
|
drawContext.canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -183,6 +183,20 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_activeIndex.value = clamped
|
_activeIndex.value = clamped
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private val _gridCount = MutableStateFlow(0)
|
||||||
|
val gridCount: StateFlow<Int> = _gridCount.asStateFlow()
|
||||||
|
|
||||||
|
fun addGridCount(nbGrid: Int) {
|
||||||
|
_gridCount.value += nbGrid
|
||||||
|
}
|
||||||
|
fun descGridCount(nbGrid: Int) {
|
||||||
|
if(_gridCount.value > 0) {
|
||||||
|
_gridCount.value -= nbGrid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun resetGridCount() {
|
||||||
|
_gridCount.value = 0
|
||||||
|
}
|
||||||
fun seekToGrid(gridIndex: Int) {
|
fun seekToGrid(gridIndex: Int) {
|
||||||
_mediaPlayer?.seekToGrid(gridIndex)
|
_mediaPlayer?.seekToGrid(gridIndex)
|
||||||
}
|
}
|
||||||
|
|
@ -327,6 +341,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
fun loadNewSong(newMidiFile: String) {
|
fun loadNewSong(newMidiFile: String) {
|
||||||
_mediaPlayer?.stop()
|
_mediaPlayer?.stop()
|
||||||
_mediaPlayer?.release()
|
_mediaPlayer?.release()
|
||||||
|
_stanza.value = 1
|
||||||
_mediaPlayer = null
|
_mediaPlayer = null
|
||||||
_isPos.value = true
|
_isPos.value = true
|
||||||
_isPlay.value = false
|
_isPlay.value = false
|
||||||
|
|
@ -436,6 +451,7 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
_tuoTimestamps.value = emptyList()
|
_tuoTimestamps.value = emptyList()
|
||||||
updateSearchTxt("")
|
updateSearchTxt("")
|
||||||
tempTimeUnitObjectList.clear()
|
tempTimeUnitObjectList.clear()
|
||||||
|
resetGridCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun lastTUO(): TimeUnitObject? {
|
fun lastTUO(): TimeUnitObject? {
|
||||||
|
|
@ -464,7 +480,6 @@ class SharedScreenModel(private val fileRepository: FileRepository) : ScreenMode
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
_stanza.value = 0
|
_stanza.value = 0
|
||||||
}
|
}
|
||||||
loadNewSong("whawyd3.mid")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSongKey(theSongKey: String) {
|
fun setSongKey(theSongKey: String) {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import org.apache.pdfbox.io.RandomAccessReadBuffer
|
||||||
import org.apache.pdfbox.pdmodel.PDDocument
|
import org.apache.pdfbox.pdmodel.PDDocument
|
||||||
import org.apache.pdfbox.pdmodel.PDPage
|
import org.apache.pdfbox.pdmodel.PDPage
|
||||||
import org.apache.pdfbox.pdmodel.PDPageContentStream
|
import org.apache.pdfbox.pdmodel.PDPageContentStream
|
||||||
import org.apache.pdfbox.pdmodel.common.PDRectangle
|
|
||||||
import org.apache.pdfbox.pdmodel.font.PDType0Font
|
import org.apache.pdfbox.pdmodel.font.PDType0Font
|
||||||
import org.apache.pdfbox.util.Matrix
|
import org.apache.pdfbox.util.Matrix
|
||||||
import java.awt.Color
|
import java.awt.Color
|
||||||
|
|
@ -388,7 +387,6 @@ private fun drawTUOUnderlines(
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Y ── identique à Compose : voiceNumber * totalH / nbNotes → bas de la ligne de voix
|
// ── Y ── identique à Compose : voiceNumber * totalH / nbNotes → bas de la ligne de voix
|
||||||
// En PDFBox Y croît vers le haut donc on descend de 2f sous la baseline
|
|
||||||
// val underlineY = y - voiceLineIndex * noteLineH - 2f
|
// val underlineY = y - voiceLineIndex * noteLineH - 2f
|
||||||
|
|
||||||
if (xEnd > xStart) {
|
if (xEnd > xStart) {
|
||||||
|
|
@ -450,12 +448,17 @@ private fun drawNoteWithSubscript(
|
||||||
val ch = text[i]
|
val ch = text[i]
|
||||||
val str = ch.toString()
|
val str = ch.toString()
|
||||||
cs.beginText()
|
cs.beginText()
|
||||||
cs.setFont(font, fontSize)
|
when(ch) {
|
||||||
|
'₁', '₂', '₃', '¹', '²', '³' -> {
|
||||||
|
cs.setFont(font, fontSize * 0.6f)
|
||||||
|
} else -> {
|
||||||
|
cs.setFont(font, fontSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
cs.newLineAtOffset(curX, y)
|
cs.newLineAtOffset(curX, y)
|
||||||
cs.showText(str.sanitize())
|
cs.showText(str.sanitize())
|
||||||
cs.endText()
|
cs.endText()
|
||||||
curX += font.getStringWidth(str.sanitize()) / 1000f * fontSize
|
curX += font.getStringWidth(str.sanitize()) / 1000f * fontSize
|
||||||
// }
|
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -465,7 +468,10 @@ private fun drawHeader(
|
||||||
cs: PDPageContentStream,
|
cs: PDPageContentStream,
|
||||||
noteFont: PDType0Font,
|
noteFont: PDType0Font,
|
||||||
lyricFont: PDType0Font?,
|
lyricFont: PDType0Font?,
|
||||||
pageWidth: Float, pageHeight: Float, marginX: Float, usableWidth: Float,
|
bolderFont: PDType0Font?,
|
||||||
|
emmentalerfFont: PDType0Font?,
|
||||||
|
pageWidth: Float, pageHeight: Float, marginX: Float, marginY: Float,
|
||||||
|
usableWidth: Float,
|
||||||
songTitle: String, songAuthor: String, songRhythm: String,
|
songTitle: String, songAuthor: String, songRhythm: String,
|
||||||
songComposer: String, songKey: String, measure: String,
|
songComposer: String, songKey: String, measure: String,
|
||||||
stanza: Int, nbStanza: Int
|
stanza: Int, nbStanza: Int
|
||||||
|
|
@ -473,20 +479,28 @@ private fun drawHeader(
|
||||||
val titleFont = lyricFont ?: noteFont
|
val titleFont = lyricFont ?: noteFont
|
||||||
val normalFont = lyricFont ?: noteFont
|
val normalFont = lyricFont ?: noteFont
|
||||||
val smallFont = noteFont
|
val smallFont = noteFont
|
||||||
|
val emmentalerFont = emmentalerfFont ?: noteFont
|
||||||
|
val boldFont = bolderFont ?: noteFont
|
||||||
|
|
||||||
fun tw(f: PDType0Font, sz: Float, t: String) =
|
fun tw(f: PDType0Font, sz: Float, t: String) =
|
||||||
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
|
try { f.getStringWidth(t.sanitize()) / 1000f * sz } catch (e: Exception) { 0f }
|
||||||
|
|
||||||
|
// ── Y calculés depuis marginY au lieu de valeurs fixes ────────────────────
|
||||||
|
val titleY = pageHeight - marginY - 5f
|
||||||
|
val line2Y = pageHeight - marginY - 21f
|
||||||
|
val line3Y = pageHeight - marginY - 33f
|
||||||
|
val sepY = line3Y - 5f
|
||||||
|
|
||||||
// Ligne 1 — Titre centré
|
// Ligne 1 — Titre centré
|
||||||
val titleSize = 15f
|
val titleSize = 15f
|
||||||
val titleW = tw(titleFont, titleSize, songTitle)
|
val titleW = tw(titleFont, titleSize, songTitle)
|
||||||
cs.beginText(); cs.setFont(titleFont, titleSize)
|
cs.beginText(); cs.setFont(titleFont, titleSize)
|
||||||
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, pageHeight - 48f)
|
cs.newLineAtOffset(marginX + (usableWidth - titleW) / 2f, titleY)
|
||||||
cs.showText(songTitle.sanitize());
|
cs.showText(songTitle.sanitize())
|
||||||
cs.endText()
|
cs.endText()
|
||||||
|
|
||||||
// Ligne 2 — Author | Composer (droite)
|
// Ligne 2 — Author | Composer (droite)
|
||||||
val line2Y = pageHeight - 64f; val sz2 = 9f
|
val sz2 = 9f
|
||||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
cs.beginText(); cs.setFont(normalFont, sz2)
|
||||||
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
|
cs.newLineAtOffset(marginX, line2Y); cs.showText(songAuthor.sanitize()); cs.endText()
|
||||||
|
|
||||||
|
|
@ -497,35 +511,71 @@ private fun drawHeader(
|
||||||
cs.showText(songComposer.sanitize());
|
cs.showText(songComposer.sanitize());
|
||||||
cs.endText()
|
cs.endText()
|
||||||
|
|
||||||
// Ligne 3 — Key | Measure | Rythm Stanza (droite)
|
// Ligne 3 — Key | Measure | Rhythm
|
||||||
val line3Y = pageHeight - 76f; val sz3 = 8f
|
val sz3 = 10f
|
||||||
val keyTxt = "Dô dia ${songKey.sanitize()}"
|
val keyTxt = "Dô dia "
|
||||||
cs.beginText();
|
cs.beginText();
|
||||||
cs.setFont(smallFont, sz3)
|
cs.setFont(smallFont, sz3)
|
||||||
cs.newLineAtOffset(marginX, line3Y); cs.showText(keyTxt);
|
cs.newLineAtOffset(marginX, line3Y);
|
||||||
|
cs.showText(keyTxt);
|
||||||
cs.endText()
|
cs.endText()
|
||||||
|
|
||||||
val keyW = tw(smallFont, sz3, keyTxt)
|
val keyW_2 = tw(smallFont, sz3, keyTxt)
|
||||||
val measureTxt = "${measure.sanitize()}"
|
val keyTxt2 = "${songKey.sanitize()}"
|
||||||
|
cs.beginText(); cs.setFont(boldFont, sz3+2f)
|
||||||
|
cs.newLineAtOffset(marginX + keyW_2 + 3f, line3Y);
|
||||||
|
cs.showText(keyTxt2);
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
|
val keyW = tw(boldFont, sz3, keyTxt+keyTxt2) + 10f
|
||||||
|
val sz32 = 14f
|
||||||
|
val splitMeaRgx = """(\d+/\d+)\s+(.*)""".toRegex()
|
||||||
|
val matchRes = splitMeaRgx.matchEntire(measure)
|
||||||
|
var timeSignature = ""
|
||||||
|
var titleSignature = ""
|
||||||
|
if(matchRes != null) {
|
||||||
|
timeSignature = matchRes.groupValues[1]
|
||||||
|
titleSignature = matchRes.groupValues[2]
|
||||||
|
}
|
||||||
|
val timesig = timeSignature.toCharArray()
|
||||||
|
|
||||||
|
var ksW = keyW
|
||||||
|
timesig.forEach { c ->
|
||||||
|
cs.beginText();
|
||||||
|
if(c == '/') {
|
||||||
|
cs.setFont(normalFont, sz32)
|
||||||
|
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||||
|
ksW += tw(normalFont, sz32, c.toString())
|
||||||
|
} else {
|
||||||
|
cs.setFont(emmentalerFont, sz32)
|
||||||
|
cs.newLineAtOffset(marginX + ksW + 10f, line3Y)
|
||||||
|
ksW += tw(emmentalerFont, sz32, c.toString())
|
||||||
|
}
|
||||||
|
cs.showText(c.toString());
|
||||||
|
cs.endText()
|
||||||
|
}
|
||||||
cs.beginText();
|
cs.beginText();
|
||||||
cs.setFont(smallFont, sz3)
|
cs.setFont(smallFont, sz3)
|
||||||
cs.newLineAtOffset(marginX + keyW + 10f, line3Y)
|
cs.newLineAtOffset(marginX + ksW + 15f, line3Y)
|
||||||
cs.showText("${measure.sanitize()}");
|
cs.showText("${titleSignature}");
|
||||||
cs.endText()
|
cs.endText()
|
||||||
|
|
||||||
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
|
val rhythmW = tw(normalFont, sz3, songRhythm.sanitize())
|
||||||
cs.beginText(); cs.setFont(normalFont, sz2)
|
cs.beginText();
|
||||||
|
cs.setFont(normalFont, sz2)
|
||||||
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
|
cs.newLineAtOffset(marginX + (usableWidth - rhythmW) / 2f, line3Y)
|
||||||
cs.showText(songRhythm.sanitize()); cs.endText()
|
cs.showText(songRhythm.sanitize());
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
// Ligne de séparation
|
// Ligne de séparation
|
||||||
cs.setStrokingColor(0.6f, 0.6f, 0.6f); cs.setLineWidth(0.4f)
|
cs.setStrokingColor(0.6f, 0.6f, 0.6f);
|
||||||
cs.moveTo(marginX, line3Y - 5f); cs.lineTo(marginX + usableWidth, line3Y - 5f); cs.stroke()
|
cs.setLineWidth(0.4f)
|
||||||
|
cs.moveTo(marginX, sepY);
|
||||||
|
cs.lineTo(marginX + usableWidth, sepY);
|
||||||
|
cs.stroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
actual fun rememberPdfExportAction(
|
actual fun rememberPdfExportAction(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
fileRepository: FileRepository,
|
fileRepository: FileRepository,
|
||||||
|
|
@ -538,13 +588,14 @@ actual fun rememberPdfExportAction(
|
||||||
songAut: String,
|
songAut: String,
|
||||||
songComp: String,
|
songComp: String,
|
||||||
songRythm: String
|
songRythm: String
|
||||||
): () -> Unit {
|
): (PrintSettings) -> Unit {
|
||||||
return {
|
return { settings: PrintSettings ->
|
||||||
scope.launch(Dispatchers.IO) {
|
scope.launch(Dispatchers.IO) {
|
||||||
// 1. Générer le PDF en mémoire (ByteArray)
|
// 1. Générer le PDF en mémoire (ByteArray)
|
||||||
val (pdfBytes, computedFileName) = generatePdfToBytes(
|
val (pdfBytes, computedFileName) = generatePdfToBytes(
|
||||||
gridData, songTitle, measure, stanza,
|
gridData, songTitle, measure, stanza,
|
||||||
nbStanza, songKey, songAut, songComp, songRythm
|
nbStanza, songKey, songAut, songComp, songRythm,
|
||||||
|
settings
|
||||||
)
|
)
|
||||||
val chosenPath = withContext(Dispatchers.Main) {
|
val chosenPath = withContext(Dispatchers.Main) {
|
||||||
fileRepository.pickSavePath(computedFileName)
|
fileRepository.pickSavePath(computedFileName)
|
||||||
|
|
@ -565,7 +616,8 @@ private suspend fun generatePdfToBytes(
|
||||||
songKey: String,
|
songKey: String,
|
||||||
songAut: String,
|
songAut: String,
|
||||||
songComp: String,
|
songComp: String,
|
||||||
songRythm: String
|
songRythm: String,
|
||||||
|
settings: PrintSettings = defaultPrintSettings()
|
||||||
): Pair<ByteArray, String> {
|
): Pair<ByteArray, String> {
|
||||||
val tuoList = gridData.tuoList.drop(1)
|
val tuoList = gridData.tuoList.drop(1)
|
||||||
|
|
||||||
|
|
@ -578,20 +630,21 @@ private suspend fun generatePdfToBytes(
|
||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val markerHeight = 16f
|
val markerHeight = settings. markerHeight
|
||||||
val noteFontSize = 12f
|
val noteFontSize = settings.noteFontSize
|
||||||
val lyricFontSize = 12.5f
|
val lyricFontSize = settings.lyricFontSize
|
||||||
val noteLineH = 17f // espace entre 2 voix
|
val noteLineH = settings.noteLineH
|
||||||
val lyricLineH = 15f // espace entre lignes de lyrics
|
val lyricLineH = settings.lyricLineH
|
||||||
val extraSylLineH = 15f // espace entre syl0 et syl1+ (cas zero spacing)
|
val extraSylLineH = settings.extraSylLineH
|
||||||
val adjustedMargin = 14f // espace entre syl0 du bloc de notes
|
val adjustedMargin = settings.adjustedMargin
|
||||||
val interRowGap = 22f // espace entre deux portées
|
val interRowGap = settings.interRowGap
|
||||||
val marginX = 42.5f
|
val marginX = settings.marginX
|
||||||
val marginY = 42.5f
|
val marginY = settings.marginY
|
||||||
val headerH = 55f // hauteur réservée au header
|
val headerH = settings.headerH
|
||||||
val pageWidth = PDRectangle.A4.width
|
val pageWidth = settings.pageWidth
|
||||||
val pageHeight = PDRectangle.A4.height
|
val pageHeight = settings.pageHeight
|
||||||
val usableWidth = pageWidth - 2 * marginX
|
val usableWidth = pageWidth - 2 * marginX
|
||||||
|
val customNbGrid = settings.nbGridOnMeasure
|
||||||
|
|
||||||
// ── Polices ────────────────────────────────────────────────────
|
// ── Polices ────────────────────────────────────────────────────
|
||||||
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
|
val noteFontBytes = Res.readBytes("files/PTSerif-Regular.ttf")
|
||||||
|
|
@ -614,17 +667,20 @@ private suspend fun generatePdfToBytes(
|
||||||
)
|
)
|
||||||
// ── Calcul de la largeur optimale de colonne (= bestTUOWidth) ──
|
// ── Calcul de la largeur optimale de colonne (= bestTUOWidth) ──
|
||||||
val bestColW = bestColWidthPt(
|
val bestColW = bestColWidthPt(
|
||||||
/*tuoList, customFont,
|
|
||||||
if (lyricFont is PDType0Font) lyricFont else customFont,
|
|
||||||
noteFontSize, lyricFontSize, stanza*/
|
|
||||||
tuoList,
|
tuoList,
|
||||||
customFont,
|
customFont,
|
||||||
noteFontSize,
|
noteFontSize,
|
||||||
lyricFont,
|
lyricFont,
|
||||||
lyricFontSize
|
lyricFontSize
|
||||||
)
|
)
|
||||||
val colCount = ((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
val colCount = if (customNbGrid == 0) {
|
||||||
val colWidth = bestColW
|
((usableWidth / bestColW).toInt()).coerceAtLeast(1)
|
||||||
|
} else {
|
||||||
|
customNbGrid
|
||||||
|
}
|
||||||
|
val finalColWidth = usableWidth / colCount
|
||||||
|
//println("ici Colc est $colCount")
|
||||||
|
val colWidth = finalColWidth
|
||||||
|
|
||||||
|
|
||||||
val maxSylsSize = tuoList.maxOfOrNull { tuo -> // lyrics Alt
|
val maxSylsSize = tuoList.maxOfOrNull { tuo -> // lyrics Alt
|
||||||
|
|
@ -635,7 +691,7 @@ private suspend fun generatePdfToBytes(
|
||||||
|
|
||||||
// ── Fonctions de dessin ────────────────────────────────────
|
// ── Fonctions de dessin ────────────────────────────────────
|
||||||
fun newPage(): PDPageContentStream {
|
fun newPage(): PDPageContentStream {
|
||||||
val page = PDPage(PDRectangle.A4)
|
val page = PDPage(settings.pdRectangle)
|
||||||
doc.addPage(page)
|
doc.addPage(page)
|
||||||
val cs = PDPageContentStream(doc, page)
|
val cs = PDPageContentStream(doc, page)
|
||||||
|
|
||||||
|
|
@ -832,8 +888,8 @@ private suspend fun generatePdfToBytes(
|
||||||
var y = pageHeight - marginY - headerH
|
var y = pageHeight - marginY - headerH
|
||||||
|
|
||||||
drawHeader(
|
drawHeader(
|
||||||
cs, customFont, lyricFont as? PDType0Font,
|
cs, customFont, lyricFont as? PDType0Font, markerBoldFont, markerEmmetFont,
|
||||||
pageWidth, pageHeight, marginX, usableWidth,
|
pageWidth, pageHeight, marginX, marginY, usableWidth,
|
||||||
songTitle = songTitle,
|
songTitle = songTitle,
|
||||||
songAuthor = songAut,
|
songAuthor = songAut,
|
||||||
songRhythm = songRythm,
|
songRhythm = songRythm,
|
||||||
|
|
@ -844,9 +900,18 @@ private suspend fun generatePdfToBytes(
|
||||||
nbStanza = nbStanza
|
nbStanza = nbStanza
|
||||||
)
|
)
|
||||||
|
|
||||||
val rows = tuoList.chunked(colCount)
|
val allRows = tuoList.chunked(colCount)
|
||||||
|
// si dernier portée n'a qu'une séparateur pas de note on dessine sur l'avant dernier
|
||||||
|
val hasTrailingBarlineRow = allRows.size > 1 &&
|
||||||
|
allRows.last().size == 1 &&
|
||||||
|
allRows.last().first().noteAsMultiString().trim().isEmpty() &&
|
||||||
|
allRows.last().first().sep0 in listOf("|", "/")
|
||||||
|
|
||||||
|
val rows = if (hasTrailingBarlineRow) allRows.dropLast(1) else allRows
|
||||||
|
|
||||||
rows.forEachIndexed { rowIdx, rowTuos ->
|
rows.forEachIndexed { rowIdx, rowTuos ->
|
||||||
|
// println("Portée n°: $rowIdx: && rs ${rows.size}")
|
||||||
|
val isLastVisualRow = rowIdx == rows.size - 1
|
||||||
val maxNoteLines = (rowTuos.maxOfOrNull { it.noteAsMultiString().split("\n").size } ?: 1) + 1
|
val maxNoteLines = (rowTuos.maxOfOrNull { it.noteAsMultiString().split("\n").size } ?: 1) + 1
|
||||||
val maxLyricLines = rowTuos.maxOfOrNull { it.getSingleSyllable(stanza).size } ?: 0
|
val maxLyricLines = rowTuos.maxOfOrNull { it.getSingleSyllable(stanza).size } ?: 0
|
||||||
|
|
||||||
|
|
@ -871,13 +936,11 @@ private suspend fun generatePdfToBytes(
|
||||||
// Point de départ des notes (en tenant compte des marqueurs)
|
// Point de départ des notes (en tenant compte des marqueurs)
|
||||||
val notesStartY = if (rowHasMarkers) y - markerHeight else y
|
val notesStartY = if (rowHasMarkers) y - markerHeight else y
|
||||||
|
|
||||||
// On regarde sur toute la ligne quelles voix (0,1,2,3) ont des notes
|
|
||||||
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
|
val pitchPattern = "\\b(d|di|r|ri|m|mi|f|fi|s|si|l|la|t|ta)\\b".toRegex()
|
||||||
fun singingVoiceIndices(tuo: TimeUnitObject): List<Int> {
|
fun singingVoiceIndices(tuo: TimeUnitObject): List<Int> {
|
||||||
return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line ->
|
return tuo.noteAsMultiString().split("\n").mapIndexedNotNull { li, line ->
|
||||||
val clean = line.trim()
|
val clean = line.trim()
|
||||||
// On considère que ça "chante" si ce n'est pas vide,
|
|
||||||
// pas un simple tiret de prolongation, et que ça contient une note (do, re, mi...)
|
|
||||||
val sings = clean.isNotBlank()
|
val sings = clean.isNotBlank()
|
||||||
&& !clean.all { it == '-' || it.isWhitespace() }
|
&& !clean.all { it == '-' || it.isWhitespace() }
|
||||||
&& clean != "—" && clean != "–"
|
&& clean != "—" && clean != "–"
|
||||||
|
|
@ -901,7 +964,6 @@ private suspend fun generatePdfToBytes(
|
||||||
val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||||
|
|
||||||
// NOUVEAU : Est-ce qu'une autre ligne (syl1, syl2...) contient un préfixe "1." ?
|
|
||||||
val uneAutreVoixPrendLeSoprano = (1..4).any { vIdx ->
|
val uneAutreVoixPrendLeSoprano = (1..4).any { vIdx ->
|
||||||
val s = rawSyls.getOrNull(vIdx) ?: ""
|
val s = rawSyls.getOrNull(vIdx) ?: ""
|
||||||
s.contains("1.") && REGEX_HAS_LYRIC.containsMatchIn(s.replace(REGEX_CLEAN, ""))
|
s.contains("1.") && REGEX_HAS_LYRIC.containsMatchIn(s.replace(REGEX_CLEAN, ""))
|
||||||
|
|
@ -924,10 +986,10 @@ private suspend fun generatePdfToBytes(
|
||||||
val prefixInts = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
val prefixInts = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
||||||
|
|
||||||
if (prefixInts.isNotEmpty()) {
|
if (prefixInts.isNotEmpty()) {
|
||||||
val hasTrioATB = prefixInts.size == 3 && prefixInts.containsAll(listOf(2, 3, 4))
|
|
||||||
val hasDuoSA = prefixInts.size == 2 && prefixInts.containsAll(listOf(1, 2))
|
val hasDuoSA = prefixInts.size == 2 && prefixInts.containsAll(listOf(1, 2))
|
||||||
val hasDuoAT = prefixInts.size == 2 && prefixInts.containsAll(listOf(2, 3))
|
val hasDuoAT = prefixInts.size == 2 && prefixInts.containsAll(listOf(2, 3))
|
||||||
val hasDuoTB = prefixInts.size == 2 && prefixInts.containsAll(listOf(3, 4))
|
val hasDuoTB = prefixInts.size == 2 && prefixInts.containsAll(listOf(3, 4))
|
||||||
|
val hasTrioATB = prefixInts.size == 3 && prefixInts.containsAll(listOf(2, 3, 4))
|
||||||
val isUnisson = prefixInts.containsAll(listOf(1, 2, 3, 4))
|
val isUnisson = prefixInts.containsAll(listOf(1, 2, 3, 4))
|
||||||
|
|
||||||
// Logique simplifiée pour le calcul des spacings (Duo consécutif)
|
// Logique simplifiée pour le calcul des spacings (Duo consécutif)
|
||||||
|
|
@ -945,19 +1007,14 @@ private suspend fun generatePdfToBytes(
|
||||||
else if (hasDuoSA) {
|
else if (hasDuoSA) {
|
||||||
spacings[1] = 1
|
spacings[1] = 1
|
||||||
}
|
}
|
||||||
/*else if (hasDuoAT) {
|
|
||||||
spacings[1] = 0
|
|
||||||
spacings[2] = 1
|
|
||||||
}*/
|
|
||||||
else if (hasDuoTB) {
|
else if (hasDuoTB) {
|
||||||
if (syl0HasRealText) { spacings[1] = 1 }
|
|
||||||
spacings[3] = 1
|
spacings[3] = 1
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Cas Standard (Individuel)
|
// Cas Standard (Individuel)
|
||||||
prefixInts.forEach { voiceNum ->
|
prefixInts.forEach { voiceNum ->
|
||||||
when (voiceNum) {
|
when (voiceNum) {
|
||||||
1 -> if (syl0HasRealText) spacings[0] = 1
|
1 -> if (syl0HasRealText && prefixInts.contains(1)) spacings[0] = 1
|
||||||
2 -> if (!prefixInts.contains(1)) spacings[1] = 1
|
2 -> if (!prefixInts.contains(1)) spacings[1] = 1
|
||||||
3 -> if (!prefixInts.contains(2)) spacings[2] = 1
|
3 -> if (!prefixInts.contains(2)) spacings[2] = 1
|
||||||
4 -> if (!prefixInts.contains(3)) spacings[3] = 1
|
4 -> if (!prefixInts.contains(3)) spacings[3] = 1
|
||||||
|
|
@ -972,7 +1029,7 @@ private suspend fun generatePdfToBytes(
|
||||||
val customSpacings = spacings.toList()
|
val customSpacings = spacings.toList()
|
||||||
|
|
||||||
|
|
||||||
/*if (rowIdx == 7 || (rowIdx == 8)) {
|
/*if (rowIdx == 2 || (rowIdx == 2)) {
|
||||||
println("╠══════════════════════════════════════════════════════════════╣")
|
println("╠══════════════════════════════════════════════════════════════╣")
|
||||||
println("║ customSpacings calculés : $customSpacings ")
|
println("║ customSpacings calculés : $customSpacings ")
|
||||||
println("╚══════════════════════════════════════════════════════════════╝")
|
println("╚══════════════════════════════════════════════════════════════╝")
|
||||||
|
|
@ -1014,7 +1071,6 @@ private suspend fun generatePdfToBytes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println("\n" + "═".repeat(64))
|
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
// println("\n[FIN SCAN ROW $rowIdx]")
|
// println("\n[FIN SCAN ROW $rowIdx]")
|
||||||
|
|
@ -1034,10 +1090,10 @@ private suspend fun generatePdfToBytes(
|
||||||
drawHairPinForTUO(
|
drawHairPinForTUO(
|
||||||
cs = cs,
|
cs = cs,
|
||||||
tuo = tuo,
|
tuo = tuo,
|
||||||
gridColumnCount = colCount, // tu as déjà colCount
|
gridColumnCount = colCount,
|
||||||
colX = x,
|
colX = x,
|
||||||
colWidth = colWidth,
|
colWidth = colWidth,
|
||||||
markerY = y, // ton y actuel
|
markerY = y,
|
||||||
noteLineH = noteLineH
|
noteLineH = noteLineH
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1093,6 +1149,27 @@ private suspend fun generatePdfToBytes(
|
||||||
rowTuos.forEachIndexed { colIdx, tuo ->
|
rowTuos.forEachIndexed { colIdx, tuo ->
|
||||||
val x = marginX + colIdx * colWidth
|
val x = marginX + colIdx * colWidth
|
||||||
|
|
||||||
|
// on dessine à droite
|
||||||
|
if (hasTrailingBarlineRow && isLastVisualRow && colIdx == rowTuos.size - 1) {
|
||||||
|
val finalX = x + colWidth
|
||||||
|
val sepToDraw = allRows.last().first().sep0
|
||||||
|
|
||||||
|
cs.setStrokingColor(0f, 0f, 0f)
|
||||||
|
cs.setLineWidth(0.8f)
|
||||||
|
|
||||||
|
val sepW = textWidth(customFont, noteFontSize, "|")
|
||||||
|
cs.moveTo(finalX - sepW, lineTop)
|
||||||
|
cs.lineTo(finalX - sepW, lineBottom)
|
||||||
|
cs.stroke()
|
||||||
|
|
||||||
|
if (sepToDraw == "/") {
|
||||||
|
cs.moveTo(finalX - (sepW * 0.5f), lineTop)
|
||||||
|
cs.lineTo(finalX - (sepW * 0.5f), lineBottom)
|
||||||
|
cs.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
val sepW = textWidth(customFont, noteFontSize, "|")
|
val sepW = textWidth(customFont, noteFontSize, "|")
|
||||||
val noteX = when (tuo.sep0) {
|
val noteX = when (tuo.sep0) {
|
||||||
"/", "|" -> {
|
"/", "|" -> {
|
||||||
|
|
@ -1110,8 +1187,7 @@ private suspend fun generatePdfToBytes(
|
||||||
|
|
||||||
|
|
||||||
val noteLines = tuo.noteAsMultiString().split("\n")
|
val noteLines = tuo.noteAsMultiString().split("\n")
|
||||||
|
// println("\n=== ROW0 col[$colIdx] sep0='${tuo.sep0}' note=${noteLines[0]} ===")
|
||||||
// println("\n=== ROW0 col[$colIdx] sep0='${tuo.sep0}' noteX=$noteX ===")
|
|
||||||
// println(" noteAsMultiString raw = ${tuo.noteAsMultiString().replace("\n", "\\n")}")
|
// println(" noteAsMultiString raw = ${tuo.noteAsMultiString().replace("\n", "\\n")}")
|
||||||
// println(" noteLines.size = ${noteLines.size}")
|
// println(" noteLines.size = ${noteLines.size}")
|
||||||
noteLines.forEachIndexed { li, line ->
|
noteLines.forEachIndexed { li, line ->
|
||||||
|
|
@ -1201,133 +1277,101 @@ private suspend fun generatePdfToBytes(
|
||||||
// println("║ lyricLineH = $lyricLineH")
|
// println("║ lyricLineH = $lyricLineH")
|
||||||
|
|
||||||
// Numéro de stanza
|
// Numéro de stanza
|
||||||
val stanzaNumTxt = "$currentStanza."
|
var hasLyrics = false
|
||||||
val stanzaNumSize = lyricFontSize * 0.85f
|
allTemps.forEachIndexed { sIdx, syllables ->
|
||||||
val stanzaNumFont = if (lyricFont is PDType0Font) lyricFont else customFont
|
|
||||||
val stanzaNumW = try {
|
|
||||||
stanzaNumFont.getStringWidth(stanzaNumTxt) / 1000f * stanzaNumSize
|
|
||||||
} catch (e: Exception) { 0f }
|
|
||||||
|
|
||||||
cs.beginText()
|
|
||||||
cs.setFont(stanzaNumFont, stanzaNumSize)
|
|
||||||
cs.setNonStrokingColor(Color.BLACK)
|
|
||||||
cs.newLineAtOffset(marginX - stanzaNumW - 4f, currentYLyric)
|
|
||||||
cs.showText(stanzaNumTxt)
|
|
||||||
cs.endText()
|
|
||||||
|
|
||||||
// Syllabes
|
|
||||||
/*allTemps.forEachIndexed { sIdx, syllables ->
|
|
||||||
val x = marginX + sIdx * colWidth
|
|
||||||
val noteX = if (rowTuos.getOrNull(sIdx)?.sep0 in listOf("/", "|")) x + 3f else x
|
|
||||||
|
|
||||||
syllables.forEachIndexed { lineIdx, syl ->
|
syllables.forEachIndexed { lineIdx, syl ->
|
||||||
val (spacedSyl, alignLeft) = spacedSyllable(
|
if(syl.isNotEmpty()) hasLyrics=true
|
||||||
syl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize
|
|
||||||
)
|
|
||||||
val lyricX = if (alignLeft) noteX
|
|
||||||
else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
|
|
||||||
|
|
||||||
cs.beginText()
|
|
||||||
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
|
||||||
cs.newLineAtOffset(lyricX, yLyric - lineIdx * lyricLineH)
|
|
||||||
cs.showText(spacedSyl.sanitize())
|
|
||||||
cs.endText()
|
|
||||||
}
|
}
|
||||||
}*/
|
|
||||||
fun drawInOverrideSlot(
|
|
||||||
cs: PDPageContentStream,
|
|
||||||
targetIdx: Int, // 0=S, 1=A, 2=T, 3=B, ou -1 pour le remplacement direct de syl0
|
|
||||||
cleanSyl: String,
|
|
||||||
sIdx: Int,
|
|
||||||
lineIdx: Int,
|
|
||||||
allTemps: List<List<String>>,
|
|
||||||
noteX: Float,
|
|
||||||
colWidth: Float,
|
|
||||||
customFont: PDType0Font,
|
|
||||||
lyricFontSize: Float,
|
|
||||||
notesStartY: Float, // Le point de référence (soit notesStartY pour SATB, soit yLyric pour le S)
|
|
||||||
noteLineH: Float,
|
|
||||||
customSpacings: List<Int>,
|
|
||||||
isDirectY: Boolean = false // Si true, on ignore le calcul de vLi et on utilise notesStartY direct
|
|
||||||
) {
|
|
||||||
val lyricColor = when (targetIdx) {
|
|
||||||
-1, 0 -> Color(180, 0, 0) // Soprano (Remplacement ou standard)
|
|
||||||
1 -> Color(0, 120, 0) // Alto
|
|
||||||
2 -> Color(0, 0, 180) // Ténor
|
|
||||||
3 -> Color(0, 100, 120) // Basse
|
|
||||||
else -> Color.BLACK // Par défaut (sécurité)
|
|
||||||
}
|
|
||||||
// 1. Calcul du Y final
|
|
||||||
val finalY = if (isDirectY) {
|
|
||||||
notesStartY // On se pose directement sur yLyric
|
|
||||||
} else {
|
|
||||||
// Calcul standard pour les espaces vides entre les notes
|
|
||||||
val vLi = getVirtualLineIndex(targetIdx, customSpacings)
|
|
||||||
notesStartY - ((vLi + 1) * noteLineH) + 2f
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Calcul de l'alignement horizontal
|
|
||||||
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
|
||||||
val lyricX = noteX + offsetX /*if (alignLeft) {
|
|
||||||
noteX // Alignement gauche sur la note
|
|
||||||
} else {
|
|
||||||
// CENTRAGE : On calcule le milieu de la colonne et on retire la moitié de la largeur du texte
|
|
||||||
val txtW = textWidth(customFont, lyricFontSize, spacedSyl)
|
|
||||||
noteX + (colWidth / 2f) - (txtW / 2f)
|
|
||||||
}*/
|
|
||||||
// val lyricX = if (alignLeft) noteX else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
|
|
||||||
|
|
||||||
// 3. Dessin
|
|
||||||
cs.saveGraphicsState()
|
|
||||||
cs.beginText()
|
|
||||||
cs.setFont(customFont, lyricFontSize * 0.9f)
|
|
||||||
cs.setNonStrokingColor(lyricColor) // Définit la couleur de remplissage du texte
|
|
||||||
cs.newLineAtOffset(lyricX, finalY)
|
|
||||||
cs.showText(spacedSyl.sanitize())
|
|
||||||
cs.endText()
|
|
||||||
cs.restoreGraphicsState()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// println("hasL? = $hasLyrics")
|
||||||
|
if(hasLyrics) {
|
||||||
|
val stanzaNumTxt = "$currentStanza."
|
||||||
|
val stanzaNumSize = lyricFontSize * 0.85f
|
||||||
|
val stanzaNumFont = if (lyricFont is PDType0Font) lyricFont else customFont
|
||||||
|
val stanzaNumW = try {
|
||||||
|
stanzaNumFont.getStringWidth(stanzaNumTxt) / 1000f * stanzaNumSize
|
||||||
|
} catch (e: Exception) { 0f }
|
||||||
|
|
||||||
allTemps.forEachIndexed { sIdx, syllables ->
|
cs.beginText()
|
||||||
val x = marginX + sIdx * colWidth
|
cs.setFont(stanzaNumFont, stanzaNumSize)
|
||||||
val noteX = if (rowTuos.getOrNull(sIdx)?.sep0 in listOf("/", "|")) x + 3f else x
|
cs.setNonStrokingColor(Color.BLACK)
|
||||||
|
cs.newLineAtOffset(marginX - stanzaNumW - 4f, currentYLyric)
|
||||||
|
cs.showText(stanzaNumTxt)
|
||||||
|
cs.endText()
|
||||||
|
|
||||||
// On suit quels slots (0,1,2,3) ont déjà reçu du texte pour CETTE colonne
|
fun drawInOverrideSlot(
|
||||||
val filledSlots = mutableSetOf<Int>()
|
cs: PDPageContentStream,
|
||||||
|
targetIdx: Int, // 0=S, 1=A, 2=T, 3=B, ou -1 pour le remplacement direct de syl0
|
||||||
val isStandardLayout = customSpacings.all { it == 0 }
|
cleanSyl: String,
|
||||||
|
sIdx: Int,
|
||||||
syllables.forEachIndexed { lineIdx, rawSyl ->
|
lineIdx: Int,
|
||||||
val expectedY = currentYLyric - lineIdx * lyricLineH
|
allTemps: List<List<String>>,
|
||||||
// println("║ col[$sIdx] syl[$lineIdx] '$rawSyl' → Y=$expectedY")
|
noteX: Float,
|
||||||
|
colWidth: Float,
|
||||||
if (lineIdx == 0) {
|
customFont: PDType0Font,
|
||||||
// SYL0 : On dessine normalement en bas (comportement standard)
|
lyricFontSize: Float,
|
||||||
val (spacedSyl, offsetX) = spacedSyllable(rawSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
notesStartY: Float, // Le point de référence (soit notesStartY pour SATB, soit yLyric pour le S)
|
||||||
// val lyricX = if (alignLeft) noteX else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
|
noteLineH: Float,
|
||||||
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
customSpacings: List<Int>,
|
||||||
|
isDirectY: Boolean = false // Si true, on ignore le calcul de vLi et on utilise notesStartY direct
|
||||||
cs.beginText()
|
) {
|
||||||
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
val lyricColor = when (targetIdx) {
|
||||||
cs.newLineAtOffset(lyricX, currentYLyric)
|
-1, 0 -> Color(180, 0, 0) // Soprano (Remplacement ou standard)
|
||||||
cs.showText(spacedSyl.sanitize())
|
1 -> Color(0, 120, 0) // Alto
|
||||||
cs.endText()
|
2 -> Color(0, 0, 180) // Ténor
|
||||||
|
3 -> Color(0, 100, 120) // Basse
|
||||||
|
else -> Color.BLACK // Par défaut (sécurité)
|
||||||
|
}
|
||||||
|
// 1. Calcul du Y final
|
||||||
|
val finalY = if (isDirectY) {
|
||||||
|
notesStartY // On se pose directement sur yLyric
|
||||||
} else {
|
} else {
|
||||||
// 1. D'abord, on vérifie si c'est un Solo Soprano "1." même si spacing est à 0
|
// Calcul standard pour les espaces vides entre les notes
|
||||||
val prefixes = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
val vLi = getVirtualLineIndex(targetIdx, customSpacings)
|
||||||
val rawSyl0 = syllables.getOrNull(0) ?: ""
|
notesStartY - ((vLi + 1) * noteLineH) + 2f
|
||||||
val cleanSyl0 = cleanSyllable(rawSyl0)
|
}
|
||||||
val cleanSyl = cleanSyllable(rawSyl)
|
|
||||||
|
|
||||||
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
// 2. Calcul de l'alignement horizontal
|
||||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
||||||
|
val lyricX = noteX + offsetX /*if (alignLeft) {
|
||||||
|
noteX // Alignement gauche sur la note
|
||||||
|
} else {
|
||||||
|
// CENTRAGE : On calcule le milieu de la colonne et on retire la moitié de la largeur du texte
|
||||||
|
val txtW = textWidth(customFont, lyricFontSize, spacedSyl)
|
||||||
|
noteX + (colWidth / 2f) - (txtW / 2f)
|
||||||
|
}*/
|
||||||
|
// val lyricX = if (alignLeft) noteX else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
|
||||||
|
|
||||||
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
// 3. Dessin
|
||||||
|
cs.saveGraphicsState()
|
||||||
|
cs.beginText()
|
||||||
|
cs.setFont(customFont, lyricFontSize * 0.9f)
|
||||||
|
cs.setNonStrokingColor(lyricColor) // Définit la couleur de remplissage du texte
|
||||||
|
cs.newLineAtOffset(lyricX, finalY)
|
||||||
|
cs.showText(spacedSyl.sanitize())
|
||||||
|
cs.endText()
|
||||||
|
cs.restoreGraphicsState()
|
||||||
|
}
|
||||||
|
|
||||||
if (prefixes.contains(1) && isAllZeroSpacing && lignePrincipaleEstLibre) {
|
|
||||||
// FORCE LE DESSIN SUR LA LIGNE PRINCIPALE (SYL0)
|
allTemps.forEachIndexed { sIdx, syllables ->
|
||||||
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
val x = marginX + sIdx * colWidth
|
||||||
|
val noteX = if (rowTuos.getOrNull(sIdx)?.sep0 in listOf("/", "|")) x + 3f else x
|
||||||
|
|
||||||
|
// On suit quels slots (0,1,2,3) ont déjà reçu du texte pour CETTE colonne
|
||||||
|
val filledSlots = mutableSetOf<Int>()
|
||||||
|
|
||||||
|
val isStandardLayout = customSpacings.all { it == 0 }
|
||||||
|
|
||||||
|
syllables.forEachIndexed { lineIdx, rawSyl ->
|
||||||
|
val expectedY = currentYLyric - lineIdx * lyricLineH
|
||||||
|
// println("║ col[$sIdx] syl[$lineIdx] '$rawSyl' → Y=$expectedY")
|
||||||
|
|
||||||
|
if (lineIdx == 0) {
|
||||||
|
// SYL0 : On dessine normalement en bas (comportement standard)
|
||||||
|
val (spacedSyl, offsetX) = spacedSyllable(rawSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
||||||
|
// val lyricX = if (alignLeft) noteX else noteX + colWidth - textWidth(customFont, lyricFontSize, spacedSyl)
|
||||||
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
||||||
|
|
||||||
cs.beginText()
|
cs.beginText()
|
||||||
|
|
@ -1335,83 +1379,98 @@ private suspend fun generatePdfToBytes(
|
||||||
cs.newLineAtOffset(lyricX, currentYLyric)
|
cs.newLineAtOffset(lyricX, currentYLyric)
|
||||||
cs.showText(spacedSyl.sanitize())
|
cs.showText(spacedSyl.sanitize())
|
||||||
cs.endText()
|
cs.endText()
|
||||||
|
} else {
|
||||||
} else if (isAllZeroSpacing) {
|
// 1. D'abord, on vérifie si c'est un Solo Soprano "1." même si spacing est à 0
|
||||||
// ── CAS SIMPLE : pas d'espace entre notes → syl1+ descend sous syl0 ──
|
val prefixes = REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
||||||
|
val rawSyl0 = syllables.getOrNull(0) ?: ""
|
||||||
|
val cleanSyl0 = cleanSyllable(rawSyl0)
|
||||||
val cleanSyl = cleanSyllable(rawSyl)
|
val cleanSyl = cleanSyllable(rawSyl)
|
||||||
if (cleanSyl.isNotBlank()) {
|
|
||||||
val thisLineY = currentYLyric - lineIdx * extraSylLineH
|
val syl0LyricsIsFree = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
||||||
|
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
||||||
|
|
||||||
|
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
||||||
|
|
||||||
|
if (prefixes.contains(1) && isAllZeroSpacing && syl0LyricsIsFree) {
|
||||||
|
println("DEBUG: Force SYL1 sur ligne principale (Rouge) -> $cleanSyl")
|
||||||
|
|
||||||
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
||||||
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
||||||
|
|
||||||
|
cs.saveGraphicsState()
|
||||||
cs.beginText()
|
cs.beginText()
|
||||||
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
||||||
cs.setNonStrokingColor(Color.BLACK)
|
cs.setNonStrokingColor(Color.RED)
|
||||||
cs.newLineAtOffset(lyricX, thisLineY) // thisLineY = currentYLyric - lineIdx * lyricLineH
|
cs.newLineAtOffset(lyricX, currentYLyric)
|
||||||
cs.showText(spacedSyl.sanitize())
|
cs.showText(spacedSyl.sanitize())
|
||||||
cs.endText()
|
cs.endText()
|
||||||
}
|
cs.restoreGraphicsState()
|
||||||
} else { // --- OVERRIDES (SYL 1, 2, 3...) ---
|
|
||||||
val prefixes =
|
|
||||||
REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
|
||||||
val cleanSyl = cleanSyllable(rawSyl)
|
|
||||||
|
|
||||||
val rawSyl0 = syllables.getOrNull(0) ?: ""
|
} else if (isAllZeroSpacing) {
|
||||||
val cleanSyl0 = cleanSyllable(rawSyl0)
|
// ── CAS SIMPLE : pas d'espace entre notes → syl1+ descend sous syl0 ──
|
||||||
val lignePrincipaleEstLibre = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl0) ||
|
val cleanSyl = cleanSyllable(rawSyl)
|
||||||
cleanSyl0.all { it == '―' || it == '-' || it == '—' }
|
if (cleanSyl.isNotBlank()) {
|
||||||
|
val thisLineY = currentYLyric - lineIdx * extraSylLineH
|
||||||
|
|
||||||
|
val (spacedSyl, offsetX) = spacedSyllable(cleanSyl, allTemps, sIdx, lineIdx, colWidth, customFont, lyricFontSize)
|
||||||
|
val lyricX = noteX + offsetX /*if (alignLeft) noteX else noteX + (colWidth / 2f) - (textWidth(customFont, lyricFontSize, spacedSyl) / 2f)*/
|
||||||
|
|
||||||
|
cs.beginText()
|
||||||
|
cs.setFont(if (lyricFont is PDType0Font) lyricFont else customFont, lyricFontSize)
|
||||||
|
cs.setNonStrokingColor(Color.BLACK)
|
||||||
|
cs.newLineAtOffset(lyricX, thisLineY) // thisLineY = currentYLyric - lineIdx * lyricLineH
|
||||||
|
cs.showText(spacedSyl.sanitize())
|
||||||
|
cs.endText()
|
||||||
|
}
|
||||||
|
} else { // --- OVERRIDES (SYL 1, 2, 3...) ---
|
||||||
|
val prefixes =
|
||||||
|
REGEX_PREFIX_DIGITS.findAll(rawSyl).map { it.groupValues[1].toInt() }.toList()
|
||||||
|
val cleanSyl = cleanSyllable(rawSyl)
|
||||||
|
|
||||||
|
val rawSyl0 = syllables.getOrNull(0) ?: ""
|
||||||
|
val cleanSyl0 = cleanSyllable(rawSyl0)
|
||||||
|
|
||||||
|
|
||||||
val hasActualText = REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) &&
|
val hasActualText = REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) &&
|
||||||
!cleanSyl.all { it == '―' || it == '-' }
|
!cleanSyl.all { it == '―' || it == '-' }
|
||||||
val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) ||
|
val syl0EstVraimentVide = !REGEX_HAS_LYRIC.containsMatchIn(cleanSyl) ||
|
||||||
cleanSyl.all { it == '―' || it == '-' || it == '—' }
|
cleanSyl.all { it == '―' || it == '-' || it == '—' }
|
||||||
|
|
||||||
if (prefixes.isNotEmpty() && hasActualText) {
|
if (prefixes.isNotEmpty() && hasActualText) {
|
||||||
prefixes.forEach { voiceNum ->
|
prefixes.forEach { voiceNum ->
|
||||||
val targetIdx = voiceNum - 1 // 0=S, 1=A, 2=T, 3=B
|
val targetIdx = voiceNum - 1 // 0=S, 1=A, 2=T, 3=B
|
||||||
|
|
||||||
// --- CAS SPÉCIAL : LE PRÉFIXE "1." (SOPRANO) ---
|
// --- CAS SPÉCIAL : LE PRÉFIXE "1." (SOPRANO) ---
|
||||||
if (voiceNum == 1) {
|
if (voiceNum == 1) {
|
||||||
// A. PRIORITÉ : Si la ligne principale est libre, on s'installe dessus
|
if (customSpacings.getOrElse(0) { 0 } == 1 && !filledSlots.contains(0)) {
|
||||||
if (lignePrincipaleEstLibre && !filledSlots.contains(-1)) {
|
drawInOverrideSlot(
|
||||||
drawInOverrideSlot(
|
cs,
|
||||||
cs, -1, cleanSyl, sIdx, lineIdx, allTemps, noteX, colWidth,
|
0,
|
||||||
customFont, lyricFontSize, currentYLyric, noteLineH, customSpacings,
|
cleanSyl,
|
||||||
isDirectY = true // Pose sur yLyric
|
sIdx,
|
||||||
)
|
lineIdx,
|
||||||
filledSlots.add(-1)
|
allTemps,
|
||||||
}
|
noteX,
|
||||||
// B. REPLI : Si la ligne 0 est occupée, on regarde si un espace (1) est ouvert dans cS
|
colWidth,
|
||||||
else if (customSpacings.getOrElse(0) { 0 } == 1 && !filledSlots.contains(0)) {
|
customFont,
|
||||||
drawInOverrideSlot(
|
lyricFontSize,
|
||||||
cs,
|
notesStartY - yLyricOffset,
|
||||||
0,
|
noteLineH,
|
||||||
cleanSyl,
|
customSpacings
|
||||||
sIdx,
|
)
|
||||||
lineIdx,
|
filledSlots.add(0)
|
||||||
allTemps,
|
}
|
||||||
noteX,
|
} else {
|
||||||
colWidth,
|
// --- CAS GÉNÉRAL (Alto 2, Ténor 3, Basse 4) ---
|
||||||
customFont,
|
if (customSpacings.getOrElse(targetIdx) { 0 } == 1 && !filledSlots.contains(
|
||||||
lyricFontSize,
|
targetIdx
|
||||||
notesStartY - yLyricOffset,
|
)) {
|
||||||
noteLineH,
|
drawInOverrideSlot(
|
||||||
customSpacings
|
cs, targetIdx, cleanSyl, sIdx, lineIdx, allTemps, noteX, colWidth,
|
||||||
)
|
customFont, lyricFontSize, notesStartY, noteLineH, customSpacings
|
||||||
filledSlots.add(0)
|
)
|
||||||
}
|
filledSlots.add(targetIdx)
|
||||||
} else {
|
}
|
||||||
// --- CAS GÉNÉRAL (Alto 2, Ténor 3, Basse 4) ---
|
|
||||||
if (customSpacings.getOrElse(targetIdx) { 0 } == 1 && !filledSlots.contains(
|
|
||||||
targetIdx
|
|
||||||
)) {
|
|
||||||
drawInOverrideSlot(
|
|
||||||
cs, targetIdx, cleanSyl, sIdx, lineIdx, allTemps, noteX, colWidth,
|
|
||||||
customFont, lyricFontSize, notesStartY, noteLineH, customSpacings
|
|
||||||
)
|
|
||||||
filledSlots.add(targetIdx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1419,29 +1478,29 @@ private suspend fun generatePdfToBytes(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// println("╚══════════════════════════════════════════════════════════")
|
||||||
// println("╚══════════════════════════════════════════════════════════")
|
|
||||||
|
|
||||||
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
val isAllZeroSpacing = customSpacings.all { it == 0 }
|
||||||
val maxExtraLines = if (isAllZeroSpacing) {
|
val maxExtraLines = if (isAllZeroSpacing) {
|
||||||
allTemps.maxOfOrNull { sylList ->
|
allTemps.maxOfOrNull { sylList ->
|
||||||
sylList.count { rawSyl ->
|
sylList.count { rawSyl ->
|
||||||
val clean = cleanSyllable(rawSyl)
|
val clean = cleanSyllable(rawSyl)
|
||||||
REGEX_HAS_LYRIC.containsMatchIn(clean) &&
|
REGEX_HAS_LYRIC.containsMatchIn(clean) &&
|
||||||
!clean.all { it == '―' || it == '-' || it == '—' }
|
!clean.all { it == '―' || it == '-' || it == '—' }
|
||||||
}.coerceAtLeast(1)
|
}.coerceAtLeast(1)
|
||||||
} ?: 1
|
} ?: 1
|
||||||
} else {
|
} else {
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
// Le vrai bas = currentYLyric - (toutes les lignes sauf syl0) * lyricLineH
|
// Le vrai bas = currentYLyric - (toutes les lignes sauf syl0) * lyricLineH
|
||||||
val bottomOfCurrentStanza = currentYLyric - ((maxExtraLines - 1) * extraSylLineH) - 4f
|
val bottomOfCurrentStanza = currentYLyric - ((maxExtraLines - 1) * extraSylLineH) - 4f
|
||||||
|
|
||||||
|
|
||||||
// MAJ du point le plus bas : on retire la hauteur de la ligne de texte
|
// MAJ du point le plus bas : on retire la hauteur de la ligne de texte
|
||||||
// val bottomOfCurrentStanza = currentYLyric - (lyricLineH * 0.5f)
|
// val bottomOfCurrentStanza = currentYLyric - (lyricLineH * 0.5f)
|
||||||
if (bottomOfCurrentStanza < lowestYOfThisRow) {
|
if (bottomOfCurrentStanza < lowestYOfThisRow) {
|
||||||
lowestYOfThisRow = bottomOfCurrentStanza
|
lowestYOfThisRow = bottomOfCurrentStanza
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// val bottomOfStanza = yLyric - (maxLyricLines * lyricLineH)
|
// val bottomOfStanza = yLyric - (maxLyricLines * lyricLineH)
|
||||||
// if (bottomOfStanza < lowestYOfThisRow) lowestYOfThisRow = bottomOfStanza
|
// if (bottomOfStanza < lowestYOfThisRow) lowestYOfThisRow = bottomOfStanza
|
||||||
|
|
@ -1494,7 +1553,7 @@ private suspend fun generatePdfToBytes(
|
||||||
// println(" DC[$tuoIdx]: '$dcPart'")
|
// println(" DC[$tuoIdx]: '$dcPart'")
|
||||||
}
|
}
|
||||||
if (tuo.pTemplate.markerToString().contains("DC")) {
|
if (tuo.pTemplate.markerToString().contains("DC")) {
|
||||||
// println(" 🎯 DC MARQUEUR[$tuoIdx]")
|
// println(" DC MARQUEUR[$tuoIdx]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1509,15 +1568,14 @@ private suspend fun generatePdfToBytes(
|
||||||
val dcPart = syls.drop(1).joinToString(" ") { it.trim() }
|
val dcPart = syls.drop(1).joinToString(" ") { it.trim() }
|
||||||
|
|
||||||
|
|
||||||
// ✅ 2e : NORMAL (si existe)
|
// NORMAL (si existe)
|
||||||
if (normal.isNotEmpty()) {
|
if (normal.isNotEmpty()) {
|
||||||
result.append(normal).append(" ")
|
result.append(normal).append(" ")
|
||||||
// println(" → AJOUT NORMAL[$tuoIdx]: '$normal'")
|
|
||||||
}
|
}
|
||||||
// ✅ 1er : CROCHET DC (TOUJOURS, même si normal vide)
|
// CROCHET DC (TOUJOURS, même si normal vide)
|
||||||
if (dcPart.isNotEmpty()) {
|
if (dcPart.isNotEmpty()) {
|
||||||
result.append("[$dcPart] ")
|
// dcPart
|
||||||
// println(" 🟦 CROCHET DC[$tuoIdx]: '[$dcPart]'")
|
result.append("[${cleanSyllable(dcPart)}] ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. DC MARQUEUR (parenthèses)
|
// 3. DC MARQUEUR (parenthèses)
|
||||||
|
|
@ -1739,7 +1797,7 @@ private suspend fun generatePdfToBytes(
|
||||||
os.contains("win") -> Runtime.getRuntime().exec(arrayOf("explorer", outputDir))
|
os.contains("win") -> Runtime.getRuntime().exec(arrayOf("explorer", outputDir))
|
||||||
}
|
}
|
||||||
emmentalerTTF.close()
|
emmentalerTTF.close()
|
||||||
println("✅ PDF : $outputPath")*/
|
println(" PDF : $outputPath")*/
|
||||||
val computedFileName = "${cleanTitle}_R_${rStr}_U_${uStr}.pdf"
|
val computedFileName = "${cleanTitle}_R_${rStr}_U_${uStr}.pdf"
|
||||||
|
|
||||||
// Sérialiser en mémoire
|
// Sérialiser en mémoire
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package mg.dot.feufaro.pdf
|
||||||
|
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle
|
||||||
|
|
||||||
|
actual data class PrintSettings actual constructor(
|
||||||
|
actual val pageSize : PageSize,
|
||||||
|
actual val orientation : PageOrientation,
|
||||||
|
actual val markerHeight : Float,
|
||||||
|
actual val noteFontSize : Float,
|
||||||
|
actual val lyricFontSize : Float,
|
||||||
|
actual val noteLineH : Float,
|
||||||
|
actual val lyricLineH : Float,
|
||||||
|
actual val extraSylLineH : Float,
|
||||||
|
actual val adjustedMargin : Float,
|
||||||
|
actual val interRowGap : Float,
|
||||||
|
actual val marginX : Float,
|
||||||
|
actual val marginY : Float,
|
||||||
|
actual val headerH : Float,
|
||||||
|
actual val nbGridOnMeasure: Int
|
||||||
|
) {
|
||||||
|
val pdRectangle: PDRectangle
|
||||||
|
get() {
|
||||||
|
val base = when (pageSize) {
|
||||||
|
PageSize.A4 -> PDRectangle.A4
|
||||||
|
PageSize.A3 -> PDRectangle.A3
|
||||||
|
PageSize.LETTER -> PDRectangle.LETTER
|
||||||
|
}
|
||||||
|
return if (orientation == PageOrientation.PORTRAIT) base
|
||||||
|
else PDRectangle(base.height, base.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual val pageWidth : Float get() = pdRectangle.width
|
||||||
|
actual val pageHeight : Float get() = pdRectangle.height
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun defaultPrintSettings(): PrintSettings = PrintSettings(
|
||||||
|
pageSize = PageSize.A4,
|
||||||
|
orientation = PageOrientation.PORTRAIT,
|
||||||
|
markerHeight = 16f,
|
||||||
|
noteFontSize = 12f,
|
||||||
|
lyricFontSize = 12.5f,
|
||||||
|
noteLineH = 17f,
|
||||||
|
lyricLineH = 15f,
|
||||||
|
extraSylLineH = 15f,
|
||||||
|
adjustedMargin = 14f,
|
||||||
|
interRowGap = 22f,
|
||||||
|
marginX = 42.5f,
|
||||||
|
marginY = 42.5f,
|
||||||
|
headerH = 55f,
|
||||||
|
nbGridOnMeasure = 0
|
||||||
|
)
|
||||||
Loading…
Add table
Reference in a new issue