/////////////////////////////////////////////////////////////////////////
// AccordionButtons: tests
/////////////////////////////////////////////////////////////////////////


/////////////////////////////////////////////////////////////////////////
// AccordionButtons: javascript modules
/////////////////////////////////////////////////////////////////////////



// globals ...

// list of translation tables to try (in priority order)
var xtrs = null;

// translation tables imported as specified in configuration
// (AccordionButtons.Translations)

var translationTables = {   "fr": {
    // French translations thanks to Marc Duval
    "++8ve": "++8ve",
    "+8ve": "+8ve",
    "--8ve": "--8ve",
    "-8ve": "-8ve",
    "Alternatives": "Alternatives",
    "Bold": "Gras",
    "BoldItalic": "GrasItalique",
    "CABD": "CABD",
    "Chord": "Accord",
    "Chord Style": "Style d'Accords",
    "Combined": "Combinée",
    "Concert": "Concert",
    "Default": "Défaut",
    "Font": "Police",
    "French/Irish Folk": "Folk français/irlandais",
    "Grey": "Gris",
    "Harmonica": "Harmonica",
    "Instrument": "Instrument",
    "Italic": "Italique",
    "Label": "Description",
    "LanguageSym": "[FR]",
    "Not Shown": "Non montré",
    "Oops. Something went wrong.": "Oups. Quelque chose a mal tourné.",
    "Pad": "Remplissez",
    "Preset": "Préréglage",
    "Pull": "Tiré",
    "Push": "Poussé",
    "Regular": "Régulier",
    "Row": "Rangée",
    "Size/Style": "Taille/Style",
    "Variant": "Variante",
    "Blues Harp": "Harmonica Blues",
    "Chromatic Harmonica": "Harmonica Chromatique",
    "Handry 12 Bass 3 Row Accordion": "Accordéon Handry 12 Basse 3 Rangée",
    "Lyric Font": "Police des Paroles",
    "N/A": "N/D",
    "Please report error to": "Veuillez signaler les erreurs à",
    "Simple": "Simple",
    "The AccordionButtons plugin requires MuseScore 4 or later": "Le plugin AccordionButtons nécessite MuseScore 4 ou une version supérieure",
    "chord includes both push and pull notes": "l'accord comprend à la fois des notes en poussée et tirée",
    "instrument can not play this note": "l'instrument ne peut pas jouer cette note",
    "no playable options": "aucune option jouable",
    "Diatonic Accordion, 1 Row (10 Button)": "Accordéon Diatonique 1 Rangée (10 Boutons)",
    "Diatonic Accordion, 2 Row (21 Button)": "Accordéon Diatonique 2 Rangées (21 Boutons)",
    "Diatonic Accordion, 2 Row (23 Button)": "Accordéon Diatonique 2 Rangées (23 Boutons)",
    "Piano Accordion (chords/bass)": "Accordéon Piano (accords/basses)",
    "note out of range": "note hors de portée",
    "A new version of the AccordionButtons plugin has been installed. Saved setting have been reset to the default values.": "Une nouvelle version du plugin AccordionButtons a été installée. Les paramètres enregistrés ont été réinitialisés aux valeurs par défaut.",
    "Contributed by": "Contribué par",
    "A(3)/D(3)": "La(3)/Ré(3)",
    "B♭(3)/E♭(3)": "Si♭(3)/Mi♭(3)",
    "C(3)": "Do(3)",
    "C(4)/B(3)": "Do(4)/Si(3)",
    "C(5)/B(4)": "Do(5)/Si(4)",
    "D(3)": "Ré(3)",
    "D(4)/C♯(3)": "Ré(4)/Do♯(3)",
    "D(5)/C♯(4)": "Ré(5)/Do♯(4)",
    "G(3)/C(3)": "Sol(3)/Do(3)",
    "A(2)/D(3)/G(3)": "La(2)/Ré((3)/Sol(3)",
    "B(3)/C(3)": "Si(3)/Do(3)",
    "C♯(3)/D(3)": "Do♯(3)/Ré((3)",
    "Diatonic Accordion, 3 Row (34 Button)": "Accordéon Diatonique 3 Rangées (34 Boutons)",
    "G(2)/C(3)/F(3)": "Sol(2)/Do(3)/Fa(3)",
    // "A": "A",
    // "A♭": "A♭",
    // "B": "B",
    // "B♭": "B♭",
    // "C": "C",
    // "C♯": "C♯",
    // "D": "D",
    // "E": "E",
    // "E♭": "E♭",
    // "F": "F",
    // "F♯": "F♯",
    // "G": "G",
  }
 };

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: translation
/////////////////////////////////////////////////////////////////////////

// none of this should be necessary because Musescore should
// read the standard Qt translation files from the plugin directory
// but it doesn't : https://github.com/musescore/MuseScore/issues/30833
// if that ever gets fixed, it should be possible to just replace the
// custom translation files with the Qt equivalents and this should
// continue to work because it calls qsTr() as a fallback

// return translation of given string into current language
function xsTr(s) {

  // build translation list on first call
  if (xtrs == null) xtrs = findTranslations();
  // try all the tables in order
  for (var i=0; i<xtrs.length; ++i) {
    const tr = xtrs[i];
    if ( tr[s] != null ) {

      return tr[s];
    }
  }
  // try the built-in Musescore translations


  // no Qt functions available when testing without GUI
  return s;

  return qsTr(s);
}

// return list of translation tables
// for current language
function findTranslations() {
  const xtrs = [];

  // no translation modules loaded when testing without GUI
  return xtrs;

  const locale = Qt.locale().name;
  const lang = locale ? locale.split("_")[0] : "en";
  // look several places for a translation (in order) ...

  // "en_GB" (eg.) in custom translation file
  if (CT.translations[locale] != null) {

    xtrs.push(CT.translations[locale]);
  }
  // "en" (eg.) in custom translation file
  if (CT.translations[lang] != null) {
 
    xtrs.push(CT.translations[lang]);
  }
  // "en-GB" (eg.) in built-in translation tables
  if (translationTables[locale] != null) {

    xtrs.push(translationTables[locale]);
  }
  // "en" (eg.) in built-in translation tables
  if (translationTables[lang] != null) {

    xtrs.push(translationTables[lang]);
  }

  return xtrs;
}

// return interface title string
// including any language symbol
function titleString() {
  var sym = xsTr("LanguageSym");
  if (sym == "LanguageSym") sym = "";
  else sym = sym+" ";
  const link = "<a href=\"https://www.nine3.org/downloads/home/accordion.html\" style=\"text-decoration: none;\">🌐</a>";
  return link+" "+sym+"Accordion Buttons 2.3";
}


// instrument tables imported as specified in configuration
// (AccordionButtons.Instruments)

var instrumentTables = [ // Accordéon Diatonique 10 boutons
// contributed by Marc Duval <mduvy@hotmail.com>

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html
{
  name: xsTr("Diatonic Accordion, 1 Row (10 Button)"),
  author: "Marc Duval",
  id: "10button",
  keys: [
    { name: xsTr("C(3)"), id: "C" },
    { name: xsTr("D(3)"), transpose: 2, id: "D" },
  ],
  staves: {
    treble: {
      min: 52,
      max: 88,
      notes: [
        "+1",        // E3  Mi
        "?",         // F3  Fa
        "?",         // F3# Fa#
        "+2|-1",     // G3  Sol
        "?",         // G3# Sol#
        "?",         // A3  La
        "?",         // A3# La#
        "-2",        // B3  Si
        "+3",        // C4  Do (60 middle C)
        "?",         // C4# Do#
        "-3",        // D4  Ré
        "?",         // D4# Ré#
        "+4",        // E4  Mi
        "-4",        // F4  Fa
        "?",         // F4# Fa#
        "+5",        // G4  Sol
        "?",         // G4# Sol#
        "-5",        // A4  La
        "?",         // A4# La#
        "-6",        // B4  Si
        "+6",        // C5  Do
        "?",         // C5# Do#
        "-7",        // D5  Ré

        "?",         // D5# Ré#
        "+7",        // E5  Mi
        "-8",        // F5  Fa
        "?",         // F5# Fa#
        "+8",        // G5  Sol
        "?",         // G5# Sol#
        "-9",        // A5  La
        "?",         // A5# La#
        "-10",       // B5  Si
        "+9",        // C6  Do
        "?",         // C6# Do#
        "?",         // D6  Ré
        "?",         // D6# Ré#
        "+10",       // E6  Mi
      ],
    },
    bass: {
      auto: true,
    },
  },
}
,// Accordéon Diatonique 21 boutons
// contributed by Marc Duval <mduvy@hotmail.com>

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html
{
  name: xsTr("Diatonic Accordion, 2 Row (21 Button)"),
  author: "Marc Duval & Paul Anderson",
  id: "21button",
  keys: [
    { name: xsTr("B(3)/C(3)"), staves: "paul", id: "BC" },
    { name: xsTr("C♯(3)/D(3)"), staves: "paul", transpose: 2, id: "C#D" },
    { name: xsTr("G(3)/C(3)"), staves: "marc1", id: "GC" },
    { name: xsTr("A(3)/D(3)"), staves: "marc1", transpose: 2, id: "AD" },
    { name: xsTr("B♭(3)/E♭(3)"), staves: "marc1", transpose: 3, id: "BbEb" },
    { name: xsTr("D(4)/C♯(3)"), staves: "marc2", id: "DC#" },
    { name: xsTr("C(4)/B(3)"), staves: "marc2", transpose: -2, id: "CB" },
  ],
  staves_marc1: {
     treble: {
      min: 50,
      max: 88,
      notes: [
        "+2",        // D3  Ré
        "?",         // D3# Ré#
        "?",         // E3  Mi
        "?",         // F3  Fa
        "-2",        // F3# Fa#
        "+3|+2^",    // G3  Sol
        "?",         // G3# Sol#
        "-3",        // A3  La
        "?",         // A3# La#
        "+4|-2^",    // B3  Si
        "-4|+3^",    // C4  Do (60 middle C)
        "+1",        // C4# Do#
        "+5|-3^",    // D4  Ré
        "-1",        // D4# Ré#
        "-5|+4^",    // E4  Mi
        "-4^",       // F4  Fa
        "-6",        // F4# Fa#
        "+6|+5^",    // G4  Sol
        "-1^",       // G4# Sol#
        "-7|-5^",    // A4  La
        "+1^",       // A4# La#
        "+7|-6^",    // B4  Si
        "-8|+6^",    // C5  Do
        "?",         // C5# Do#
        "+8|-7^",    // D5  Ré
        "?",         // D5# Ré#
        "-9|+7^",    // E5  Mi
        "-8^",       // F5  Fa
        "-10",       // F5# Fa#
        "+9|+8^",    // G5  Sol
        "?",         // G5# Sol#
        "-11|-9^",   // A5  La
        "?",         // A5# La#
        "+10|-10^",  // B5  Si
        "+9^",       // C6  Do
        "?",         // C6# Do#
        "+11",       // D6  Ré
        "?",         // D6# Ré#
        "+10^",      // E6  Mi
      ],
    },
    bass: {
      auto: true,
    },
  },
  staves_marc2: {
    treble: {
      min: 50,
      max: 90,
      notes: [
        "+1",        // D3  Ré (50)
        "?",         // D3# Ré#
        "?",         // E3  Mi
        "+1^",       // F3  Fa
        "+2",        // F3# Fa#
        "-1",        // G3  Sol
        "+2^",       // G3# Sol#
        "+3",        // A3  La
        "-1^",       // A3# La#
        "-2",        // B3  Si
        "-2^",       // C4  Do (60 middle C)
        "-3|+3^",    // C4# Do#
        "+4",        // D4  Ré
        "-3^",       // D4# Ré#
        "-4",        // E4  Mi
        "+4^",       // F4  Fa
        "+5|-4^",    // F4# Fa#
        "-5",        // G4  Sol
        "+5^",       // G4# Sol#
        "+6",        // A4  La
        "-5^",       // A4# La#
        "-6",        // B4  Si
        "-6^",       // C5  Do
        "-7|+6^",    // C5# Do#
        "+7",        // D5  Ré
        "-7^",       // D5# Ré#
        "-8",        // E5  Mi
        "+7^",       // F5  Fa
        "+8|-8^",    // F5# Fa#
        "-9",        // G5  Sol
        "+8^",       // G5# Sol#
        "+9",        // A5  La
        "-9^",       // A5# La#
        "-10",       // B5  Si
        "-10^",      // C6  Do
        "-11|+9^",   // C6# Do#
        "+10",       // D6  Ré
        "?",         // D6# Ré#
        "?",         // E6  Mi
        "+10^",      // F6  Fa
        "+11",       // F6# Fa# (90)
      ],
    },
    bass: {
      auto: true,
    },
  },
  staves_paul: {
    treble: {
      min: 51,
      max: 90,
      notes: [
        "+1^",      // Eb (51)
        "+1",       // E
        "?",        // F
        "+2^",      // F#
        "+2",       // G
        "-1^",      // G#
        "-1",       // A
        "-2",       // Bb
        "+3^|-2",   // B
        "+3",       // (middle) C
        "-3^",      // C#
        "-3",       // D
        "+4^",      // Eb
        "+4|-4^",   // E
        "-4",       // F
        "+5^",      // F#
        "+5",       // G
        "-5^",      // G#
        "-5",       // A
        "-6^",      // Bb
        "+6^|-6",   // B
        "+6",       // C
        "-7^",      // C#
        "-7",       // D
        "+7^",      // Eb
        "+7|-8^",   // E
        "-8",       // F
        "+8^",      // F#
        "+8",       // G
        "-9^",      // G#
        "-9",       // A
        "-10^",     // Bb
        "+9^|-10",  // B
        "+9",       // C
        "-11^",     // C#
        "?",        // D
        "+10^",     // Eb
        "+10",      // E
        "?",        // F
        "+11",      // F# (90)
      ],
    },
    bass: {
      auto: true,
    },
  },
}
,// Accordéon Diatonique 23 boutons
// contributed by Marc Duval <mduvy@hotmail.com>

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html
{
  name: xsTr("Diatonic Accordion, 2 Row (23 Button)"),
  author: "Marc Duval",
  id: "23button",
  keys: [
    { name: xsTr("D(5)/C♯(4)") },
    { name: xsTr("C(5)/B(4)"), transpose: -2 },
    { name: xsTr("D(4)/C♯(3)"), staves: "A" },
    { name: xsTr("C(4)/B(3)"), transpose: -2, staves: "A" },
  ],
  staves: {
    treble: {
      min: 45,
      max: 90,
      notes: [
        "+1",        // A2  La (45)
        "?",         // A2# La#
        "?",         // B2  Si
        "?",         // C3  Do
        "+1^",       // C3# Do#
        "+2",        // D3  Ré
        "?",         // D3# Ré#
        "-1",        // E3  Mi
        "+2^",       // F3  Fa
        "+3|-1^",    // F3# Fa#
        "-2",        // G3  Sol
        "+3^",       // G3# Sol#
        "+4",        // A3  La
        "-2^",       // A3# La#
        "-3",        // B3  Si
        "-3^",       // C4  Do (60 middle C)
        "-4|+4^",    // C4# Do#
        "+5",        // D4  Ré
        "-4^",       // D4# Ré#
        "-5",        // E4  Mi
        "+5^",       // F4  Fa
        "+6|-5^",    // F4# Fa#
        "-6",        // G4  Sol
        "+6^",       // G4# Sol#
        "+7",        // A4  La
        "-6^",       // A4# La#
        "-7",        // B4  Si
        "-7^",       // C5  Do
        "-8|+7^",    // C5# Do#
        "+8",        // D5  Ré
        "-8^",       // D5# Ré#
        "-9",        // E5  Mi
        "+8^",       // F5  Fa
        "+9|-9^",    // F5# Fa#
        "-10",       // G5  Sol
        "+9^",       // G5# Sol#
        "+10",       // A5  La
        "-10^",      // A5# La#
        "-11",       // B5  Si
        "-11^",      // C6  Do
        "-12|+10^",  // C6# Do#
        "+11",       // D6  Ré
        "?",         // D6# Ré#
        "?",         // E6  Mi
        "+11^",      // F6  Fa
        "+12",       // F6# Fa# (90)
      ],
    },
    bass: {
      auto: true,
    },
  },

  staves_A: {
    treble: {
      min: 50,
      max: 93,
      notes: [
        "+1",        // D3  Ré (50)
        "?",         // D3# Ré#
        "?",         // E3  Mi
        "+1^",       // F3  Fa
        "+2",        // F3# Fa#
        "-1",        // G3  Sol
        "+2^",       // G3# Sol#
        "+3",        // A3  La
        "-1^",       // A3# La#
        "-2",        // B3  Si
        "-2^",       // C4  Do (60 middle C)
        "-3|+3^",    // C4# Do#
        "+4",        // D4  Ré
        "-3^",       // D4# Ré#
        "-4",        // E4  Mi
        "+4^",       // F4  Fa
        "+5|-4^",    // F4# Fa#
        "-5",        // G4  Sol
        "+5^",       // G4# Sol#
        "+6",        // A4  La
        "-5^",       // A4# La#
        "-6",        // B4  Si
        "-6^",       // C5  Do
        "-7|+6^",    // C5# Do#
        "+7",        // D5  Ré
        "-7^",       // D5# Ré#
        "-8",        // E5  Mi
        "+7^",       // F5  Fa
        "+8|-8^",    // F5# Fa#
        "-9",        // G5  Sol
        "+8^",       // G5# Sol#
        "+9",        // A5  La
        "-9^",       // A5# La#
        "-10",       // B5  Si
        "-10^",      // C6  Do
        "-11|+9^",   // C6# Do#
        "+10",       // D6  Ré
        "-11^",      // D6# Ré#
        "-12",       // E6  Mi
        "+10^",      // F6  Fa
        "+11",       // F6# Fa#
        "?",         // G6  Sol
        "+11^",      // G6# Sol#
        "+12",       // A6  La (93)
      ],
    },
    bass: {
      auto: true,
    },
  },
}
,// Accordéon Diatonique 34 boutons
// contributed by Marc Duval <mduvy@hotmail.com>

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html
{
  name: xsTr("Diatonic Accordion, 3 Row (34 Button)"),
  author: "Marc Duval",
  id: "34button",
  keys: [
    { name: xsTr("G(2)/C(3)/F(3)"), id: "GCF" },
    { name: xsTr("A(2)/D(3)/G(3)"), transpose: 2, id: "ADG" },
  ],
  staves: {
    treble: {
      min: 55,
      max: 96,
      notes: [
        "+2|+2^",         // G3  Sol
        "?",              // G3# Sol#
        "-2",             // A3  La
        "?",              // A3# La#
        "+3|-2^",         // B3  Si
        "-3|+3^|+2^^",    // C4  Do (60 middle C)
        "+1",             // C4# Do#
        "+4|-3^",         // D4  Ré
        "-1",             // D4# Ré#
        "-4|+4^|-2^^",    // E4  Mi
        "-4^|+3^^",       // F4  Fa
        "-5|+1^",         // F4# Fa#
        "+5|+5^|-3^^",    // G4  Sol
        "-1^",            // G4# Sol#
        "-6|-5^|+4^^",    // A4  La
        "-4^^",           // A4# La#
        "+6|-6^",         // B4  Si
        "-7|+6^|+5^^",    // C5  Do
        "-1^^",           // C5# Do#
        "+7|-7^|-5^^",    // D5  Ré
        "+1^^",           // D5# Ré#
        "-8|+7^|-6^^",    // E5  Mi
        "-8^|+6^^",       // F5  Fa
        "-9",             // F5# Fa#
        "+8|+8^|-7^^",    // G5  Sol
        "?",              // G5# Sol#
        "-10|-9^|+7^^",   // A5  La
        "-8^^",           // A5# La#
        "+9|-10^",        // B5  Si
        "-11|+9^|+8^^",   // C6  Do
        "?",              // C6# Do#
        "+10|-11^|-9^^",  // D6  Ré
        "?",              // D6# Ré#
        "+10^|-10^^",     // E6  Mi
        "-12^|+9^^",      // F6  Fa
        "?",              // F6# Fa#
        "+11|+11^|-11^^", // G6  Sol
        "?",              // G6# Sol#
        "+10^^",          // A6  La
        "?",              // A6# La#
        "?",              // B6  Si
        "+12^|+11^^",     // C7  Do
      ],
    },
    bass: {
      auto: true,
    },
  },
}
,
// Handry12 Bass 3 row accordion
// contributed by Daniel Meyer <daniel_meyer_dm@icloud.com>

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html

{
  name: xsTr("Handry 12 Bass 3 Row Accordion"),
  id: "handry3",
  author: "Daniel Meyer",
  staves: {
    treble: {
      min: 47,
      max: 92,
      notes: [
       "-1",            // B (47)
       "?",             // C
       "?",             // C#
       "-2",            // D
       "?",             // D#
       "+1|-1^",        // E
       "?",             // F
       "+2",            // F#
       "-3|+1^|-2^",    // G
       "-1^^",          // G#
       "+3|-2^^",       // A
       "+1^^",          // A#
       "-4|+2^",        // B
       "+4|-3^",        // C
       "+2^^",          // C#
       "-5|+3^",        // D
       "-3^^",          // D#
       "+5|-4^",        // E
       "+4^",           // F
       "+6",            // F#
       "-6|-5^|+3^^",   // G
       "-4^^",          // G#
       "+7|+5^|-5^^",   // A
       "+5^^",          // A#
       "-7|+6^",        // B
       "+8|-6^",        // C
       "+6^^",          // C#
       "-8|+7^",        // D
       "-6^^",          // D#
       "+9|-7^",        // E
       "+8^",           // F
       "+10",           // F#
       "-9|-8^|+7^^",   // G
       "-7^^|+8^^",     // G#
       "+11|+9^|-8^^",  // A
       "+9^^",          // A#
       "-10|+10^",      // B
       "+12|-9^",       // C
       "+10^^",         // C#
       "-11|+11^",      // D
       "-9^^",          // D#
       "-10^",          // E
       "?",             // F
       "?",             // F#
       "-12|-11^",      // G
       "-10^^",         // G# (92)
      ],
    },
    bass: {
      auto: true,
    },
  },
}
,// this is the left hand only for piano accordion

{
  name: xsTr("Piano Accordion (chords/bass)"),
  id: "piano",
  staves: {
    bass: {
      auto: true,
    },
  },
}
,// blues harp

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html

{
  name: xsTr("Blues Harp"),
  id: "bluesharp",
  info:`
This is a <i>Blues Harp</i> or <i>Richter-Tuned Harmonica</i>.<br/>
See the <a href="https://en.wikipedia.org/wiki/Richter-tuned_harmonica">Wikipedia Page</a>.
`,
  keys: [
    { name: xsTr("B♭"), transpose: -2, id: "Bb" },
    { name: xsTr("F"), transpose: 5, id: "F" },
    { name: xsTr("C"), id: "C" },
    { name: xsTr("G"), transpose: -5, id: "G" },
    { name: xsTr("D"), transpose: 2, id: "D" },
    { name: xsTr("A"), transpose: -3, id: "A" },
    { name: xsTr("E"), transpose: 4, id: "E" },
  ],
  staves: {
    default: {
      min: 60,
      max: 96,
      notes: [
        "+1",  // C (60)
        "?",   // C#
        "-1",  // D
        "?",   // Eb
        "+2",  // E
        "?",   // F
        "?",   // F#
        "-2",  // G
        "?",   // G#
        "?",   // A
        "?",   // Bb
        "-3",  // B
        "+4",  // C
        "?",   // C#
        "-4",  // D
        "?",   // Eb
        "+5",  // E
        "-5",  // F
        "?",   // F#
        "+6",  // G
        "?",   // G#
        "-6",  // A
        "?",   // Bb
        "-7",  // B
        "+7",  // C
        "?",   // C#
        "-8",  // D
        "?",   // Eb
        "+8",  // E
        "-9",  // F
        "?",   // F#
        "+9",  // G
        "?",   // G#
        "-10", // A
        "?",   // Bb
        "?",   // B
        "+10", // C (96)
      ],
    },
  },
}
,// chromatic harmonica

// for the pitch numbers, see:
// https://musescore.github.io/MuseScore_PluginAPI_Docs/plugins/html/pitch.html

{
  name: xsTr("Chromatic Harmonica"),
  id: "chromatic_harmonica",
  info:`
This is a <i>Chromatic Harmonica</i>.<br/>
See the <a href="https://en.wikipedia.org/wiki/Chromatic_harmonica">Wikipedia Page</a>.
`,
  keys: [
    { name: xsTr("B♭"), transpose: -2, id: "Bb" },
    { name: xsTr("F"), transpose: 5, id: "F" },
    { name: xsTr("C"), id: "C" },
    { name: xsTr("G"), transpose: -5, id: "G" },
    { name: xsTr("D"), transpose: 2, id: "D" },
    { name: xsTr("A"), transpose: -3, id: "A" },
    { name: xsTr("E"), transpose: 4, id: "E" },
  ],
  staves: {
    default: {
      min: 60,
      max: 90,
      notes: [
        "+1",         // C (60)
        "+1^",        // C#
        "-1",         // D
        "-1^",        // Eb
        "+2",         // E
        "-2|+2^",     // F
        "-2^",        // F#
        "+3",         // G
        "+3^",        // G#
        "-3",         // A
        "-3^",        // Bb
        "-4",         // B
        "+4|-4^|+5",  // C
        "+4^|+5^",    // C#
        "-5",         // D
        "-5^",        // Eb
        "+6",         // E
        "-6|+6^",     // F
        "-6^",        // F#
        "+7",         // G
        "+7^",        // G#
        "-7",         // A
        "-7^",        // Bb
        "-7",         // B
        "+7|-8^|+9",  // C
        "+8^|+9^",    // C#
        "-9",         // D
        "-9^",        // Eb
        "+10",        // E
        "-10|+10^",   // F
        "-10^",       // F# (90)
      ],
    },
  },
}
, ];

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: abstract node superclass
/////////////////////////////////////////////////////////////////////////

class ABNode {

  constructor() {}

  // subclasses need to define these (if called)
  add() { throw("no "+this.constructor.name+".add()"); }
  asString() { throw("no "+this.constructor.name+".asString()"); }
  asDisplayString() { throw("no "+this.constructor.name+".asDisplayString()"); }
  compareIndex() { throw("no "+this.constructor.name+".compareIndex()"); }

  asList() { return [ this ];}    // return list of choices
  pickOne() { return this; }      // return one arbitrary choice
  isPush() { return false; }      // return true if this requires a "push" action
  isPull() { return false; }      // return true if this requires a "pull" action
  isPlayable() { return true; }   // return true if playable
  isOption() { return false; }    // return true if is list of choices
  isChord() { return false; }     // return true if explicit chord
  isNote() { return false; }      // return true if single notes
  isChordSym() { return false; }  // return true if symbolic chord
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: styles
/////////////////////////////////////////////////////////////////////////

var presets = {

  default: {
    name: xsTr("Default"),        // name (for menu)
    prePull: "",                  // symbol to appear before a "pull"
    postPull: "",                 // symbol to appear after a "pull"
    pullUnderscore: false,        // true to underscore "pull"s
    pullShift: false,             // true to shift "pull"s down
    prePush: "",                  // symbol to appear before a "push"
    postPush: "",                 // symbol to appear after a "push"
    pushUnderscore: true,         // true to underscore "push"s
    pushShift: false,             // true to shift "push"s down
    preRow: "",                   // row symbol to appear after annotation
    postRow: "'",                 // row symbol to appear before annotation
    preChord: "(",                // symbol to appear before chord
    postChord: ")",               // symbol to appear after a chord
    chordSeparator: "+",          // symbol to separate notes in a chord
    font: bestFont([]),           // font (default only)
    rowInside: true,              // true if row symbol appears inside push/pull symbols
    showLabel: true,              // true to label score with instrument name
    pad: false,                   // pad symbols with space at either side
    info: '',                     // html to display info box

    includeInstrument: true,
    instrumentIndex: menuIndexForInstrumentID("21button"),
    variantIndex: menuIndexForVariantID("GC","21button"),

    chordStyleIndex: menuIndexForChordStyle("Default"),
    fontSizeIndex: menuIndexForFontSize("12"),
    fontStyleIndex: menuIndexForFontStyle("Bold"),
    altStyleIndex: menuIndexForAltStyle("Regular"),
    octaveIndex: menuIndexForOctave("Concert"),

    // the following are not (currently) user-configurable

    altColour: "silver", // colour used for gray alternatives
  },

  magnus: {
    name: "Magnus",
    prePull: "",
    postPull: "O",
    pullUnderscore: false,
    pullShift: false,
    prePush: "",
    postPush: "I",
    pushUnderscore: false,
    pushShift: false,
    preRow: "[",
    postRow: "]",
    rowInside: false,
    font: bestFont([ 'Gill Sans MT Ext Condensed Bold' ]),
    fontSizeIndex: menuIndexForFontSize("20"),
    fontStyleIndex: menuIndexForFontStyle("Regular"),
    altStyleIndex: menuIndexForAltStyle("Grey"),
    octaveIndex: menuIndexForOctave("Concert"),
    chordStyleIndex: menuIndexForChordStyle("Simple"),
    showLabel: false,
    includeInstrument: true,
    instrumentIndex: menuIndexForInstrumentID("21button"),
    variantIndex: menuIndexForVariantID("BC","21button"),
    info:`
This is a style with a large font size. It is intended to use the
<code>Gill Sans extended</code> font which is not normally installed by default,
but is freely available available from the
<a href="https://www.freefontdownload.org/en/gill-sans-mt-extended-condensed-bold.font">FreeFontDownload</a> site.
`,
  },

  cabd: {
    name: xsTr("CABD"),
    prePull: "",
    postPull: "",
    pullUnderscore: false,
    pullShift: false,
    prePush: "",
    postPush: "",
    pushUnderscore: false,
    pushShift: true,
    preRow: "",
    postRow: "'",
    rowInside: false,
    font: bestFont([]),
    fontSizeIndex: menuIndexForFontSize("12"),
    fontStyleIndex: menuIndexForFontStyle("Bold"),
    altStyleIndex: menuIndexForAltStyle("Not Shown"),
    chordStyleIndex: menuIndexForChordStyle("Simple"),
    includeInstrument: false,
    info:`
This is a style which shows the <i>push</i> and <i>pull</i> notes on separate lines,
with the <i>push</i> notes higher than the <i>pull</i> notes.
`,
  },

  folk: {
    name: xsTr("French/Irish Folk"),
    prePull: "",
    postPull: "",
    pullUnderscore: false,
    pullShift: false,
    prePush: "",
    postPush: "",
    pushUnderscore: true,
    pushShift: false,
    preRow: "",
    postRow: "'",
    rowInside: false,
    font: bestFont([]),
    fontSizeIndex: menuIndexForFontSize("12"),
    fontStyleIndex: menuIndexForFontStyle("Bold"),
    altStyleIndex: menuIndexForAltStyle("Not Shown"),
    chordStyleIndex: menuIndexForChordStyle("Simple"),
    includeInstrument: false,
    info:`
This is a style which shows the <i>push</i> notes underlined
(and the <i>pull</i> notes not underlined).
`,
  },

  harmonica: {
    name: xsTr("Harmonica"),
    prePull: "",
    postPull: "↓",
    pullUnderscore: false,
    pullShift: false,
    prePush: "",
    postPush: "↑",
    pushUnderscore: false,
    pushShift: false,
    preRow: "",
    postRow: "'",
    rowInside: true,
    font: bestFont([]),
    fontSizeIndex: menuIndexForFontSize("12"),
    fontStyleIndex: menuIndexForFontStyle("Bold"),
    altStyleIndex: menuIndexForAltStyle("Not Shown"),
    chordStyleIndex: menuIndexForChordStyle("Default"),
    includeInstrument: false,
    info:`
This style is intended for the harmonica.
It shows the blow/suck notes as up/down arrows.
`,
  },

  // from Marc Duval <mduvy@hotmail.com>
  louis: {
    name: "Louis",
    prePull: "(",
    postPull: ")",
    pullUnderscore: false,
    pullShift: false,
    prePush: "",
    postPush: "",
    pushUnderscore: false,
    pushShift: false,
    preRow: "",
    postRow: "'",
    rowInside: true,
    font: bestFont([]),
    fontSizeIndex: menuIndexForFontSize("10"),
    fontStyleIndex: menuIndexForFontStyle("Regular"),
    altStyleIndex: menuIndexForAltStyle("Combined"),
    chordStyleIndex: menuIndexForChordStyle("Default"),
    showLabel: false,
    includeInstrument: false,
    info: '',
  },

  test: {
    name: "Test",
    prePull: "",
    postPull: "↓",
    pullUnderscore: false,
    pullShift: false,
    prePush: "",
    postPush: "↑",
    pushUnderscore: false,
    pushShift: false,
    preRow: "\[",
    postRow: "\]",
    rowInside: false,
    chordStyleIndex: 0,
    includeInstrument: false,
    info: '',
  },
};

// order in which presets are to appear in the menu
// ⚠️ the numerical index of these values gets saved so that we
// are independent of the language - but of you change the order,
// any saved value will be wrong
var presetMenuOrder = [
  'default', 'cabd', 'folk', 'harmonica', 'magnus', 'louis'
];

// return list of preset names for the menu
function presetMenuNames() {
  let names = new Array(0);
  presetMenuOrder.forEach(key => { names.push(presets[key].name) });
  return names;
}

// the default style provides fallback values
// for unspecified values in named styles
var defaultStyle = presets.default;

// the currently selected style
// this will contain values from one of the presets
// some of which will possibly be overridden with explicit values from the GUI
var currentStyle = defaultStyle;

// set the current style to the specified style index
// (as displayed in the menu)
// and update the dialog to match
function setStyleByIndex(index) {
  var key = presetMenuOrder[index];
  setCurrentStyleByKey(key);
  putStyleToDialog();
}

// set the current style from the specified key
// use the default style as fallback values for any unspecified values
function setCurrentStyleByKey(key) {
  var style = presets[key];
  var defaultStyle = presets.default;
  var newStyle = {};
  for (const [k,v] of Object.entries(defaultStyle)) {
    newStyle[k]
      = (style[k]!=null) ? style[k]
      : (currentStyle[k]!=null) ? currentStyle[k]
      : defaultStyle[k];
  }
  currentStyle = newStyle;
  configureStyleInfo();
}

// show/hide the style info button
function configureStyleInfo() {

  // no Qt functions available when testing without GUI
  return;

}



// update the current style to match the contents of the dialog
// do this immediately before we do any actions (eg. add annotations)
function getStyleFromDialog() {
  var key = presetMenuOrder[presetSelector.currentIndex];
  setCurrentStyleByKey(key);
  currentStyle.prePull = prePullSymInput.text;
  currentStyle.postPull = postPullSymInput.text;
  currentStyle.prePush = prePushSymInput.text;
  currentStyle.postPush = postPushSymInput.text;
  currentStyle.preRow = preRowSymInput.text;
  currentStyle.postRow = postRowSymInput.text;
  currentStyle.preChord = preChordSymInput.text;
  currentStyle.postChord = postChordSymInput.text;
  currentStyle.chordSeparator = chordSeparatorSymInput.text;
  currentStyle.font = fontButton.text;
  currentStyle.fontSizeIndex = fontSizeSelector.currentIndex;
  currentStyle.fontStyleIndex = fontStyleSelector.currentIndex;
  currentStyle.variantIndex = variantSelector.currentIndex;
  currentStyle.octaveIndex = octaveSelector.currentIndex;

  currentStyle.chordStyleIndex = chordStyleSelector.currentIndex;
  currentStyle.altStyleIndex = altStyleSelector.currentIndex;
  currentStyle.instrumentIndex = instrumentSelector.currentIndex;
  currentStyle.rowInside = dialogGrid.rowInside;
  currentStyle.pullUnderscore = pullUnderscore.checked;
  currentStyle.pushUnderscore = pushUnderscore.checked;
  currentStyle.pullShift = pullShift.checked;
  currentStyle.pushShift = pushShift.checked;
  currentStyle.showLabel = labelBox.checked;
  currentStyle.pad = padBox.checked;
  setAnnotationOrder();
}

// update the dialog to match the specified style
// do this if we have programatically loaded a new style
function putStyleToDialog() {
  prePushSymInput.text = currentStyle.prePush;
  postPushSymInput.text = currentStyle.postPush;
  prePullSymInput.text = currentStyle.prePull;
  postPullSymInput.text = currentStyle.postPull;
  preRowSymInput.text = currentStyle.preRow;
  postRowSymInput.text = currentStyle.postRow;
  preChordSymInput.text = currentStyle.preChord;
  postChordSymInput.text = currentStyle.postChord;
  chordSeparatorSymInput.text = currentStyle.chordSeparator;
  fontButton.text = currentStyle.font;
  fontSizeSelector.currentIndex = currentStyle.fontSizeIndex;
  fontStyleSelector.currentIndex = currentStyle.fontStyleIndex;
  altStyleSelector.currentIndex = currentStyle.altStyleIndex;
  chordStyleSelector.currentIndex = currentStyle.chordStyleIndex;
  dialogGrid.rowInside = currentStyle.rowInside;
  pullUnderscore.checked = currentStyle.pullUnderscore;
  pushUnderscore.checked = currentStyle.pushUnderscore;
  pullShift.checked = currentStyle.pullShift;
  pushShift.checked = currentStyle.pushShift;
  labelBox.checked = currentStyle.showLabel;
  padBox.checked = currentStyle.pad;
  if (currentStyle.includeInstrument) {
    instrumentSelector.currentIndex = currentStyle.instrumentIndex;
    setInstrument();
    variantSelector.currentIndex = currentStyle.variantIndex;
    octaveSelector.currentIndex = currentStyle.octaveIndex;
  }
  setAnnotationOrder();
}

// return octave menu index for named item
function menuIndexForOctave(item) {
  const indexByItem = { "++8ve":0, "+8ve":1, "Concert":2, "-8ve":3, "--8ve":4 };
  var index = indexByItem[item];
  if (index!=null) return index;
  console.log("⚠️ no octave menu item: "+item);
  return 0;
}

// return font size menu index for named item
function menuIndexForFontSize(item) {
  const indexByItem = { "6":0, "8":1, "9":2, "10":3, "11":4, "12":5, "14":6, "16":7, "18":8, "20":9 };
  var index = indexByItem[item];
  if (index!=null) return index;
  console.log("⚠️ no font size menu item: "+item);
  return 0;
}

// return font style menu index for named item
function menuIndexForFontStyle(item) {
  const indexByItem = { "Regular":0, "Bold":1, "Italic":2, "BoldItalic":3 };
  var index = indexByItem[item];
  if (index!=null) return index;
  console.log("⚠️ no font style menu item: "+item);
  return 0;
}

// return alternative menu index for named item
function menuIndexForAltStyle(item) {
  const indexByItem = { "Not Shown":0, "Regular":1, "Italic":2, "Grey":3, "Combined":4 };
  var index = indexByItem[item];
  if (index!=null) return index;
  console.log("⚠️ no alternative style menu item: "+item);
  return 0;
}

// return chord style menu index for named item
function menuIndexForChordStyle(item) {
  const indexByItem = { "Default":0, "Simple":1 };
  var index = indexByItem[item];
  if (index!=null) return index;
  console.log("⚠️ no chord style menu item: "+item);
  return 0;
}

// return instrument menu index for instrument with "id"
function menuIndexForInstrumentID(id) {
  for (var i = 0; i < instrumentTables.length; ++i) {
    if (instrumentTables[i].id == id) return i;
  }
  console.log("⚠️ no instrument for id: "+id);
  return 0;
}

// return variant menu index for variant id "vid" of instrument id "iid"
function menuIndexForVariantID(vid,iid) {
  var inst = instrumentTables[menuIndexForInstrumentID(iid)];
  var keys = inst.keys;
  if (keys==null) {
    console.log("⚠️ no variants for instrument: "+inst.name+" ("+iid+")");
    return 0;
  }
  for (var i = 0; i < keys.length; ++i) {
    var fields = keys[i];
    if (fields.id == vid) return i;
  }
  console.log("⚠️ no variant: "+inst.name+" ("+vid+")");
  return 0;
}

// return name for note
var _noteNames = null;
function nameForNote(n) {
  // memoize the note name translations
  if (_noteNames==null) _noteNames =
    [ xsTr("C"), xsTr("C♯"), xsTr("D"), xsTr("E♭"), xsTr("E"), xsTr("F"),
      xsTr("F♯"), xsTr("G"), xsTr("A♭"), xsTr("A"), xsTr("B♭"), xsTr("B") ];
  return _noteNames[n]
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: chord styles
/////////////////////////////////////////////////////////////////////////

var chordStyles = {

  default: {
    name: xsTr("Default"),
    maj: "△",
    min: "m",
    dim: "∅",
    aug: "+",
    dom7: "7",
    maj7: "△7",
    min7: "m7",
    m7b5: "∅7",
    dim7: "○",
    aug7: "7+",
    slash: true,
    lcbass: false,
    lcchord: false,
  },

  simple: {
    name: xsTr("Simple"),
    maj: "",
    min: "m",
    dim: "dim",
    aug: "aug",
    dom7: "7",
    maj7: "",
    min7: "m",
    m7b5: "dim",
    dim7: "dim",
    aug7: "aug",
    slash: false,
    lcbass: false,
    lcchord: true,
  },
};

// return default chord style
function defaultChordStyle() {
  return chordStyles.default;
}

// return chord style corresponding to given index in dropdown
// return default style if index out of range
function chordStyleForIndex(index) {
  const englishNames = [ "Default", "Simple" ];
  var name = englishNames[index];
  if (name==null) name = englishNames[0];
  var style = chordStyles[name.toLowerCase()];
  if (style==null) style = defaultChordStyle();
  return style;
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: font handling
/////////////////////////////////////////////////////////////////////////

// create a list of fonts for the chooser
// put the default font and the selected font (if any) at the top
// (this avoids a long scroll)
// return the list and the selected index
function makeFontList(selectedFont) {

  // rebuild the (existing) font list
  let theFontList = [];
  let theIndex = 0;
  // add the system fonts
  let systemFonts = Qt.fontFamilies();
  let seenSelectedFont = false;
  for (var i=0; i<systemFonts.length; ++i) {
    let fontName = systemFonts[i];
    // exclude the selected font (if any)
    if (selectedFont != null && fontName == selectedFont) {
      seenSelectedFont = true;

      continue;
    }

    // exclude special font names starting with "."
    if (/^\./.exec(fontName) != null) continue;
    theFontList.push(fontName);
  }
  // if the selected font exists in the list
  // put it at the front of the list
  // and make it the selected item
  if (seenSelectedFont) {
    theFontList.unshift(selectedFont);
    theIndex = 1;

  }
  // put the default & lyric fonts at the top
  // and make it the selected item
  theFontList.unshift(xsTr("Lyric Font"));
  if (!seenSelectedFont) {
    theIndex = 0;

  }
  return [theFontList,theIndex];
}

// return the first font name from the list which exists on the current system
// return the default font name if none found
function bestFont(fontList) {
  for (var i=0; i<fontList.length; ++i) {
    if (hasFont(fontList[i])) return fontList[i];
  }

  return "Arial Narrow";
}

// return true if current system has the specified font
var fontExists = null;
function hasFont(name) {

  // if there is no GUI (testing withut GUI)
  // assume all fonts are available
  return true;

  if (fontExists==null) {
    // memoize font existence table
    fontExists = {};
    let systemFonts = Qt.fontFamilies();
    for (var i=0; i<systemFonts.length; ++i) {
      fontExists[systemFonts[i]] = true;
    }
  }
  if (fontExists[name]) {

    return true;
  }

  return false;
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: key handling
/////////////////////////////////////////////////////////////////////////

// override the keys which we want to handle specially
function overrideKey(event) {
  event.accepted
    =  (event.key === Qt.Key_Return)
    || (event.key === Qt.Key_Up)
    || (event.key === Qt.Key_Tab)
    || (event.key === Qt.Key_Backtab)
    || (event.key === Qt.Key_Down);
}

// insert special symbols when up/down/return/space pressed
// don't change the text when (back)tab pressed
// insert otehr stuff
function mapKey(event,s) {
  let text = s;
  if (event.key === Qt.Key_Return) text = "⏎";
  else if (event.key === Qt.Key_Up) text = "↑";
  else if (event.key === Qt.Key_Down) text = "↓";
  else if (event.key === Qt.Key_Tab) text = text;
  else if (event.key === Qt.Key_Backtab) text = text;
  else if (event.text == " ") text = "␣";
  else if (event.text != "") text = event.text;

  return text;
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: layout support
/////////////////////////////////////////////////////////////////////////

// these buttons control the order in which
// the symbols are added to the notes for display
// if "rowInside" is true, the the row symbols are added first (inside)
// and the push/pull symbols are added afterwards (outside)

// this function updates all three buttons (pre/post/row)
// when any one is changed

function setAnnotationOrder() {

}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: instrument class
/////////////////////////////////////////////////////////////////////////

// when we create a new note from the table, we add an index value
// which is used for sorting the notes in a predictable order
// (eg. for printing or display)
var currentIndex = 0;

class Instrument {

  // create instrument object from instrument index and key offset
  constructor(index,keyOffset,variant) {
    this.table = instrumentTables[index];
    this.name = this.table.name;
    this.keyOffset = keyOffset;
    this.staves = this.table.staves;
    if (variant != "") {
      let vstave = this.table["staves_"+variant];
      if (vstave != null) this.staves = vstave;
    }
    if (this.staves==null) this.staves = {};
    currentIndex = 0;
    for (const [staffType,stave] of Object.entries(this.staves)) {
      if (stave.min==null) stave.min = 0;
      if (stave.max==null) stave.max = stave.min -1;
      if (stave.notes==null) stave.notes = [];
      if (stave.auto==null) stave.auto = false;
      if (stave.max-stave.min != stave.notes.length-1) {
        throw( "invalid table size: "+staffType+" => "+stave.max+"-"
          +stave.min+"!="+(stave.notes.length-1) );
      }
      stave.nodes = [];
      for (var i=0; i<stave.notes.length; ++i) {
        stave.nodes[i] = parseTableElement(stave.notes[i],stave.min+i);
      }
    }
  }

  // return node for given Musescore note value
  nodeForNote(note,staffType) {
    let stave = this.staves[staffType];
    if (stave==null) stave = this.staves["default"];
    if (stave==null) return null;
    var i = note.pitch - this.keyOffset;
    var node = (i>=stave.min && i<=stave.max)
      ? stave.nodes[i-stave.min]
      : stave.auto ? new GenericNote(note.pitch)
      : new Unplayable( xsTr("note out of range") );

    return node;
  }

  // return node for list of Musescore note values
  nodeForNotes(notes,staffType) {
    // if we have multiple notes, try to build a chord from them
    let node = (notes.length>1) ? buildChord(notes,this.table,staffType) : null;
    if (node != null) return node;
    // if we can't, try the individual notes
    for (var i=1; i<notes.length+1; ++i) {
      let n = this.nodeForNote(notes[notes.length-i],staffType);
      if (n==null) return null;
      node = (i==1) ? n : node.add(n);
    }
    return node;
  }

  // string useful for debugging (only?)
  asString() {
    let s = "";
    let names = ["C ","C♯","D ","E♭","E ","F ","F♯","G ","A♭","A ","B♭","B "];
    for (const [staffType,stave] of Object.entries(this.staves)) {
      s = s + this.name+" \["+staffType+"\]:\n";
      for (var i=0; i<stave.nodes.length; ++i) {
        let name = names[(i+stave.min)%12];
        s = s + name + " " + stave.nodes[i].asString()
          + " => " + stave.nodes[i].asDisplayString() + "\n";
      }
    }
    return s;
  }
}

var prevIndex = -1;

// setup key selector according to instrument
function setInstrument() {
  let index = instrumentSelector.currentIndex;
  let keys = instrumentTables[index].keys;
  // reset the octave selector
  if (prevIndex<0) prevIndex = index;
  else if (index!=prevIndex) {
    octaveSelector.currentIndex = menuIndexForOctave("Concert");
    prevIndex = index;
  }
  // enable/disable the info button
  let info = instrumentTables[index].info;
  if (info == null) {
    infoButton.visible = false;
    instrumentSelector.implicitWidth =
      278+15+2+5;
  } else {
    infoButton.visible = true;
    instrumentSelector.implicitWidth = 278;
  }
  // if the instrument has no alternative keys, disable the selector
  if (keys==null) {

    variantSelector.enabled = false;
    variantSelector.model = [ xsTr("N/A") ];
    return;
  }
  // build a new model and compare it with the existing one
  let oldModel = variantSelector.model;
  let newModel = [];
  let changes = 0;
  let defaultIndex = 0;
  for (var i=0; i<keys.length; ++i) {
    // the default is the one with a zero offset
    let offSet = keys[i].transpose;
    if (offSet==null) offSet=0;
    if (offSet==0) defaultIndex = i;
    newModel[i] = keys[i].name;
    if (newModel[i] != oldModel[i]) ++changes;
  }
  // only instantiate the new model if it has changed
  // (to avoid resetting the currentIndex)
  if (newModel.length!=oldModel.length || changes>0) {
    variantSelector.model = newModel;
    variantSelector.currentIndex = defaultIndex;

  }
  variantSelector.enabled = true;
}

// get note offset for current key (in semitones)
function keyOffset() {
  let index = instrumentSelector.currentIndex;
  let keys = instrumentTables[index].keys;
  let offSet = (keys==null) ? 0 : keys[currentStyle.variantIndex].transpose;
  if (offSet==null) offSet = 0;
  offSet -= (currentStyle.octaveIndex-menuIndexForOctave("Concert"))*12;

  return offSet;
}

// get variant of current instrument
// different keys of the same instrument may (or may not) have different tables
function variant() {
  let index = instrumentSelector.currentIndex;
  let keys = instrumentTables[index].keys;
  let v = (keys==null) ? null : keys[currentStyle.variantIndex].staves;
  if (v==null) v = "";

  return v;
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: option class
/////////////////////////////////////////////////////////////////////////

class ABOption extends ABNode {

  isOption() { return true; }

  // create option from list of possibilities
  constructor(ps) {
    super();

    this.ps = ps.sort( (a, b) => a.compareIndex(b) );

  }

  // add a node
  add(node) {

    // adding something unplayable
    // isn't going to make this playable
    if (!node.isPlayable()) return node;
    // if it is a note or chord, add it to every possibility
    if (node.isNote() || node.isChord()) {
      var newps = [];
      for (const p1 of this.ps) {
        let p = p1.add(node);
        if (p.isPlayable()) newps.push(p);
      }
      if (newps.length>0) return new ABOption(newps);
      // 👉 we could gather all of the unplayable options into a list here
      // and attach them to the new unplayable ...
      // but we aren't doing anything with the message anyway (yet)
      return new Unplayable( "no playable options" );
    }
    // if it is an option, create the cross-product
    if (node.isOption()) {
      var ps = [];
      for (const p1 of this.ps) {
        for (const p2 of node.ps) {
          let p = p1.add(p2);
          if (p.isPlayable()) ps.push(p);
        }
      }
      if (ps.length>0) return new ABOption(ps);
      // 👉 we could gather all of the unplayable options into a list here
      // and attach them to the new unplayable ...
      // but we aren't doing anything with the message anyway (yet)
      return new Unplayable( xsTr("no playable options") );
    }
    throw("can't add "+this.constructor.name+" to option");
  }

  // a string (for debugging etc.)
  asString() {
    return this.ps.map( (x) =>
      x.isChord() ? "("+x.asString()+")" : x.asString()
      ) . join("|");
  }

  // this is (probably!) only used for testing because the choices are
  // now displayed on separate lyric lines ...
  asDisplayString() {
    let s = this.asList().map( (x) => x.asDisplayString() ).join("|");
    s = s.replace(/␣/g," ");
    s = s.replace(/⏎/g,"\n");
    return s;
  }

  // a list of alternatives
  asList() {
    return this.ps;
  }

  // used for sorting things into a consistent order
  // (should not be called on an option)
  compareIndex(node) {

    throw("can't compareIndex of "+this.constructor.name);
  }

  pickOne() { return this.ps[0]; }
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: chord class (list of simultaneous notes)
/////////////////////////////////////////////////////////////////////////

class Chord extends ABNode {

  isChord() { return true; }

  // create chord from a list of notes
  constructor(notes) {
    super();

    // keep the notes sorted by pitch
    this.notes = notes.sort( (a,b) => a.compareIndex(b) );

  }

  // add a node to the chord
  add(node) {

    // adding something unplayable
    // isn't going to make this playable
    if (!node.isPlayable()) return node;
    // if it is an option, add the chord to it
    if (node.isOption()) return node.add(this);
    // if it is a note, add it to the chord
    if (node.isNote()) {
      let notes = this.notes.concat([node]);
      return newChordIfPlayable(notes);
    }
    // if it is a chord, union the notes
    if (node.isChord()) {
      let notes = this.notes.concat(node.notes);
      return newChordIfPlayable(notes);
    }
    throw("can't add "+this.constructor.name+" to chord");
  }

  // a string (for debugging etc.)
  asString() {
    return this.notes.map( (x) => x.asString() ) . join(",");
  }

  // a string as displayed on the score
  asDisplayString() {
    let sep = currentStyle.chordSeparator;
    if (this.notes.length == 1) return this.notes[0].asDisplayString();
    let s = this.notes.map( (x) => x.asDisplayString() ) . join(sep);
    s = currentStyle.preChord + s + currentStyle.postChord;
    s = s.replace(/␣/g," ");
    s = s.replace(/⏎/g,"\n");
    return s;
  }

  // used for sorting things into a consistent order
  compareIndex(node) {

    // chords sort after unplayables
    if (!node.isPlayable()) return 1;
    // and before chord symbols
    if (node.isChordSym()) return -1;
    // compare with note
    if (node.isNote()) return -node.compareIndex(this);
    // compare with chord by doing note-wise comparison
    if (node.isChord()) {
      let i = 0;
      while (i < this.notes.length && i < node.notes.length) {
        let c = this.notes[i].compareIndex(node.notes[i]);
        if (c != 0) return c;
        ++i;
      }
      return (i < this.notes.length) ? 1
        : (i < node.notes.length) ? -1 : 0;
    }
    // anything else (option?) is incomparable
    throw("can't compareIndex of "+node.constructor.name);
  }

  // we should have filtered out chords which have both push & pull
  // so we can just look at the first note
  isPush() { return (this.notes.length<1) ? false : this.notes[0].isPush(); }
  isPull() { return (this.notes.length<1) ? false : this.notes[0].isPull(); }
}

// create chord from notes
// return an unplayable if it needs push & pull at the same time!
function newChordIfPlayable(notes) {
  let pullNote;
  let pushNote;
  for (const note of notes) {
    if (note.isPush()) pushNote = note;
    if (note.isPull()) pullNote = note;
    if (pullNote!=null && pushNote!=null) {
      return new Unplayable( xsTr("chord includes both push and pull notes")
        + " ("+pushNote.asString()+" & "+pullNote.asString()+")" );
    }
  }
  return new Chord(notes);
}


/////////////////////////////////////////////////////////////////////////
// AccordionButtons: chord defined by explicit set of notes
/////////////////////////////////////////////////////////////////////////

class ExplicitChord extends ABNode {

  isChordSym() { return true; }

  constructor(s,pitches) {
    super();
    this.pitches = pitches;
    this.push = false;
    this.pull = false;
    this.inversion = 0;
    let match = /^(\+|\-)(.+)$/.exec(s);
    if (match!=null) {
      if (match[1]=='+') this.push = true;
      if (match[1]=='-') this.pull = true;
      this.annotation = match[2];
    } else {
      this.annotation = s;
    }
  }

  // a string (for debugging etc.)
  asString() {
    let s = this.push ? "+" : "";
    s += this.pull ? "-" : "";
    return s+this.annotation;
  }

  // a string as displayed on the score
  asDisplayString() {
    let s = this.annotation;
    if (this.push) s = currentStyle.prePush + s + currentStyle.postPush;
    if (this.pull) s = currentStyle.prePull + s + currentStyle.postPull;
    return s;
  }

  // used for sorting things into a consistent order
  compareIndex(node) {

    // chords sort after everything else
    if (!node.isChordSym()) return 1;
    // prefer lower inversions
    if (this.inversion>node.inversion) return 1;
    if (this.inversion<node.inversion) return -1;
    // otherwise, chord symbols compare as strings
    return this.asString().localeCompare(node.asString());
  }

  isPush() { return this.push; }
  isPull() { return this.pull; }
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: chord defined by symbol
/////////////////////////////////////////////////////////////////////////

class SymbolicChord extends ABNode {

  isChordSym() { return true; }

  constructor(s,root,type,inversion,pitches,suffix) {
    super();
    this.root = root; // name of root - eg "C", "Bb", ...
    this.type = type; // chord type - eg "maj", "min"
    this.inversion = inversion; // inversion = 0,1,2,3,....
    this.pitches = pitches; // list of musescore pitch values
    this.push = false;
    this.pull = false;
    let match = /^(\+|\-)(.*)$/.exec(s);
    if (match!=null) {
      if (match[1]=='+') this.push = true;
      if (match[1]=='-') this.pull = true;
    }
    let info = this.chordInfo(type);
    let rc = info[2] ? root.toLowerCase() : root;
    // chord name
    this.annotation = rc+info[0];
    // inversion
    if (inversion>0 && info[1]) {
      this.annotation = this.annotation+suffix;
    }
  }

  // a string (for debugging/testing etc.)
  asString() {
    let s = this.push ? "+" : "";
    s += this.pull ? "-" : "";
    // return s+this.annotation+" ("+this.root+this.type+")";
    return s+this.root+this.type;
  }

  // a string as displayed on the score
  asDisplayString() {
    let s = this.annotation;
    if (this.push) s = currentStyle.prePush + s + currentStyle.postPush;
    if (this.pull) s = currentStyle.prePull + s + currentStyle.postPull;
    return s;
  }

  // used for sorting things into a consistent order
  compareIndex(node) {

    // chords sort after everything else
    if (!node.isChordSym()) return 1;
    // prefer lower inversions
    if (this.inversion>node.inversion) return 1;
    if (this.inversion<node.inversion) return -1;
    // otherwise, chord symbols compare as strings
    return this.asString().localeCompare(node.asString());
  }

  // get info for chord type (depending on style)
  // eg: "maj" => ["△",true,false] (in default style)
  chordInfo(typename) {
    let chordStyle = chordStyleForIndex(chordStyleSelector.currentIndex);
    // type name
    let type = chordStyle[typename];
    if (type==null) type = defaultChordStyle()[typename];
    if (type==null) type = '?';
    // show slash ?
    let slash = chordStyle.slash;
    if (slash==null) type = defaultChordStyle().slash;
    if (slash==null) slash = false;
    // lower case chords ?
    let lcchord = chordStyle.lcchord;
    if (lcchord==null) lcchord = defaultChordStyle().lcchord;
    if (lcchord==null) lcchord = false;
    return [type,slash,lcchord];
  }

  isPush() { return this.push; }
  isPull() { return this.pull; }
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: build chord from set of notes
/////////////////////////////////////////////////////////////////////////

var chordTypes = {
  "0,4,7": "maj",
  "0,3,7": "min",
  "0,3,6": "dim",
  "0,4,8": "aug",
  "0,4,7,10": "dom7",
  "0,4,7,11": "maj7",
  "0,3,7,10": "min7",
  "0,3,6,10": "m7b5",
  "0,3,6,9": "dim7",
  "0,4,8,10": "aug7",
};

var inversions = null;

// build chord from a list of notes
function buildChord(notes,table,staffType) {
  // create the inversion table if we haven't already
  // this holds all the inversions of all the chords in chordTypes
  if (inversions==null) createInversions();
  // get the table corresponding to this stave
  if (table.staves==null) return null;
  let stave = table.staves[staffType];
  if (stave==null) stave = table.staves["default"];
  if (stave==null) return null;
  if (stave.chords==null) {
      if (stave.auto) stave.chords = {};
      else return null;
  }
  // get the notes and sort them in order
  let pitches = [];
  for (var i=0; i<notes.length; ++i) {
    if (notes[i]!=null) pitches.push(notes[i].pitch);
  }
  // use the sorted list of pitches as the key to the explicit chord tble
  pitches = pitches.sort( (a,b) => (a>b) ? 1 : (a<b) ? -1 : 0 );
  let index = pitches.join(",");
  // the list of playable chords corresponding to these notes
  let playable = [];

  // is there an annotation for the explicit voicing in the stave table?
  // the table key is ascending pitches separated by commas
  // the value is a set of symbols separated by |
  let c = stave.chords[index];
  let ss = (c==null) ? [] : c.split("|");
  for (var i=0; i<ss.length; ++i) {
    let s = ss[i];
    let chord = new ExplicitChord(s,pitches);

    playable.push(chord);
  }
  // if not, see if it is a symbolic chord ...
  if (playable.length==0) {

    let chords = analyseChord(stave,pitches);
    for (var i=0; i<chords.length; ++i) {
      let chord = chords[i];

      playable.push(chord);
    }
  }
  // if we didn't find anything, return null
  // the caller will try to play the individual notes
  if (playable.length==0) {

    return null;
  }
  // return a single chord, or a list of alternatives
  if (playable.length==1) {
    let c = playable[0];

    return c;
  } else {
    let cs = new ABOption(playable);

    return cs;
  }
}

// analyse set of pitches to see if they look like a chord
function analyseChord(stave,pitches) {
  // compute the pitches relative to the lowest note
  let relative = [0];
  let seen = [0];
  let chords = [];
  for (var i=1; i<pitches.length; ++i) {
    let pitch = ( pitches[i] - pitches[0] ) % 12;
    // remove duplicates (including octaves)
    if (seen[pitch]==null) relative.push(pitch);
    seen[pitch] = 1;
  }
  relative = relative.sort( (a,b) => (a>b) ? 1 : (a<b) ? -1 : 0 );
  let size = relative.length;
  // look up the chord type in the inversion table
  let index = relative.join(",");
  let type = inversions[index];

  // not a known chord ....
  if (type==null) return [];
  // handle alternatives
  let ts = type.split('|');
  for (var i=0; i<ts.length; ++i) {
    let t = ts[i];
    // parse out the root and inversion
    let match = /^(.+)\/(\d)$/.exec(t);
    let inversion = 0;
    if (match!=null) {
      t = match[1];
      inversion = (size-match[2])%size;
    }
    // get the root pitch ignoring the octave
    var rootPitch = (pitches[0]+relative[inversion]+120)%12;
    // get the name for the root
    var root = nameForNote(rootPitch);
    let index = root+t;

    // the inversion suffix
    let suffix = "/"+nameForNote(pitches[0]%12);
    // if there is nothing explicit for the chord ...
    // and we are labeling them, use the name as the label
    let c = null;
    if (stave.auto) {
      // add a slash/root unless we are root inversion
      c = (inversion==0) ? index : index+suffix;
    }
    let ss = (c==null) ? [] : c.split("|");
    // look up the annotations for each alternative
    for (var j=0; j<ss.length; ++j) {
      let s = ss[j];

      let chord = new SymbolicChord(s,root,t,inversion,pitches,suffix);

      chords.push(chord);
    }
  }
  return chords;
}

// create inversion table
// map "0,semitones,semitones,..." => "symbol/N"
// where N is the index of the root
function createInversions() {
  inversions = {};
  for (const [string,type] of Object.entries(chordTypes)) {
    // parse the note list
    let notes = string.split(",");
    let size = notes.length;
    for (var i = 0; i < size; ++i) {
      if (i>0) notes = nextInversion(notes);
      let s = notes.join(",");
      let sym = type+"/"+i;
      let root = (size-i)%size;

      inversions[s] = (inversions[s]==null) ? sym : inversions[s]+"|"+sym;
    }
  }
}

// create next inversion of chord
function nextInversion(notes) {
  // copy the notes
  let inversion = notes.slice();
  // take the bottom note and move it to the top
  let note = inversion.shift();
  inversion.push(note+12);
  // make everything relative to the bottom note
  note = inversion[0];
  for (var i = 0; i < inversion.length; ++i) { inversion[i] -= note; }
  return inversion;
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: note class
/////////////////////////////////////////////////////////////////////////

class Note extends ABNode {

  isNote() { return true; }

  // create new note
  constructor(pitch,push,hole,row) {
    super();
    this.pitch = pitch;
    this.push = push;
    this.hole = hole;
    this.row = row;
    this.index = ++currentIndex;

  }

  // add a node to the note
  add(node) {

    // adding something unplayable
    // isn't going to make this playable
    if (!node.isPlayable()) return node;
    // if it is a chord/option, add the note to it
    if (node.isChord()) return node.add(this);
    if (node.isOption()) return node.add(this);
    // two notes make a chord
    if (node.isNote()) return newChordIfPlayable([this,node]);
    throw("can't add "+this.constructor.name+" to note");
  }

  // a string (for debugging etc.)
  asString() {
    return (this.push ? '+' : '-') + this.hole + "^".repeat(this.row);
  }

  // a string as displayed on the score
  asDisplayString() {
    let s = currentStyle.rowInside
      ? currentStyle.preRow.repeat(this.row) + this.hole + currentStyle.postRow.repeat(this.row)
      : this.hole;
    s = this.push
      ? currentStyle.prePush + s + currentStyle.postPush
      : currentStyle.prePull + s + currentStyle.postPull;
    s = currentStyle.rowInside
      ? s
      : currentStyle.preRow.repeat(this.row) + s + currentStyle.postRow.repeat(this.row);
    s = s.replace(/␣/g," ");
    s = s.replace(/⏎/g,"\n");
    return s;
  }

  // used for sorting things into a consistent order
  compareIndex(node) {

    // notes sort after unplayables
    if (!node.isPlayable()) return 1;
    // and before chord symbols
    if (node.isChordSym()) return -1;
    // compare two notes using the index
    if (node.isNote()) {
      let comp = (this.index>node.index) ? 1 :
      (this.index<node.index) ? -1 : 0;
      return comp;
    }
    // compare note with a chord by comparing
    // with the first note
    if (node.isChord()) {
      let c = this.compare(node.notes[0]);
      if (c != 0) return c;
      return (node.notes.length > 1) ? -1 : 0;
    }
    // anything else (option?) is incomparable
    throw("can't compareIndex of "+node.constructor.name);
  }

  isPush() { return this.push; }
  isPull() { return !this.push; }
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: generic note class (no fingering)
/////////////////////////////////////////////////////////////////////////

class GenericNote extends ABNode {

  isNote() { return true; }

  // create new note
  constructor(pitch) {
    super();
    this.pitch = pitch;
    // sort these things in pitch order
    // sort them before and notes which have an explicit (positive) index
    this.index = this.pitch-9999;

  }

  // add a node to the note
  add(node) {

    // adding something unplayable
    // isn't going to make this playable
    if (!node.isPlayable()) return node;
    // if it is a chord/option, add the note to it
    if (node.isChord()) return node.add(this);
    if (node.isOption()) return node.add(this);
    // two notes make a chord
    if (node.isNote()) return newChordIfPlayable([this,node]);
    throw("can't add "+this.constructor.name+" to generic note");
  }

  // a string
  asString() {
    let chordStyle = chordStyleForIndex(chordStyleSelector.currentIndex);
    let lcbass = chordStyle.lcbass;
    if (lcbass==null) lcbass = defaultChordStyle().lcbass;
    if (lcbass==null) lcbass = false;
    let cn = nameForNote(this.pitch%12);
    if (cn.length<2) cn =cn+" ";
    return lcbass ? cn.toLowerCase() : cn;
  }

  // a string as displayed on the score
  asDisplayString() {
     return this.asString();
  }

  // used for sorting things into a consistent order
  compareIndex(node) {

    // notes sort after unplayables
    if (!node.isPlayable()) return 1;
    // and before chord symbols
    if (node.isChordSym()) return -1;
    // compare two notes using the index
    if (node.isNote()) {
      let comp = (this.index>node.index) ? 1 :
      (this.index<node.index) ? -1 : 0;
      return comp;
    }
    // compare note with a chord by comparing
    // with the first note
    if (node.isChord()) {
      let c = this.compare(node.notes[0]);
      if (c != 0) return c;
      return (node.notes.length > 1) ? -1 : 0;
    }
    // anything else (option?) is incomparable
    throw("can't compareIndex of "+node.constructor.name);
  }
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: unplayable class
/////////////////////////////////////////////////////////////////////////

class Unplayable extends ABNode {

  isPlayable() { return false; }

  // create a new unplayable with a reason
  constructor(reason) {
    super();
    this.reason = reason;
    this.index = 0;

  }

  // adding anything to something unplayable
  // isn't going to make it playable!
  add(node) {

    return this;
  }

  // a string (for debugging etc.)
  asString() {
    return "? "+this.reason;
  }

  // a string as displayed on the score
  asDisplayString() {
    return "?";
  }

  // sort unplayable notes before anything else
  compareIndex(node) {

    return (node.isPlayable()) ? -1 : 0;
  }
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: instrument table routines
/////////////////////////////////////////////////////////////////////////

var includesCustom = false;

// return list of instrument names from the table for the dropdown
function instrumentList() {
  // add custom instruments
  if (!includesCustom) {
    for (var i=0; i<CI.instruments.length; ++i) {
      instrumentTables.push(CI.instruments[i]);
    }
    includesCustom = true;
  }
  // build menu
  let names = new Array(0);
  for (var i = 0; i < instrumentTables.length; ++i) {
    names.push(instrumentTables[i].name);
  }
  return names;
}

// each table element is a string representing ..
// either (A) an unplayable note (a "?"")
// or (B) a list of alternative ways of playing the same note, separated by '|'
// each alternative has:
// '+' (push) or '-' (pull)
// a number (the "hole")
// an optional number of '^' characters (the "row")
function parseTableElement(str,pitch) {
  if (str == "?") return new Unplayable(xsTr("instrument can not play this note")+" ("+pitch+")");
  let ss = str.split('|');
  let notes = [];
  for (var i=0; i<ss.length; ++i) {
    let s = ss[i];
    let match = /^(\+|\-)(\d+)(\^*)$/.exec(s);
    if (match==null) {
      throw("syntax error in chord table: "+s+" ("+pitch+")");
    } else {
      let push = (match[1]=='+');
      let hole = match[2];
      let row = match[3].length;
      notes[i] = new Note(pitch,push,hole,row);
    }
  }
  if (notes.length == 0) throw("empty note string in chord table ("+pitch+")");
  if (notes.length == 1) return notes[0];
  return new ABOption(notes);
}

/////////////////////////////////////////////////////////////////////////
// AccordionButtons: musescore interface
/////////////////////////////////////////////////////////////////////////

// build a map of measure (bar) records indexed by time ("tick")
// based on: https://musescore.org/node/345087
function buildMeasureMap(score) {
  let map = {};
  let no = 1;
  let cursor = score.newCursor();
  cursor.rewind(Cursor.SCORE_START);
  while (cursor.measure) {
    let m = cursor.measure;
    let tick = m.firstSegment.tick;
    let tsD = m.timesigActual.denominator;
    let tsN = m.timesigActual.numerator;
    let ticksB = division * 4.0 / tsD; // 🤔 what is "division" ?
    let ticksM = ticksB * tsN;
    no += m.noOffset; // 🤔
    let cur = {
      "tick": tick,                 // tick count at start of bar
      "tsD": tsD,                   // bottom of time signature
      "tsN": tsN,                   // top of time signature
      "ticksB": ticksB,             // 🤔
      "ticksM": ticksM,             // number of ticks in bar
      "past" : (tick + ticksM),     // tick count at start of next bar
      "no": no                      // bar number (for debugging only ?)
    };
    map[cur.tick] = cur;

    if (!m.irregular) ++no; // 🤔
    cursor.nextMeasure();
  }
  return map;
}

// show position of cursor (debugging only?)
// based on: https://musescore.org/node/345087
function showPos(cursor, measureMap) {
  let t = cursor.segment.tick;
  let m = measureMap[cursor.measure.firstSegment.tick];
  let b = "?";
  // if we have a record for this bar (which we should have!)
  // and the cursor position is within the bar (which it should be!)
  if (m && t >= m.tick && t < m.past) {
    // 🤔 compute the beat number (?)
    b = 1 + (t - m.tick) / m.ticksB;
  }
  return "St" + (cursor.staffIdx + 1) +   // staff
      " Vc" + (cursor.voice + 1) +        // voice
      " Ms" + m.no +                      // measure (bar)
      " Bt" + b;                          // beat
}

// apply function to every element of the selection (or score)
// signature: applyToSelectionOrScore(cb, ...args)
// based on: https://musescore.org/node/345087
function applyToSelectionOrScore(cb) {
  let args = Array.prototype.slice.call(arguments, 1);
  let staveBeg;
  let staveEnd;
  let tickEnd;
  let rewindMode;
  let toEOF;
  let cursor = curScore.newCursor();
  cursor.rewind(Cursor.SELECTION_START);
  if (cursor.segment) {
    staveBeg = cursor.staffIdx;
    cursor.rewind(Cursor.SELECTION_END);
    staveEnd = cursor.staffIdx;
    if (!cursor.tick) {
      // This happens when the selection goes to the
      // end of the score — rewind() jumps behind the
      // last segment, setting tick = 0.
      toEOF = true;
    } else {
      toEOF = false;
      tickEnd = cursor.tick;
    }
    rewindMode = Cursor.SELECTION_START;
  } else {
    // no selection
    staveBeg = 0;
    staveEnd = curScore.nstaves - 1;
    toEOF = true;
    rewindMode = Cursor.SCORE_START;
  }
  for (var stave = staveBeg; stave <= staveEnd; ++stave) {
    for (var voice = 0; voice < 4; ++voice) {
      cursor.staffIdx = stave;
      cursor.voice = voice;
      cursor.rewind(rewindMode);
      cursor.staffIdx = stave;
      cursor.voice = voice;
      while (cursor.segment &&
          (toEOF || cursor.tick < tickEnd)) {
        if (cursor.element)
          cb.apply(null,[cursor].concat(args));
        cursor.next();
      }
    }
  }
}

// the maximum line used for "real" lyrics
// (as opposed to annotations that we have generated)
var maxLyricLine = -1;

// delete lyrics at cursor position
// find maximum used lyric line
// based on: https://musescore.org/node/345087
function dropLyrics(cursor, measureMap) {
  let elt = cursor.element;

  // delete any annotations (e.g. system text) that we added previously
  if ((elt.type==Element.CHORD || elt.type==Element.REST)
      && elt.parent!=null && elt.parent.annotations!= null) {
    // ⚠️ iterate in reverse order
    // because we are deleting elements from the end !!!!
    for (var i = elt.parent.annotations.length-1; i >= 0; --i) {
      let e = elt.parent.annotations[i];
      if (e!=null) {
        // ⚠️ hacky!
        // a magic number so that we can identify elements which we added last time
        if (e.z == 9312) {

          removeElement(e);

        }
      }
    }
  }
  // make all of the notes black
  // (we might have made them red last time)
  // do this here because we might not revisit the notes later (when
  // we add the annotations) if we are using an instrument configuration
  // with fewer staves
  let notes = elt.notes;
  if (notes!=null) {
    for (var j = 0; j < notes.length; ++j) {
      notes[j].color = 'black';
    }
  }
  // process lyrics
  if (!elt.lyrics) return;
  // ⚠️ iterate in reverse order
  // because we are deleting elements from the end !!!!
  for (var i = elt.lyrics.length-1; i >= 0; --i) {
    let e = cursor.element.lyrics[i];
    // ⚠️ hacky!
    // a magic number so that we can identify elements which we added last time
    if ( e.z == 9312 ) {
      // remove only elements that we created

      removeElement(e);
    } else {
      // other elements are "real" lyrics
      // keep track of the maximum used lyric line

      if (i>maxLyricLine) maxLyricLine = i;
    }
  }
}

// add note names at cursor position
// based on: https://musescore.org/node/345087
function nameNotes(cursor, measureMap, instrument) {

  // only interested in chords (one or more notes)
  if (cursor.element.type !== Element.CHORD) return;
  let notes = cursor.element.notes;

  // if every note in the chord is a tieback
  // we don't want to take up space by repeating the last annotation
  let tk=0;
  for (var k=0; k<notes.length; ++k) { if (notes[k].tieBack!=null) ++tk; }
  if (tk==notes.length) return;

  // compute the type of staff
  let staffType = (cursor.staffIdx==0) ? "treble"
    : (cursor.staffIdx==1) ? "bass"
    : "other";

  // build string for all alternative chords at this position
  let node = instrument.nodeForNotes(notes,staffType);
  if (node == null) return; // staff not supported, for example

  // the initial offset
  let offsetY = 0.4;

  // pick one alternative unless we are showing all of them
  if (altStyleSelector.currentIndex==menuIndexForAltStyle("Not Shown")) node = node.pickOne();

  // possibly leave some verses clear for lyrics
  let verse = maxLyricLine+1;

  // process each alternative
  let alternatives = node.asList();
  let es = [];
  for (var i=0; i<alternatives.length; ++i) {

    // each alternative has its own text element
    let text = newElement(Element.LYRICS);
    text.verse = verse;

    // get the display string for this alternative
    let a = alternatives[i];
    let t = a.asDisplayString();
    // I'd really like to set some padding on the text
    // I can do that, but Musescore doesn't seem to honor it when laying out the text (?)
    // this hack just adds a small space at each side of the text
    // but unfortunately, padding things like this means that any underscore
    // extends cross the padding (which is horrible)
    if (currentStyle.pad) t = " "+t+" ";
    text.text = t;

    // underscore if required
    // the "4" here is the bit of the MS style flag which represents underscore
    // There should be a symbol for it somewhere, but I can't find it ...
    let underScoreBit = (
      (a.isPull() && pullUnderscore.checked) ||
      (a.isPush() && pushUnderscore.checked)
    ) ? 4 : 0;

    // set note and text colour
    let noteColour = !a.isPlayable() ? "orange" : "black";
    let textColour = !a.isPlayable() ? "orange"
      : (i==0) ? "black"
      : (altStyleSelector.currentIndex==menuIndexForAltStyle("Grey")) ? currentStyle.altColour
      : "black";
    for (var j = 0; j < notes.length; ++j) {
      notes[j].color = noteColour;
    }

    // the "2" here is the bit of the MS style flag which represents italic
    // There should be a symbol for it somewhere, but I can't find it ...
    let italicBit = (i>0 && altStyleSelector.currentIndex==menuIndexForAltStyle("Italic")) ? 2 : 0;



    // set the properties
    let font = fontButton.text;
    if (font != xsTr("Lyric Font")) text.fontFace = font;
    text.fontSize = fontSizeSelector.currentValue;
    // ⚠️ beware - relies on the items in the font style menu being in the right order!
    text.fontStyle = fontStyleSelector.currentIndex | underScoreBit | italicBit;
    text.color = textColour;

    // ⚠️ hacky!
    // a magic number so that we can identify elements which we added last time
    // this allows to remove (only) elements that we created
    text.z = 9312;

    // in CABD style the push and/or may be shifted down
    // (subsequent ones are fixed relative to the first)
    let height = text.bbox.bottom - text.bbox.top;
    if (i==0) {
      let nudge = height * 0.4;
      if (pullShift.checked || pushShift.checked) {
        if (a.isPull()) offsetY += pullShift.checked ? 2*nudge : 0;
        else if (a.isPush()) offsetY += pushShift.checked ? 2*nudge : 0;
        else offsetY += nudge;
      }
    }
    if (offsetY!=0) text.offsetY += offsetY;

    // add the text element to the list
    es.push(text);



    // next alternative
    offsetY += height * (1+0.4);
  }

  // attempt to combine elements if required
  if (altStyleSelector.currentIndex==menuIndexForAltStyle("Combined")) es = combineElements(es);

  // add elements
  for (var i=0; i<es.length; ++i) {
    cursor.element.add(es[i]);
  }
}

// attempt to combine alternatives into a single element
function combineElements(es) {
  var ce = es[0];
  var ct = ce.text;
  // we can only combine elements if all the attributes are the same
  for (var i=1; i<es.length; ++i) {
    var e = es[i];
    if (e.fontSize != ce.fontSize) return es;
    if (e.fontStyle != ce.fontStyle) return es;
    if (e.color != ce.color) return es;
    ct = ct + "\n" + e.text;
  }
  // all atributes are the same
  // return a single element with the combined text
  ce.text = ct;
  return [ce];
}

// add annotations
function annotate() {
  // get the current style settings from the dialog
  getStyleFromDialog();

  // build instrument table for selected instrument and key
  let instrument = new Instrument(
    instrumentSelector.currentIndex, keyOffset(), variant() );
  // build map of info for each measure
  let measureMap = buildMeasureMap(curScore);
  // remove any previously added annotations (lyrics)
  // and keep track of the highest line used for (real) lyrics
  maxLyricLine = -1;
  applyToSelectionOrScore(dropLyrics, measureMap);

  // add note names
  applyToSelectionOrScore(nameNotes, measureMap, instrument);
  // add instrument name
  if (labelBox.checked) {
    let name = instrument.name;
    let i = instrumentSelector.currentIndex;
    let keys = instrumentTables[i].keys;
    if (keys!=null) {
      name = keys[currentStyle.variantIndex].name+" "+name;
    }
    let cursor = curScore.newCursor();
    cursor.rewind(Cursor.SCORE_START);
    cursor.voice    = 0;
    cursor.staffIdx = 0;
    let text = newElement(Element.SYSTEM_TEXT);
    text.text = name;
    text.color = "black";
    text.z = 9312;
    cursor.add(text);
  }
}

// remove all annotations
function resetAnnotations() {
  let measureMap = buildMeasureMap(curScore);
  maxLyricLine = -1;
  applyToSelectionOrScore(dropLyrics, measureMap);
}





// create a test instrument
let index = null;
for (var i=0; i<instrumentTables.length; ++i) {
    if (instrumentTables[i].name == "Test Instrument") index = i;
}
if (index==null) {
    console.log("❰ITABLE❱\nFAILED: no Test Instrument table\n❰END❱");
    return;
}
// default variant
let inst = new Instrument(index,0,"");
console.log("❰ITABLE❱\nOK\n❰END❱");

// print node as string
// enclose in delimiters for parsing by test script
function test(tag,node) {
    console.log("❰"+tag+"❱\n"+node.asString()+"\n❰END❱");
}

// print node as display string
// enclose in delimiters for parsing by test script
function dtest(tag,node) {
    console.log("❰"+tag+"❱\n"+node.asDisplayString()+"\n❰END❱");
}

// parse table element and return node
function P(s,n) {
    try {
        return parseTableElement(s,n);
    }
    catch(err) {
        return new Unplayable(err);
    }
}

setCurrentStyleByKey('test');

// node parse/stringify tests
test("PARSE_PUSH",P("+1",1));
test("PARSE_PULL",P("-2",1));
test("PARSE_ROW",P("+3^^",1));
test("PARSE_ERROR",P("4",1));
test("PUSH_CHORD",P("+5",1).add(P("+6",1)));
test("PULL_CHORD",P("-7",1).add(P("-8",2)));
test("UNPLAYABLE_CHORD",P("+9",1).add(P("-10",2)));
test("PARSE_OPTION",P("+11|-12|+13",1));
test("PRODUCT",P("-7^^|+6",2).add(P("+4|-5",1)));
test("UNPLAYABLE_PRODUCT",P("-7^^|-6",2).add(P("+4|+5",1)));
test("BIG_PRODUCT",P("-1|+2|-3|+4",1).add(P("-5|+6",2)));
test("BIGGER_PRODUCT",P("-7|+8",3).add(P("-1|+2|-3|+4",1)).add(P("-5|+6",2)));
dtest("DISPLAY_PUSH",P("+1",1));
dtest("DISPLAY_PULL",P("-2",1));
dtest("DISPLAY_ROW",P("+3^^",1));
dtest("DISPLAY_CHORD",P("+5",1).add(P("+6",1)));
dtest("DISPLAY_CHOICE",P("-7^^|+6",2).add(P("+4|-5",1)));

// test chord builder
function ctest(tag,pitches,display) {
    let notes = [];
    for (var i=0; i<pitches.length; ++i) { notes.push({pitch:pitches[i]}); }
    let node = buildChord(notes,inst.table,'default');
    if (node==null) node = new Unplayable("not a chord");
    if (display) console.log("❰"+tag+"❱\n"+node.asDisplayString()+"\n❰END❱");
    else console.log("❰"+tag+"❱\n"+node.asString()+"\n❰END❱");
}

// mock chord style selector
let chordStyleSelector = { currentValue: 0 }

// test chords
ctest("NO_CHORD",[62,63,64],0);
ctest("EXPLICIT_CHORD",[72,76,79],0);
ctest("MAJ_CHORD",[57,61,64],0);
ctest("MIN_CHORD",[59,62,66],0);
ctest("DIM_CHORD",[60,63,66],0);
ctest("AUG_CHORD",[62,66,70],0);
ctest("DOM7_CHORD",[63,67,70,73],0);
ctest("MAJ7_CHORD",[64,68,71,75],0);
ctest("MIN7_CHORD",[65,68,72,75],0);
ctest("M7b5_CHORD",[66,69,72,76],0);
ctest("DIM7_CHORD",[67,70,73,76],0);
ctest("AUG7_CHORD",[68,72,76,78],0);
ctest("NO_CHORD_DISP",[62,63,64],1);
ctest("EXPLICIT_CHORD_DISP",[72,76,79],1);
ctest("MAJ_CHORD_DISP",[57,61,64],1);
ctest("MIN_CHORD_DISP",[59,62,66],1);
ctest("DIM_CHORD_DISP",[60,63,66],1);
ctest("AUG_CHORD_DISP",[62,66,70],1);
ctest("DOM7_CHORD_DISP",[63,67,70,73],1);
ctest("MAJ7_CHORD_DISP",[64,68,71,75],1);
ctest("MIN7_CHORD_DISP",[65,68,72,75],1);
ctest("M7b5_CHORD_DISP",[66,69,72,76],1);
ctest("DIM7_CHORD_DISP",[67,70,73,76],1);
ctest("AUG7_CHORD_DISP",[68,72,76,78],1);




