1062 lines
45 KiB
QML
1062 lines
45 KiB
QML
//=============================================================================
|
|
// MuseScore
|
|
// Music Composition & Notation
|
|
//
|
|
// Generalized whistle tab plugin
|
|
// Requires the tin whistle font downloaded from Blayne Chastain:
|
|
// https://www.blaynechastain.com/tin-whistle-tab-sibelius-plugin/
|
|
//
|
|
// Based on the Note Names Plugin which is:
|
|
// Copyright (C) 2012 Werner Schweer
|
|
// Copyright (C) 2013 - 2016 Joachim Schmitz
|
|
// Copyright (C) 2014 Jörn Eichler
|
|
//
|
|
// and also based on the Recorder Woodwind Tablature plugin:
|
|
// Copyright (C)2011 Dario Escobedo, Werner Schweer, Jens Iwanenko and others
|
|
//
|
|
// This program is free software; you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License version 2
|
|
// as published by the Free Software Foundation and appearing in
|
|
// the file LICENCE
|
|
//=============================================================================
|
|
|
|
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
import MuseScore 3.0
|
|
import FileIO 3.0
|
|
|
|
MuseScore {
|
|
version: "4.1"
|
|
title: "ASCII Whistle Fingering"
|
|
description: "Inserts ASCII fingering diagrams using binary dictionary"
|
|
pluginType: "dialog"
|
|
categoryCode: "composing-arranging-tools"
|
|
thumbnailName: "whistle_tab.png"
|
|
|
|
width: 790
|
|
height: 644
|
|
|
|
property bool isDarkMode: false
|
|
property color textColor: isDarkMode ? "#ffffff" : "#000000"
|
|
property color backgroundColor: isDarkMode ? "#333333" : "#ffffff"
|
|
|
|
//---------------------------------------------------------
|
|
// USER SETTINGS (defaults)
|
|
//---------------------------------------------------------
|
|
|
|
property int userFontSize: 14
|
|
property int userJustification: 1 // 0=left,1=center,2=right
|
|
property real userOffsetY: 3.0
|
|
property real userLineSpacing: 0.5
|
|
property string userFormatString: "$1 \n$2\n$3\n$4\n$5\n$6\n$7\n$8\n$9\n$+"
|
|
|
|
// For undo/redo
|
|
property var history: 0
|
|
property var modified: false
|
|
|
|
//---------------------------------------------------------
|
|
// FINGERING DICTIONARY
|
|
// Last bit = plus sign indicator
|
|
//---------------------------------------------------------
|
|
|
|
//Dictionary for your whistle, specifying the note and the fingering pattern (starting from the fipple end).
|
|
//2 indicates a closed hole, 1 indicates a half hole, 0 indicates open hole. The last bit is a reserved flag for whether a (+) symbol should be drawn.
|
|
property var fingeringDict: ({
|
|
"G5": "1111111111",
|
|
"A5": "1111111100",
|
|
"B5": "1111111000",
|
|
"C6": "1111110000",
|
|
"D6": "1111100000",
|
|
"E6": "1111000000",
|
|
"F#6": "1110000000",
|
|
"G6": "1100000001"
|
|
})
|
|
|
|
onRun: {
|
|
if (!curScore) {
|
|
error("No score open.\nThis plugin requires an open score to run.\n")
|
|
quit()
|
|
}
|
|
}
|
|
|
|
function getHistory() {
|
|
if (history == 0) {
|
|
history = new commandHistory()
|
|
}
|
|
return history
|
|
}
|
|
|
|
function error(errorMessage) {
|
|
errorDialog.text = qsTr(errorMessage)
|
|
errorDialog.open()
|
|
}
|
|
|
|
SystemPalette {
|
|
id: systemPalette
|
|
onWindowTextChanged: {
|
|
// Detect if we're in dark mode by comparing text and background brightness
|
|
var textBrightness = getBrightness(windowText)
|
|
var windowBrightness = getBrightness(window)
|
|
isDarkMode = textBrightness > windowBrightness
|
|
}
|
|
Component.onCompleted: {
|
|
// Initial detection
|
|
var textBrightness = getBrightness(windowText)
|
|
var windowBrightness = getBrightness(window)
|
|
isDarkMode = textBrightness > windowBrightness
|
|
}
|
|
}
|
|
|
|
function getBrightness(color) {
|
|
// Simple brightness calculation (0-255)
|
|
return 0.299 * color.r * 255 + 0.587 * color.g * 255 + 0.114 * color.b * 255
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// MIDI → NOTE NAME
|
|
//---------------------------------------------------------
|
|
|
|
function pitchToName(midiPitch) {
|
|
var names = ["C","C#","D","D#","E","F",
|
|
"F#","G","G#","A","A#","B"]
|
|
|
|
var pitchClass = midiPitch % 12
|
|
var octave = Math.floor(midiPitch / 12) - 1
|
|
|
|
return names[pitchClass] + octave
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// BUILD ASCII DIAGRAM
|
|
//---------------------------------------------------------
|
|
|
|
//Build fingering text using binary representation of the fingering for a note.
|
|
//The last bit is the plus bit, indicating the first octave (0) or the second octave (1), to be annotated with a +
|
|
function buildFingeringText(binaryString, formatString) {
|
|
|
|
//-------------------------------------------------
|
|
// Basic validation
|
|
//-------------------------------------------------
|
|
|
|
if (!binaryString || binaryString.length < 2)
|
|
return "Invalid fingering pattern. Length is wrong"
|
|
|
|
if (!formatString || typeof formatString !== "string")
|
|
return "ERR"
|
|
|
|
var holeCount = binaryString.length - 1
|
|
var plusBit = binaryString[binaryString.length - 1]
|
|
|
|
//-------------------------------------------------
|
|
// Validate allowed characters
|
|
//-------------------------------------------------
|
|
|
|
for (var i = 0; i < holeCount; i++) {
|
|
if (binaryString[i] !== "0" &&
|
|
binaryString[i] !== "1" &&
|
|
binaryString[i] !== "2")
|
|
return "Invalid Fingering Pattern Value. Must be one of 0,1,2"
|
|
}
|
|
|
|
if (plusBit !== "0" && plusBit !== "1")
|
|
return "Invalid Plus Bit"
|
|
|
|
//-------------------------------------------------
|
|
// Clone format string
|
|
//-------------------------------------------------
|
|
|
|
var output = formatString
|
|
|
|
//-------------------------------------------------
|
|
// Replace numbered placeholders ($1 … $N)
|
|
//-------------------------------------------------
|
|
|
|
for (var i = 0; i < holeCount; i++) {
|
|
var symbol
|
|
if (binaryString[i] === "2")
|
|
symbol = "●"
|
|
else if (binaryString[i] === "1")
|
|
symbol = "◐"
|
|
else
|
|
symbol = "○"
|
|
|
|
// Use global replace to replace all occurrences
|
|
var token = "$" + (i+1)
|
|
while (output.indexOf(token) !== -1) {
|
|
output = output.replace(token, symbol)
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------
|
|
// Replace plus placeholder ($+)
|
|
//-------------------------------------------------
|
|
|
|
var plusSymbol = (plusBit === "1") ? "+" : " "
|
|
while (output.indexOf("$+") !== -1) {
|
|
output = output.replace("$+", plusSymbol)
|
|
}
|
|
|
|
//-------------------------------------------------
|
|
// Final validation
|
|
//-------------------------------------------------
|
|
|
|
if (output.indexOf("$") !== -1) {
|
|
console.log("Warning: Unreplaced placeholders in:", output)
|
|
return output // Return anyway with what we have
|
|
}
|
|
|
|
return output
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// APPLY TEXT FORMATTING
|
|
//---------------------------------------------------------
|
|
|
|
function formatText(text) {
|
|
console.log("Formatting text - setting fontSize to:", userFontSize)
|
|
text.fontSize = userFontSize
|
|
text.placement = Placement.BELOW
|
|
text.autoplace = false
|
|
text.offsetY = userOffsetY
|
|
text.lineSpacing = userLineSpacing // Multiply by 10 for better range
|
|
|
|
if (userJustification === 0)
|
|
text.align = Align.LEFT
|
|
else if (userJustification === 1)
|
|
text.align = Align.HCENTER
|
|
else
|
|
text.align = Align.RIGHT
|
|
}
|
|
|
|
//---------------------------------------------------------
|
|
// APPLY FINGERINGS
|
|
//---------------------------------------------------------
|
|
|
|
function applyFingerings() {
|
|
if (typeof curScore === "undefined") {
|
|
error("No score selected")
|
|
return false
|
|
}
|
|
|
|
console.log("=== Applying Fingerings ===")
|
|
console.log("userFontSize:", userFontSize)
|
|
console.log("userFormatString:", userFormatString)
|
|
console.log("userJustification:", userJustification)
|
|
console.log("userOffsetY:", userOffsetY)
|
|
console.log("userLineSpacing:", userLineSpacing)
|
|
|
|
curScore.startCmd()
|
|
|
|
var cursor = curScore.newCursor()
|
|
cursor.rewind(0)
|
|
|
|
var count = 0
|
|
while (cursor.segment) {
|
|
if (cursor.element && cursor.element.type === Element.CHORD) {
|
|
var chord = cursor.element
|
|
var midi = chord.notes[0].pitch
|
|
var noteName = pitchToName(midi)
|
|
|
|
var text = newElement(Element.STAFF_TEXT)
|
|
|
|
if (!fingeringDict[noteName]) { //note not in fingering dictionary
|
|
text.text = "☒"
|
|
console.log("Note not in dictionary:", noteName)
|
|
} else {
|
|
var diagram = buildFingeringText(fingeringDict[noteName], userFormatString)
|
|
text.text = diagram
|
|
console.log("Note:", noteName, "Diagram:", diagram.replace(/\n/g, "\\n"))
|
|
}
|
|
|
|
cursor.add(text)
|
|
formatText(text)
|
|
|
|
// Verify the text object has the properties set
|
|
console.log("Text fontSize after format:", text.fontSize)
|
|
console.log("Text offsetY after format:", text.offsetY)
|
|
console.log("Text lineSpacing after format:", text.lineSpacing)
|
|
|
|
count++
|
|
}
|
|
cursor.next()
|
|
}
|
|
|
|
curScore.endCmd()
|
|
console.log("Applied fingerings to", count, "notes")
|
|
console.log("=== Done ===")
|
|
return true
|
|
}
|
|
|
|
|
|
function getFirstNoteName() {
|
|
for (var note in fingeringDict) {
|
|
return note // Returns the first key in the dictionary
|
|
}
|
|
return "G5" // Fallback if dictionary is empty
|
|
}
|
|
|
|
function getPreviewText() {
|
|
var firstNote = getFirstNoteName()
|
|
if (fingeringDict[firstNote]) {
|
|
// Use the current formatInput.text instead of userFormatString
|
|
// to show real-time updates as the user types
|
|
return buildFingeringText(fingeringDict[firstNote], formatInput.text)
|
|
}
|
|
return "No preview available"
|
|
}
|
|
|
|
function updatePreview() {
|
|
previewLabel.text = getPreviewText()
|
|
}
|
|
|
|
function setUserFontSize(size) {
|
|
var oldSize = userFontSize
|
|
getHistory().add(
|
|
function() {
|
|
userFontSize = oldSize
|
|
fontSizeField.text = oldSize
|
|
previewLabel.font.pointSize = oldSize
|
|
},
|
|
function() {
|
|
userFontSize = size
|
|
fontSizeField.text = size
|
|
previewLabel.font.pointSize = size
|
|
},
|
|
"font size"
|
|
)
|
|
}
|
|
|
|
function setUserJustification(just) {
|
|
var oldJust = userJustification
|
|
getHistory().add(
|
|
function() { userJustification = oldJust; justCombo.currentIndex = oldJust },
|
|
function() { userJustification = just; justCombo.currentIndex = just },
|
|
"justification"
|
|
)
|
|
}
|
|
|
|
function setUserOffsetY(offset) {
|
|
var oldOffset = userOffsetY
|
|
getHistory().add(
|
|
function() {
|
|
userOffsetY = oldOffset
|
|
offsetField.text = oldOffset.toFixed(1)
|
|
},
|
|
function() {
|
|
userOffsetY = offset
|
|
offsetField.text = offset.toFixed(1)
|
|
},
|
|
"vertical offset"
|
|
)
|
|
}
|
|
|
|
function setUserLineSpacing(spacing) {
|
|
var oldSpacing = userLineSpacing
|
|
getHistory().add(
|
|
function() {
|
|
userLineSpacing = oldSpacing
|
|
spacingField.text = oldSpacing.toFixed(1)
|
|
},
|
|
function() {
|
|
userLineSpacing = spacing
|
|
spacingField.text = spacing.toFixed(1)
|
|
},
|
|
"line spacing"
|
|
)
|
|
}
|
|
|
|
function setUserFormatString(format) {
|
|
var oldFormat = userFormatString
|
|
getHistory().add(
|
|
function() { userFormatString = oldFormat; formatInput.text = oldFormat },
|
|
function() { userFormatString = format; formatInput.text = format },
|
|
"format string"
|
|
)
|
|
}
|
|
|
|
function setModified(state) {
|
|
var oldModified = modified
|
|
getHistory().add(
|
|
function() { modified = oldModified },
|
|
function() { modified = state },
|
|
"modified"
|
|
)
|
|
}
|
|
|
|
function formatStringChanged() {
|
|
getHistory().begin()
|
|
setModified(true)
|
|
setUserFormatString(formatInput.text)
|
|
getHistory().end()
|
|
}
|
|
|
|
function fontSizeChanged(size) {
|
|
getHistory().begin()
|
|
setModified(true)
|
|
setUserFontSize(size)
|
|
updatePreview()
|
|
getHistory().end()
|
|
}
|
|
|
|
function justificationChanged() {
|
|
getHistory().begin()
|
|
setModified(true)
|
|
setUserJustification(justCombo.currentIndex)
|
|
updatePreview()
|
|
getHistory().end()
|
|
}
|
|
|
|
function offsetYChanged() {
|
|
getHistory().begin()
|
|
setModified(true)
|
|
setUserOffsetY(offsetSpin.value / 10)
|
|
updatePreview()
|
|
getHistory().end()
|
|
}
|
|
|
|
function lineSpacingChanged() {
|
|
getHistory().begin()
|
|
setModified(true)
|
|
setUserLineSpacing(spacingSpin.value / 10)
|
|
updatePreview()
|
|
getHistory().end()
|
|
}
|
|
|
|
function formatCurrentValues() {
|
|
var data = {
|
|
fontSize: userFontSize,
|
|
justification: userJustification,
|
|
offsetY: userOffsetY,
|
|
lineSpacing: userLineSpacing,
|
|
formatString: userFormatString,
|
|
fingeringDict: fingeringDict
|
|
}
|
|
return JSON.stringify(data)
|
|
}
|
|
|
|
function restoreSavedValues(data) {
|
|
getHistory().begin()
|
|
setUserFontSize(data.fontSize)
|
|
setUserJustification(data.justification)
|
|
setUserOffsetY(data.offsetY)
|
|
setUserLineSpacing(data.lineSpacing)
|
|
setUserFormatString(data.formatString)
|
|
// Restore fingering dict if present
|
|
if (data.hasOwnProperty('fingeringDict')) {
|
|
fingeringDict = data.fingeringDict
|
|
}
|
|
getHistory().end()
|
|
}
|
|
|
|
Item {
|
|
anchors.fill: parent
|
|
|
|
GridLayout {
|
|
columns: 2
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
|
|
// Set up system palette for color management
|
|
SystemPalette { id: sysPal }
|
|
|
|
// TOP ROW - Text Formatting Options
|
|
// TOP ROW - Text Formatting Options
|
|
GroupBox {
|
|
title: "Text Formatting"
|
|
Layout.fillWidth: true
|
|
Layout.columnSpan: 2
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
label: Label {
|
|
text: parent.title
|
|
color: sysPal.windowText
|
|
background: Rectangle { color: sysPal.window }
|
|
}
|
|
|
|
GridLayout {
|
|
columns: 4
|
|
anchors.fill: parent
|
|
anchors.margins: 10
|
|
columnSpacing: 20
|
|
rowSpacing: 10
|
|
|
|
Label {
|
|
text: "Font Size:"
|
|
color: sysPal.windowText
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
// Font Size
|
|
TextField {
|
|
id: fontSizeField
|
|
Layout.preferredWidth: 80
|
|
text: userFontSize
|
|
color: sysPal.text
|
|
selectionColor: sysPal.highlight
|
|
selectedTextColor: sysPal.highlightedText
|
|
validator: IntValidator { bottom: 1; top: 72 }
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
|
|
// Handle Enter key press
|
|
onAccepted: {
|
|
var newValue = parseInt(text)
|
|
if (!isNaN(newValue) && newValue >= 1 && newValue <= 72) {
|
|
if (newValue !== userFontSize) {
|
|
fontSizeChanged(newValue)
|
|
}
|
|
} else {
|
|
text = userFontSize // Revert to previous value if invalid
|
|
}
|
|
}
|
|
|
|
// Handle focus loss
|
|
onActiveFocusChanged: {
|
|
if (!activeFocus) {
|
|
var newValue = parseInt(text)
|
|
if (!isNaN(newValue) && newValue >= 1 && newValue <= 72) {
|
|
if (newValue !== userFontSize) {
|
|
fontSizeChanged(newValue)
|
|
}
|
|
} else {
|
|
text = userFontSize // Revert to previous value if invalid
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update preview in real-time as user types
|
|
onTextChanged: {
|
|
var newValue = parseInt(text)
|
|
if (!isNaN(newValue) && newValue >= 1 && newValue <= 72) {
|
|
// Temporarily update preview without committing to history
|
|
previewLabel.font.pointSize = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "Justification:"
|
|
color: sysPal.windowText
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
ComboBox {
|
|
id: justCombo
|
|
Layout.preferredWidth: 120
|
|
model: ["Left", "Center", "Right"]
|
|
currentIndex: userJustification
|
|
onActivated: {
|
|
justificationChanged()
|
|
updatePreview()
|
|
}
|
|
contentItem: Text {
|
|
text: justCombo.displayText
|
|
color: sysPal.windowText
|
|
font: justCombo.font
|
|
horizontalAlignment: Text.AlignLeft
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "Vertical Offset:"
|
|
color: sysPal.windowText
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
// Vertical Offset
|
|
TextField {
|
|
id: offsetField
|
|
Layout.preferredWidth: 80
|
|
text: userOffsetY.toFixed(1)
|
|
color: sysPal.text
|
|
selectionColor: sysPal.highlight
|
|
selectedTextColor: sysPal.highlightedText
|
|
validator: DoubleValidator { bottom: -20.0; top: 20.0; decimals: 1 }
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
|
|
onAccepted: {
|
|
var newValue = parseFloat(text)
|
|
if (!isNaN(newValue) && newValue >= -200.0 && newValue <= 200.0) {
|
|
if (newValue !== userOffsetY) {
|
|
offsetYChanged(newValue)
|
|
updatePreview()
|
|
}
|
|
} else {
|
|
text = userOffsetY.toFixed(1)
|
|
}
|
|
}
|
|
|
|
onActiveFocusChanged: {
|
|
if (!activeFocus) {
|
|
var newValue = parseFloat(text)
|
|
if (!isNaN(newValue) && newValue >= -200.0 && newValue <= 200.0) {
|
|
if (newValue !== userOffsetY) {
|
|
offsetYChanged(newValue)
|
|
updatePreview()
|
|
}
|
|
} else {
|
|
text = userOffsetY.toFixed(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "Line Spacing:"
|
|
color: sysPal.windowText
|
|
Layout.alignment: Qt.AlignRight
|
|
}
|
|
// Line Spacing
|
|
TextField {
|
|
id: spacingField
|
|
Layout.preferredWidth: 80
|
|
text: userLineSpacing.toFixed(1)
|
|
color: sysPal.text
|
|
selectionColor: sysPal.highlight
|
|
selectedTextColor: sysPal.highlightedText
|
|
validator: DoubleValidator { bottom: 0; top: 6; decimals: 1 }
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
|
|
onAccepted: {
|
|
var newValue = parseFloat(text)
|
|
if (!isNaN(newValue) && newValue >= 0 && newValue <= 6) {
|
|
if (newValue !== userLineSpacing) {
|
|
lineSpacingChanged(newValue)
|
|
updatePreview()
|
|
}
|
|
} else {
|
|
text = userLineSpacing.toFixed(1)
|
|
}
|
|
}
|
|
|
|
onActiveFocusChanged: {
|
|
if (!activeFocus) {
|
|
var newValue = parseFloat(text)
|
|
if (!isNaN(newValue) && newValue >= 0 && newValue <= 6.0) {
|
|
if (newValue !== userLineSpacing) {
|
|
lineSpacingChanged(newValue)
|
|
updatePreview()
|
|
}
|
|
} else {
|
|
text = userLineSpacing.toFixed(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MIDDLE ROW - Save/Load/Undo/Redo Buttons
|
|
RowLayout {
|
|
Layout.columnSpan: 2
|
|
Layout.alignment: Qt.AlignRight
|
|
spacing: 10
|
|
Layout.topMargin: 10
|
|
Layout.bottomMargin: 10
|
|
|
|
Button {
|
|
id: saveButton
|
|
text: qsTranslate("PrefsDialogBase", "Save Settings")
|
|
contentItem: Text {
|
|
text: saveButton.text
|
|
color: sysPal.buttonText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.button
|
|
border.color: sysPal.mid
|
|
}
|
|
onClicked: {
|
|
saveDialog.folder = filePath
|
|
saveDialog.visible = true
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: loadButton
|
|
text: qsTranslate("PrefsDialogBase", "Load Settings")
|
|
contentItem: Text {
|
|
text: loadButton.text
|
|
color: sysPal.buttonText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.button
|
|
border.color: sysPal.mid
|
|
}
|
|
onClicked: {
|
|
loadDialog.folder = filePath
|
|
loadDialog.visible = true
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: undoButton
|
|
text: qsTranslate("PrefsDialogBase", "Undo")
|
|
contentItem: Text {
|
|
text: undoButton.text
|
|
color: sysPal.buttonText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.button
|
|
border.color: sysPal.mid
|
|
}
|
|
onClicked: {
|
|
getHistory().undo()
|
|
// Update text fields after undo
|
|
fontSizeField.text = userFontSize
|
|
offsetField.text = userOffsetY.toFixed(1)
|
|
spacingField.text = userLineSpacing.toFixed(1)
|
|
justCombo.currentIndex = userJustification
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: redoButton
|
|
text: qsTranslate("PrefsDialogBase", "Redo")
|
|
contentItem: Text {
|
|
text: redoButton.text
|
|
color: sysPal.buttonText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.button
|
|
border.color: sysPal.mid
|
|
}
|
|
onClicked: {
|
|
getHistory().redo()
|
|
// Update text fields after redo
|
|
fontSizeField.text = userFontSize
|
|
offsetField.text = userOffsetY.toFixed(1)
|
|
spacingField.text = userLineSpacing.toFixed(1)
|
|
justCombo.currentIndex = userJustification
|
|
}
|
|
}
|
|
}
|
|
|
|
// BOTTOM ROW - Format String and Preview
|
|
GroupBox {
|
|
title: "Format String & Preview"
|
|
Layout.fillWidth: true
|
|
Layout.columnSpan: 2
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
label: Label {
|
|
text: parent.title
|
|
color: sysPal.windowText
|
|
background: Rectangle { color: sysPal.window }
|
|
}
|
|
|
|
RowLayout {
|
|
width: parent.width
|
|
spacing: 20
|
|
anchors.margins: 10
|
|
|
|
// Format String Area (left side)
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
spacing: 5
|
|
|
|
Label {
|
|
text: "Format String:"
|
|
color: sysPal.windowText
|
|
font.bold: true
|
|
}
|
|
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.preferredHeight: 200
|
|
clip: true
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
|
|
TextArea {
|
|
id: formatInput
|
|
width: parent.width
|
|
height: contentHeight
|
|
wrapMode: TextEdit.Wrap
|
|
text: userFormatString
|
|
color: sysPal.text
|
|
selectionColor: sysPal.highlight
|
|
selectedTextColor: sysPal.highlightedText
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
property var previousText: userFormatString
|
|
onEditingFinished: {
|
|
formatStringChanged()
|
|
updatePreview()
|
|
}
|
|
onTextChanged: {
|
|
updatePreview()
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "Use placeholders like $1, $2, ... and $+ for the plus indicator"
|
|
font.pixelSize: 10
|
|
color: sysPal.windowText
|
|
}
|
|
}
|
|
|
|
// Preview Area (right side)
|
|
GroupBox {
|
|
title: "Preview (" + getFirstNoteName() + ")"
|
|
Layout.preferredWidth: 200
|
|
Layout.fillHeight: true
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
label: Label {
|
|
text: parent.title
|
|
color: sysPal.windowText
|
|
background: Rectangle { color: sysPal.window }
|
|
}
|
|
|
|
ScrollView {
|
|
anchors.fill: parent
|
|
anchors.margins: 5
|
|
clip: true
|
|
background: Rectangle {
|
|
color: sysPal.window
|
|
border.color: sysPal.mid
|
|
}
|
|
|
|
Label {
|
|
id: previewLabel
|
|
text: getPreviewText()
|
|
font.family: "Courier"
|
|
font.pointSize: userFontSize
|
|
color: sysPal.windowText
|
|
wrapMode: Text.WordWrap
|
|
width: parent.width
|
|
lineHeight: userLineSpacing
|
|
lineHeightMode: Text.ProportionalHeight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// BOTTOM BUTTONS - Apply and Cancel
|
|
RowLayout {
|
|
Layout.columnSpan: 2
|
|
Layout.alignment: Qt.AlignRight
|
|
spacing: 10
|
|
Layout.topMargin: 10
|
|
|
|
Button {
|
|
id: applyButton
|
|
text: qsTranslate("PrefsDialogBase", "Apply")
|
|
contentItem: Text {
|
|
text: applyButton.text
|
|
color: sysPal.buttonText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.button
|
|
border.color: sysPal.mid
|
|
}
|
|
onClicked: {
|
|
if (applyFingerings()) {
|
|
if (modified) {
|
|
quitDialog.open()
|
|
} else {
|
|
quit()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Button {
|
|
id: cancelButton
|
|
text: qsTranslate("PrefsDialogBase", "Cancel")
|
|
contentItem: Text {
|
|
text: cancelButton.text
|
|
color: sysPal.buttonText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
background: Rectangle {
|
|
color: sysPal.button
|
|
border.color: sysPal.mid
|
|
}
|
|
onClicked: {
|
|
if (modified) {
|
|
quitDialog.open()
|
|
} else {
|
|
quit()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
MessageDialog {
|
|
id: errorDialog
|
|
title: "Error"
|
|
text: ""
|
|
onAccepted: {
|
|
errorDialog.close()
|
|
}
|
|
}
|
|
|
|
MessageDialog {
|
|
id: quitDialog
|
|
title: "Quit?"
|
|
text: "Do you want to quit the plugin?"
|
|
detailedText: "You have unsaved changes. You can save your settings to a file before quitting if you like."
|
|
standardButtons: [StandardButton.Ok, StandardButton.Cancel]
|
|
onAccepted: {
|
|
quit()
|
|
}
|
|
onRejected: {
|
|
quitDialog.close()
|
|
}
|
|
}
|
|
|
|
FileIO {
|
|
id: saveFile
|
|
source: ""
|
|
}
|
|
|
|
FileIO {
|
|
id: loadFile
|
|
source: ""
|
|
}
|
|
|
|
function getFile(dialog) {
|
|
return dialog.filePath
|
|
}
|
|
|
|
FileDialog {
|
|
id: loadDialog
|
|
title: "Load Settings"
|
|
onAccepted: {
|
|
loadFile.source = getFile(loadDialog)
|
|
var data = JSON.parse(loadFile.read())
|
|
restoreSavedValues(data)
|
|
loadDialog.visible = false
|
|
}
|
|
onRejected: {
|
|
loadDialog.visible = false
|
|
}
|
|
visible: false
|
|
}
|
|
|
|
FileDialog {
|
|
id: saveDialog
|
|
title: "Save Settings"
|
|
onAccepted: {
|
|
saveFile.source = getFile(saveDialog)
|
|
saveFile.write(formatCurrentValues())
|
|
saveDialog.visible = false
|
|
}
|
|
onRejected: {
|
|
saveDialog.visible = false
|
|
}
|
|
visible: false
|
|
}
|
|
|
|
// Command pattern for undo/redo
|
|
function commandHistory() {
|
|
function Command(undo_fn, redo_fn, label) {
|
|
this.undo = undo_fn
|
|
this.redo = redo_fn
|
|
this.label = label // for debugging
|
|
}
|
|
|
|
var history = []
|
|
var index = -1
|
|
var transaction = 0
|
|
var maxHistory = 30
|
|
|
|
function newHistory(commands) {
|
|
if (index < maxHistory) {
|
|
index++
|
|
history = history.slice(0, index)
|
|
} else {
|
|
history = history.slice(1, index)
|
|
}
|
|
history.push(commands)
|
|
}
|
|
|
|
this.add = function(undo, redo, label) {
|
|
var command = new Command(undo, redo, label)
|
|
command.redo()
|
|
if (transaction) {
|
|
history[index].push(command)
|
|
} else {
|
|
newHistory([command])
|
|
}
|
|
}
|
|
|
|
this.undo = function() {
|
|
if (index != -1) {
|
|
history[index].slice().reverse().forEach(
|
|
function(command) {
|
|
command.undo()
|
|
}
|
|
)
|
|
index--
|
|
}
|
|
}
|
|
|
|
this.redo = function() {
|
|
if ((index + 1) < history.length) {
|
|
index++
|
|
history[index].forEach(
|
|
function(command) {
|
|
command.redo()
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
this.begin = function() {
|
|
if (transaction) {
|
|
throw new Error("already in transaction")
|
|
}
|
|
newHistory([])
|
|
transaction = 1
|
|
}
|
|
|
|
this.end = function() {
|
|
if (!transaction) {
|
|
throw new Error("not in transaction")
|
|
}
|
|
transaction = 0
|
|
}
|
|
}
|
|
}
|