MuseScore/libmscore/harmony.cpp
MarcSabatella b50a5543c1 fix #302690: crash after copying chord symbol
Resolves: https://musescore.org/en/node/302690

Currently, a crash can occur after copying a chord symbol.
The Harmony object contains a RealizedHarmony object,
which contains a pointer back to the Harmony object,
but the copy constructor for Harmony does not set the pointer.
This ends up yielding an invalid pointer by the time the copy completes.
This fix makes sure to set the pointer correctly.
It also copies the "_play" property,
otherwise copied chord symbols would not play by default.
2020-03-22 11:07:51 -06:00

2150 lines
74 KiB
C++

//=============================================================================
// MuseScore
// Music Composition & Notation
//
// Copyright (C) 2008-2011 Werner Schweer
//
// 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.GPL
//=============================================================================
#include "harmony.h"
#include "pitchspelling.h"
#include "score.h"
#include "system.h"
#include "measure.h"
#include "segment.h"
#include "chordlist.h"
#include "mscore.h"
#include "fret.h"
#include "staff.h"
#include "part.h"
#include "utils.h"
#include "sym.h"
#include "xml.h"
namespace Ms {
//---------------------------------------------------------
// harmonyName
//---------------------------------------------------------
QString Harmony::harmonyName() const
{
// Hack:
const_cast<Harmony*>(this)->determineRootBaseSpelling();
HChord hc = descr() ? descr()->chord : HChord();
QString s, r, e, b;
if (_leftParen)
s = "(";
if (_rootTpc != Tpc::TPC_INVALID)
r = tpc2name(_rootTpc, _rootSpelling, _rootCase);
else if (_harmonyType != HarmonyType::STANDARD)
r = _function;
if (_textName != "") {
e = _textName;
if (_harmonyType != HarmonyType::ROMAN)
e.remove('=');
}
else if (!_degreeList.empty()) {
hc.add(_degreeList);
// try to find the chord in chordList
const ChordDescription* newExtension = 0;
const ChordList* cl = score()->style().chordList();
for (const ChordDescription& cd : *cl) {
if (cd.chord == hc && !cd.names.empty()) {
newExtension = &cd;
break;
}
}
// now determine the chord name
if (newExtension)
e = newExtension->names.front();
else {
// not in table, fallback to using HChord.name()
r = hc.name(_rootTpc);
e = "";
}
}
if (_baseTpc != Tpc::TPC_INVALID)
b = "/" + tpc2name(_baseTpc, _baseSpelling, _baseCase);
s += r + e + b;
if (_rightParen)
s += ")";
return s;
}
//---------------------------------------------------------
// rootName
//---------------------------------------------------------
QString Harmony::rootName()
{
determineRootBaseSpelling();
return tpc2name(_rootTpc, _rootSpelling, _rootCase);
}
//---------------------------------------------------------
// baseName
//---------------------------------------------------------
QString Harmony::baseName()
{
determineRootBaseSpelling();
if (_baseTpc == Tpc::TPC_INVALID)
return rootName();
return tpc2name(_baseTpc, _baseSpelling, _baseCase);
}
bool Harmony::isRealizable() const
{
return (_rootTpc != Tpc::TPC_INVALID)
|| (_harmonyType == HarmonyType::NASHVILLE); // unable to fully check at for nashville at the moment
}
//---------------------------------------------------------
// resolveDegreeList
// try to detect chord number and to eliminate degree
// list
//---------------------------------------------------------
void Harmony::resolveDegreeList()
{
if (_degreeList.empty())
return;
HChord hc = descr() ? descr()->chord : HChord();
hc.add(_degreeList);
// qDebug("resolveDegreeList: <%s> <%s-%s>: ", _descr->name, _descr->xmlKind, _descr->xmlDegrees);
// hc.print();
// _descr->chord.print();
// try to find the chord in chordList
const ChordList* cl = score()->style().chordList();
for (const ChordDescription& cd : *cl) {
if ((cd.chord == hc) && !cd.names.empty()) {
qDebug("ResolveDegreeList: found in table as %s", qPrintable(cd.names.front()));
_id = cd.id;
_degreeList.clear();
return;
}
}
qDebug("ResolveDegreeList: not found in table");
}
//---------------------------------------------------------
// chordSymbolStyle
//---------------------------------------------------------
const ElementStyle chordSymbolStyle {
{ Sid::harmonyPlacement, Pid::PLACEMENT },
{ Sid::minHarmonyDistance, Pid::MIN_DISTANCE },
{ Sid::harmonyPlay, Pid::PLAY },
{ Sid::harmonyVoiceLiteral, Pid::HARMONY_VOICE_LITERAL },
{ Sid::harmonyVoicing, Pid::HARMONY_VOICING },
{ Sid::harmonyDuration, Pid::HARMONY_DURATION }
};
//---------------------------------------------------------
// Harmony
//---------------------------------------------------------
Harmony::Harmony(Score* s)
: TextBase(s, Tid::HARMONY_A, ElementFlag::MOVABLE | ElementFlag::ON_STAFF)
{
_rootTpc = Tpc::TPC_INVALID;
_baseTpc = Tpc::TPC_INVALID;
_rootSpelling = NoteSpellingType::STANDARD;
_baseSpelling = NoteSpellingType::STANDARD;
_rootCase = NoteCaseType::CAPITAL;
_baseCase = NoteCaseType::CAPITAL;
_rootRenderCase = NoteCaseType::CAPITAL;
_baseRenderCase = NoteCaseType::CAPITAL;
_id = -1;
_parsedForm = 0;
_harmonyType = HarmonyType::STANDARD;
_leftParen = false;
_rightParen = false;
_realizedHarmony = RealizedHarmony(this);
initElementStyle(&chordSymbolStyle);
}
Harmony::Harmony(const Harmony& h)
: TextBase(h)
{
_rootTpc = h._rootTpc;
_baseTpc = h._baseTpc;
_rootSpelling = h._rootSpelling;
_baseSpelling = h._baseSpelling;
_rootCase = h._rootCase;
_baseCase = h._baseCase;
_rootRenderCase = h._rootRenderCase;
_baseRenderCase = h._baseRenderCase;
_id = h._id;
_leftParen = h._leftParen;
_rightParen = h._rightParen;
_degreeList = h._degreeList;
_parsedForm = h._parsedForm ? new ParsedChord(*h._parsedForm) : 0;
_harmonyType = h._harmonyType;
_textName = h._textName;
_userName = h._userName;
_function = h._function;
_play = h._play;
_realizedHarmony = h._realizedHarmony;
_realizedHarmony.setHarmony(this);
for (const TextSegment* s : h.textList) {
TextSegment* ns = new TextSegment();
ns->set(s->text, s->font, s->x, s->y);
textList.append(ns);
}
}
//---------------------------------------------------------
// ~Harmony
//---------------------------------------------------------
Harmony::~Harmony()
{
for (const TextSegment* ts : textList)
delete ts;
if (_parsedForm)
delete _parsedForm;
}
//---------------------------------------------------------
// write
//---------------------------------------------------------
void Harmony::write(XmlWriter& xml) const
{
if (!xml.canWrite(this))
return;
xml.stag(this);
writeProperty(xml, Pid::HARMONY_TYPE);
if (_leftParen)
xml.tagE("leftParen");
if (_rootTpc != Tpc::TPC_INVALID || _baseTpc != Tpc::TPC_INVALID) {
int rRootTpc = _rootTpc;
int rBaseTpc = _baseTpc;
if (staff()) {
// parent can be a fret diagram
Segment* segment = parent()->isSegment() ? toSegment(parent()) : toSegment(parent()->parent());
Fraction tick = segment ? segment->tick() : Fraction(-1,1);
const Interval& interval = part()->instrument(tick)->transpose();
if (xml.clipboardmode() && !score()->styleB(Sid::concertPitch) && interval.chromatic) {
rRootTpc = transposeTpc(_rootTpc, interval, true);
rBaseTpc = transposeTpc(_baseTpc, interval, true);
}
}
if (rRootTpc != Tpc::TPC_INVALID) {
xml.tag("root", rRootTpc);
if (_rootCase != NoteCaseType::CAPITAL)
xml.tag("rootCase", static_cast<int>(_rootCase));
}
if (_id > 0)
xml.tag("extension", _id);
// parser uses leading "=" as a hidden specifier for minor
// this may or may not currently be incorporated into _textName
QString writeName = _textName;
if (_parsedForm && _parsedForm->name().startsWith("=") && !writeName.startsWith("="))
writeName = "=" + writeName;
if (!writeName.isEmpty())
xml.tag("name", writeName);
if (rBaseTpc != Tpc::TPC_INVALID) {
xml.tag("base", rBaseTpc);
if (_baseCase != NoteCaseType::CAPITAL)
xml.tag("baseCase", static_cast<int>(_baseCase));
}
for (const HDegree& hd : _degreeList) {
HDegreeType tp = hd.type();
if (tp == HDegreeType::ADD || tp == HDegreeType::ALTER || tp == HDegreeType::SUBTRACT) {
xml.stag("degree");
xml.tag("degree-value", hd.value());
xml.tag("degree-alter", hd.alter());
switch (tp) {
case HDegreeType::ADD:
xml.tag("degree-type", "add");
break;
case HDegreeType::ALTER:
xml.tag("degree-type", "alter");
break;
case HDegreeType::SUBTRACT:
xml.tag("degree-type", "subtract");
break;
default:
break;
}
xml.etag();
}
}
}
else
xml.tag("name", _textName);
if (!_function.isEmpty())
xml.tag("function", _function);
TextBase::writeProperties(xml, false, true);
//Pid::PLAY, Pid::HARMONY_VOICE_LITERAL, Pid::HARMONY_VOICING, Pid::HARMONY_DURATION
//written by the above function call because they are part of element style
if (_rightParen)
xml.tagE("rightParen");
xml.etag();
}
//---------------------------------------------------------
// read
//---------------------------------------------------------
void Harmony::read(XmlReader& e)
{
while (e.readNextStartElement()) {
const QStringRef& tag(e.name());
if (tag == "base")
setBaseTpc(e.readInt());
else if (tag == "baseCase")
_baseCase = static_cast<NoteCaseType>(e.readInt());
else if (tag == "extension")
setId(e.readInt());
else if (tag == "name")
_textName = e.readElementText();
else if (tag == "root")
setRootTpc(e.readInt());
else if (tag == "rootCase")
_rootCase = static_cast<NoteCaseType>(e.readInt());
else if (tag == "function")
_function = e.readElementText();
else if (tag == "degree") {
int degreeValue = 0;
int degreeAlter = 0;
QString degreeType = "";
while (e.readNextStartElement()) {
const QStringRef& t(e.name());
if (t == "degree-value")
degreeValue = e.readInt();
else if (t == "degree-alter")
degreeAlter = e.readInt();
else if (t == "degree-type")
degreeType = e.readElementText();
else
e.unknown();
}
if (degreeValue <= 0 || degreeValue > 13
|| degreeAlter < -2 || degreeAlter > 2
|| (degreeType != "add" && degreeType != "alter" && degreeType != "subtract")) {
qDebug("incorrect degree: degreeValue=%d degreeAlter=%d degreeType=%s",
degreeValue, degreeAlter, qPrintable(degreeType));
}
else {
if (degreeType == "add")
addDegree(HDegree(degreeValue, degreeAlter, HDegreeType::ADD));
else if (degreeType == "alter")
addDegree(HDegree(degreeValue, degreeAlter, HDegreeType::ALTER));
else if (degreeType == "subtract")
addDegree(HDegree(degreeValue, degreeAlter, HDegreeType::SUBTRACT));
}
}
else if (tag == "leftParen") {
_leftParen = true;
e.readNext();
}
else if (tag == "rightParen") {
_rightParen = true;
e.readNext();
}
else if (readProperty(tag, e, Pid::POS_ABOVE))
;
else if (readProperty(tag, e, Pid::HARMONY_TYPE))
;
else if (readProperty(tag, e, Pid::PLAY))
;
else if (readProperty(tag, e, Pid::HARMONY_VOICE_LITERAL))
;
else if (readProperty(tag, e, Pid::HARMONY_VOICING))
;
else if (readProperty(tag, e, Pid::HARMONY_DURATION))
;
else if (!TextBase::readProperties(e))
e.unknown();
}
// TODO: now that we can render arbitrary chords,
// we could try to construct a full representation from a degree list.
// These will typically only exist for chords imported from MusicXML prior to MuseScore 2.0
// or constructed in the Chord Symbol Properties dialog.
if (_rootTpc != Tpc::TPC_INVALID) {
if (_id > 0) {
// positive id will happen only for scores that were created with explicit chord lists
// lookup id in chord list and generate new description if necessary
getDescription();
}
else
{
// default case: look up by name
// description will be found for any chord already read in this score
// and we will generate a new one if necessary
getDescription(_textName);
}
}
else if (_textName == "") {
// unrecognized chords prior to 2.0 were stored as text with markup
// we need to strip away the markup
// this removes any user-applied formatting,
// but we no longer support user-applied formatting for chord symbols anyhow
// with any luck, the resulting text will be parseable now, so give it a shot
createLayout();
QString s = plainText();
if (!s.isEmpty()) {
setHarmony(s);
return;
}
// empty text could also indicate a root-less slash chord ("/E")
// we'll fall through and render it normally
}
// render chord from description (or _textName)
render();
setPlainText(harmonyName());
}
//---------------------------------------------------------
// determineRootBaseSpelling
//---------------------------------------------------------
void Harmony::determineRootBaseSpelling(NoteSpellingType& rootSpelling, NoteCaseType& rootCase,
NoteSpellingType& baseSpelling, NoteCaseType& baseCase)
{
// spelling
if (score()->styleB(Sid::useStandardNoteNames))
rootSpelling = NoteSpellingType::STANDARD;
else if (score()->styleB(Sid::useGermanNoteNames))
rootSpelling = NoteSpellingType::GERMAN;
else if (score()->styleB(Sid::useFullGermanNoteNames))
rootSpelling = NoteSpellingType::GERMAN_PURE;
else if (score()->styleB(Sid::useSolfeggioNoteNames))
rootSpelling = NoteSpellingType::SOLFEGGIO;
else if (score()->styleB(Sid::useFrenchNoteNames))
rootSpelling = NoteSpellingType::FRENCH;
baseSpelling = rootSpelling;
// case
// always use case as typed if automatic capitalization is off
if (!score()->styleB(Sid::automaticCapitalization)) {
rootCase = _rootCase;
baseCase = _baseCase;
return;
}
// set default
if (score()->styleB(Sid::allCapsNoteNames)) {
rootCase = NoteCaseType::UPPER;
baseCase = NoteCaseType::UPPER;
}
else {
rootCase = NoteCaseType::CAPITAL;
baseCase = NoteCaseType::CAPITAL;
}
// override for bass note
if (score()->styleB(Sid::lowerCaseBassNotes))
baseCase = NoteCaseType::LOWER;
// override for minor chords
if (score()->styleB(Sid::lowerCaseMinorChords)) {
const ChordDescription* cd = descr();
QString quality;
if (cd) {
// use chord description if possible
// this is the usual case
quality = cd->quality();
}
else if (_parsedForm) {
// this happens on load of new chord list
// for chord symbols that were added/edited since the score was loaded
// or read aloud with screenreader
// parsed form is usable even if out of date with respect to chord list
quality = _parsedForm->quality();
}
else {
// this happens on load of new chord list
// for chord symbols that have not been edited since the score was loaded
// we need to parse this chord for now to determine quality
// but don't keep the parsed form around as we're not ready for it yet
quality = parsedForm()->quality();
delete _parsedForm;
_parsedForm = 0;
}
if (quality == "minor" || quality == "diminished" || quality == "half-diminished")
rootCase = NoteCaseType::LOWER;
}
}
//---------------------------------------------------------
// determineRootBaseSpelling
//---------------------------------------------------------
void Harmony::determineRootBaseSpelling()
{
determineRootBaseSpelling(_rootSpelling, _rootRenderCase,
_baseSpelling, _baseRenderCase);
}
//---------------------------------------------------------
// convertNote
// convert something like "C#" into tpc 21
//---------------------------------------------------------
static int convertNote(const QString& s, NoteSpellingType noteSpelling, NoteCaseType& noteCase, int& idx)
{
bool useGerman = false;
bool useSolfeggio = false;
static const int spellings[] = {
// bb b - # ##
0, 7, 14, 21, 28, // C
2, 9, 16, 23, 30, // D
4, 11, 18, 25, 32, // E
-1, 6, 13, 20, 27, // F
1, 8, 15, 22, 29, // G
3, 10, 17, 24, 31, // A
5, 12, 19, 26, 33, // B
};
if (s == "")
return Tpc::TPC_INVALID;
noteCase = s[0].isLower() ? NoteCaseType::LOWER : NoteCaseType::CAPITAL;
int acci;
switch (noteSpelling) {
case NoteSpellingType::SOLFEGGIO:
case NoteSpellingType::FRENCH:
useSolfeggio = true;
if (s.toLower().startsWith("sol"))
acci = 3;
else
acci = 2;
break;
case NoteSpellingType::GERMAN:
case NoteSpellingType::GERMAN_PURE:
useGerman = true;
// fall through
default:
acci = 1;
}
idx = acci;
int alter = 0;
int n = s.size();
QString acc = s.right(n-acci);
if (acc != "") {
if (acc.startsWith("bb")) {
alter = -2;
idx += 2;
}
else if (acc.startsWith("b")) {
alter = -1;
idx += 1;
}
else if (useGerman && acc.startsWith("eses")) {
alter = -2;
idx += 4;
}
else if (useGerman && (acc.startsWith("ses") || acc.startsWith("sas"))) {
alter = -2;
idx += 3;
}
else if (useGerman && acc.startsWith("es")) {
alter = -1;
idx += 2;
}
else if (useGerman && acc.startsWith("s") && !acc.startsWith("su")) {
alter = -1;
idx += 1;
}
else if (acc.startsWith("##")) {
alter = 2;
idx += 2;
}
else if (acc.startsWith("x")) {
alter = 2;
idx += 1;
}
else if (acc.startsWith("#")) {
alter = 1;
idx += 1;
}
else if (useGerman && acc.startsWith("isis")) {
alter = 2;
idx += 4;
}
else if (useGerman && acc.startsWith("is")) {
alter = 1;
idx += 2;
}
}
int r;
if (useGerman) {
switch(s[0].toLower().toLatin1()) {
case 'c': r = 0; break;
case 'd': r = 1; break;
case 'e': r = 2; break;
case 'f': r = 3; break;
case 'g': r = 4; break;
case 'a': r = 5; break;
case 'h': r = 6; break;
case 'b':
if (alter && alter != -1)
return Tpc::TPC_INVALID;
r = 6;
alter = -1;
break;
default:
return Tpc::TPC_INVALID;
}
}
else if (useSolfeggio) {
if (s.length() < 2)
return Tpc::TPC_INVALID;
if (s[1].isUpper())
noteCase = NoteCaseType::UPPER;
QString ss = s.toLower().left(2);
if (ss == "do")
r = 0;
else if (ss == "re" || ss == "")
r = 1;
else if (ss == "mi")
r = 2;
else if (ss == "fa")
r = 3;
else if (ss == "so") // sol, but only check first 2 characters
r = 4;
else if (ss == "la")
r = 5;
else if (ss == "si")
r = 6;
else
return Tpc::TPC_INVALID;
}
else {
switch(s[0].toLower().toLatin1()) {
case 'c': r = 0; break;
case 'd': r = 1; break;
case 'e': r = 2; break;
case 'f': r = 3; break;
case 'g': r = 4; break;
case 'a': r = 5; break;
case 'b': r = 6; break;
default: return Tpc::TPC_INVALID;
}
}
r = spellings[r * 5 + alter + 2];
return r;
}
//---------------------------------------------------------
// parseHarmony
// determine root and bass tpc & case
// compare body of chordname against chord list
// return true if chord is recognized
//---------------------------------------------------------
const ChordDescription* Harmony::parseHarmony(const QString& ss, int* root, int* base, bool syntaxOnly)
{
_id = -1;
if (_parsedForm) {
delete _parsedForm;
_parsedForm = 0;
}
_textName.clear();
bool useLiteral = false;
if (ss.endsWith(' '))
useLiteral = true;
if (_harmonyType == HarmonyType::ROMAN) {
_userName = ss;
_textName = ss;
*root = Tpc::TPC_INVALID;
*base = Tpc::TPC_INVALID;
return 0;
}
// pre-process for parentheses
QString s = ss.simplified();
if ((_leftParen = s.startsWith('(')))
s.remove(0,1);
if ((_rightParen = (s.endsWith(')') && s.count('(') < s.count(')'))))
s.remove(s.size()-1,1);
if (_leftParen || _rightParen)
s = s.simplified(); // in case of spaces inside parentheses
if (s.isEmpty())
return 0;
// pre-process for lower case minor chords
bool preferMinor;
if (score()->styleB(Sid::lowerCaseMinorChords) && s[0].isLower())
preferMinor = true;
else
preferMinor = false;
if (_harmonyType == HarmonyType::NASHVILLE) {
int n = 0;
if (s[0].isDigit())
n = 1;
else if (s[1].isDigit())
n = 2;
_function = s.mid(0, n);
s = s.mid(n);
*root = Tpc::TPC_INVALID;
*base = Tpc::TPC_INVALID;
}
else {
determineRootBaseSpelling();
int idx;
int r = convertNote(s, _rootSpelling, _rootCase, idx);
if (r == Tpc::TPC_INVALID) {
if (s[0] == '/')
idx = 0;
else {
qDebug("failed <%s>", qPrintable(ss));
_userName = s;
_textName = s;
return 0;
}
}
*root = r;
*base = Tpc::TPC_INVALID;
int slash = s.lastIndexOf('/');
if (slash != -1) {
QString bs = s.mid(slash + 1).simplified();
s = s.mid(idx, slash - idx).simplified();
int idx2;
*base = convertNote(bs, _baseSpelling, _baseCase, idx2);
if (idx2 != bs.size())
*base = Tpc::TPC_INVALID;
if (*base == Tpc::TPC_INVALID) {
// if what follows after slash is not (just) a TPC
// then reassemble chord and try to parse with the slash
s = s + "/" + bs;
}
}
else
s = s.mid(idx); // don't simplify; keep leading space before extension if present
}
_userName = s;
const ChordList* cl = score()->style().chordList();
const ChordDescription* cd = 0;
if (useLiteral)
cd = descr(s);
else {
_parsedForm = new ParsedChord();
_parsedForm->parse(s, cl, syntaxOnly, preferMinor);
// parser prepends "=" to name of implied minor chords
// use this here as well
if (preferMinor)
s = _parsedForm->name();
// look up to see if we already have a descriptor (chord has been used before)
cd = descr(s, _parsedForm);
}
if (cd) {
// descriptor found; use its information
_id = cd->id;
if (!cd->names.empty())
_textName = cd->names.front();
}
else {
// no descriptor yet; just set textname
// we will generate descriptor later if necessary (when we are done editing this chord)
_textName = s;
}
return cd;
}
//---------------------------------------------------------
// startEdit
//---------------------------------------------------------
void Harmony::startEdit(EditData& ed)
{
if (!textList.empty()) {
// convert chord symbol to plain text
setPlainText(harmonyName());
// clear rendering
for (const TextSegment* t : textList)
delete t;
textList.clear();
}
// layout as text, without position reset
TextBase::layout1();
triggerLayout();
TextBase::startEdit(ed);
}
//---------------------------------------------------------
// edit
//---------------------------------------------------------
bool Harmony::edit(EditData& ed)
{
if (ed.key == Qt::Key_Return)
return true; // Harmony only single line
bool rv = TextBase::edit(ed);
// layout as text, without position reset
TextBase::layout1();
triggerLayout();
// check spelling
int root = TPC_INVALID;
int base = TPC_INVALID;
QString str = xmlText();
showSpell = !str.isEmpty() && !parseHarmony(str, &root, &base, true) && root == TPC_INVALID && _harmonyType == HarmonyType::STANDARD;
if (showSpell)
qDebug("bad spell");
return rv;
}
//---------------------------------------------------------
// endEdit
//---------------------------------------------------------
void Harmony::endEdit(EditData& ed)
{
// complete editing: generate xml text, set Pid::TEXT, perform initial layout
// if text has changed, this also triggers setHarmony() which renders chord symbol
// but any rendering or layout performed here is tentative,
// we may still need to substitute special characters,
// and that cannot be until after editing is completed
TextBase::endEdit(ed);
// get plain text
QString s = plainText();
// if user explicitly added symbols to the text,
// convert them back to their respective replacement texts
if (harmonyType() != HarmonyType::ROMAN) {
s.replace("\u1d12b", "bb"); // double-flat
s.replace("\u266d", "b"); // flat
s.replace("\ue260", "b"); // flat
// do not replace natural sign
// (right now adding the symbol explicitly is the only way to force a natural sign to appear at all)
//s.replace("\u266e", "n"); // natural, if one day we support that too
//s.replace("\ue261", "n"); // natural, if one day we support that too
s.replace("\u266f", "#"); // sharp
s.replace("\ue262", "#"); // sharp
s.replace("\u1d12a", "x"); // double-sharp
s.replace("\u0394", "^"); // &Delta;
s.replace("\u00d0", "o"); // &deg;
s.replace("\u00f8", "0"); // &oslash;
s.replace("\u00d8", "0"); // &Oslash;
}
else {
s.replace("\ue260", "\u266d"); // flat
s.replace("\ue261", "\u266e"); // natural
s.replace("\ue262", "\u266f"); // sharp
}
//play chord on edit and set dirty
score()->setPlayChord(true);
_realizedHarmony.setDirty(true);
// render and layout chord symbol
// (needs to be done here if text hasn't changed, or redone if replacemens were performed above)
score()->startCmd();
setHarmony(s);
layout1();
triggerLayout();
score()->endCmd();
// disable spell check
showSpell = false;
if (links()) {
for (ScoreElement* e : *links()) {
if (e == this)
continue;
Harmony* h = toHarmony(e);
// transpose if necessary
// at this point chord will already have been rendered in same key as original
// (as a result of TextBase::endEdit() calling setText() for linked elements)
// we may now need to change the TPC's and the text, and re-render
if (score()->styleB(Sid::concertPitch) != h->score()->styleB(Sid::concertPitch)) {
Part* partDest = h->part();
Segment* segment = toSegment(parent());
Fraction tick = segment ? segment->tick() : Fraction(-1,1);
Interval interval = partDest->instrument(tick)->transpose();
if (!interval.isZero()) {
if (!h->score()->styleB(Sid::concertPitch))
interval.flip();
int rootTpc = transposeTpc(h->rootTpc(), interval, true);
int baseTpc = transposeTpc(h->baseTpc(), interval, true);
//score()->undoTransposeHarmony(h, rootTpc, baseTpc);
h->setRootTpc(rootTpc);
h->setBaseTpc(baseTpc);
h->setPlainText(h->harmonyName());
h->setHarmony(h->plainText());
h->triggerLayout();
}
}
}
}
}
//---------------------------------------------------------
// setHarmony
//---------------------------------------------------------
void Harmony::setHarmony(const QString& s)
{
int r, b;
const ChordDescription* cd = parseHarmony(s, &r, &b);
if (!cd && _parsedForm && _parsedForm->parseable()) {
// our first time encountering this chord
// generate a descriptor and use it
cd = generateDescription();
_id = cd->id;
}
if (cd) {
setRootTpc(r);
setBaseTpc(b);
render();
}
else {
// unparseable chord, render as plain text
for (const TextSegment* ts : textList)
delete ts;
textList.clear();
setRootTpc(Tpc::TPC_INVALID);
setBaseTpc(Tpc::TPC_INVALID);
_id = -1;
render();
}
}
//---------------------------------------------------------
// baseLine
//---------------------------------------------------------
qreal Harmony::baseLine() const
{
return (textList.empty()) ? TextBase::baseLine() : 0.0;
}
//---------------------------------------------------------
// text
//---------------------------------------------------------
QString HDegree::text() const
{
if (_type == HDegreeType::UNDEF)
return QString();
const char* d = 0;
switch(_type) {
case HDegreeType::UNDEF: break;
case HDegreeType::ADD: d= "add"; break;
case HDegreeType::ALTER: d= "alt"; break;
case HDegreeType::SUBTRACT: d= "sub"; break;
}
QString degree(d);
switch(_alter) {
case -1: degree += "b"; break;
case 1: degree += "#"; break;
default: break;
}
QString s = QString("%1").arg(_value);
QString ss = degree + s;
return ss;
}
//---------------------------------------------------------
// findNext
/// find the next Harmony in the score
///
/// returns 0 if there is none
//---------------------------------------------------------
Harmony* Harmony::findNext() const
{
Segment* seg = toSegment(parent());
Segment* cur = seg->next1();
while (cur) {
//find harmony on same track
Element* e = cur->findAnnotation(ElementType::HARMONY,
track(), track());
if (e) {
//we have found harmony element
return toHarmony(e);
}
cur = cur->next1();
}
return 0;
}
//---------------------------------------------------------
// findPrev
/// find the previous Harmony in the score
///
/// returns 0 if there is none
//---------------------------------------------------------
Harmony* Harmony::findPrev() const
{
Segment* seg = toSegment(parent());
Segment* cur = seg->prev1();
while (cur) {
//find harmony on same track
Element* e = cur->findAnnotation(ElementType::HARMONY,
track(), track());
if (e) {
//we have found harmony element
return toHarmony(e);
}
cur = cur->prev1();
}
return 0;
}
//---------------------------------------------------------
// ticksTilNext
/// finds ticks until the next chord symbol or end of score
///
/// stopAtMeasureEnd being set to true will have the loop
/// stop at measure end.
//---------------------------------------------------------
Fraction Harmony::ticksTilNext(bool stopAtMeasureEnd) const
{
Segment* seg = toSegment(parent());
Fraction duration = seg->ticks();
Segment* cur = seg->next1();
while (cur) {
if (stopAtMeasureEnd && (cur->measure() != seg->measure()))
break; //limit by measure end
//find harmony on same track
Element* e = cur->findAnnotation(ElementType::HARMONY,
track(), track());
if (e) {
//we have found the next chord symbol
//set duration to the difference between
//the two chord symbols
duration = e->tick() - tick();
break;
}
//keep adding the duration of the current segment
//in case we are not able to find a next
//chord symbol
duration += cur->ticks();
cur = cur->next1();
}
return duration;
}
//---------------------------------------------------------
// fromXml
// lookup harmony in harmony data base
// using musicXml "kind" string and degree list
//---------------------------------------------------------
const ChordDescription* Harmony::fromXml(const QString& kind, const QList<HDegree>& dl)
{
QStringList degrees;
for (const HDegree& d : dl)
degrees.append(d.text());
QString lowerCaseKind = kind.toLower();
const ChordList* cl = score()->style().chordList();
for (const ChordDescription& cd : *cl) {
QString k = cd.xmlKind;
QString lowerCaseK = k.toLower(); // required for xmlKind Tristan
QStringList d = cd.xmlDegrees;
if ((lowerCaseKind == lowerCaseK) && (d == degrees)) {
// qDebug("harmony found in db: %s %s -> %d", qPrintable(kind), qPrintable(degrees), cd->id);
return &cd;
}
}
return 0;
}
//---------------------------------------------------------
// fromXml
// lookup harmony in harmony data base
// using musicXml "kind" string only
//---------------------------------------------------------
const ChordDescription* Harmony::fromXml(const QString& kind)
{
QString lowerCaseKind = kind.toLower();
const ChordList* cl = score()->style().chordList();
for (const ChordDescription& cd : *cl) {
if (lowerCaseKind == cd.xmlKind)
return &cd;
}
return 0;
}
//---------------------------------------------------------
// fromXml
// construct harmony directly from XML
// build name first
// then generate chord description from that
//---------------------------------------------------------
const ChordDescription* Harmony::fromXml(const QString& kind, const QString& kindText, const QString& symbols, const QString& parens, const QList<HDegree>& dl)
{
ParsedChord* pc = new ParsedChord;
_textName = pc->fromXml(kind, kindText, symbols, parens, dl, score()->style().chordList());
_parsedForm = pc;
const ChordDescription* cd = getDescription(_textName,pc);
return cd;
}
//---------------------------------------------------------
// descr
// look up id in chord list
// return chord description if found, or null
//---------------------------------------------------------
const ChordDescription* Harmony::descr() const
{
return score()->style().chordDescription(_id);
}
//---------------------------------------------------------
// descr
// look up name in chord list
// optionally look up by parsed chord as fallback
// return chord description if found, or null
//---------------------------------------------------------
const ChordDescription* Harmony::descr(const QString& name, const ParsedChord* pc) const
{
const ChordList* cl = score()->style().chordList();
const ChordDescription* match = 0;
if (cl) {
for (const ChordDescription& cd : *cl) {
for (const QString& s : cd.names) {
if (s == name)
return &cd;
else if (pc) {
for (const ParsedChord& sParsed : cd.parsedChords) {
if (sParsed == *pc)
match = &cd;
}
}
}
}
}
// exact match failed, so fall back on parsed match if one was found
return match;
}
//---------------------------------------------------------
// getDescription
// look up id in chord list
// return chord description if found
// if not found, and chord is parseable,
// generate a new chord description
// and add to chord list
//---------------------------------------------------------
const ChordDescription* Harmony::getDescription()
{
const ChordDescription* cd = descr();
if (cd && !cd->names.empty())
_textName = cd->names.front();
else if (_textName != "") {
cd = generateDescription();
_id = cd->id;
}
return cd;
}
//---------------------------------------------------------
// getDescription
// same but lookup by name and optionally parsed chord
//---------------------------------------------------------
const ChordDescription* Harmony::getDescription(const QString& name, const ParsedChord* pc)
{
const ChordDescription* cd = descr(name, pc);
if (cd)
_id = cd->id;
else {
cd = generateDescription();
_id = cd->id;
}
return cd;
}
//---------------------------------------------------------
// getRealizedHarmony
// get realized harmony or create one for the current symbol
// also updates the realized harmony and accounts for
// transposition. RealizedHarmony objects cannot be cached
// since the notes generated depends on context rather than
// just root, bass, chord symbol, and voicing.
//---------------------------------------------------------
const RealizedHarmony& Harmony::getRealizedHarmony()
{
int offset = 0; //semitone offset for pitch adjustment
Staff* st = staff();
Interval interval = st->part()->instrument(tick())->transpose();
if (!score()->styleB(Sid::concertPitch))
offset = interval.chromatic;
//Adjust for Nashville Notation, might be temporary
if (_harmonyType == HarmonyType::NASHVILLE && !_realizedHarmony.valid()) {
Key key = staff()->key(tick());
//parse root
int rootTpc = function2Tpc(_function, key);
//parse bass
int slash = _textName.lastIndexOf('/');
int bassTpc;
if (slash == -1)
bassTpc = Tpc::TPC_INVALID;
else
bassTpc = function2Tpc(_textName.mid(slash + 1), key);
_realizedHarmony.update(rootTpc, bassTpc, offset);
}
else
_realizedHarmony.update(_rootTpc, _baseTpc, offset);
return _realizedHarmony;
}
//---------------------------------------------------------
// realizedHarmony
// get realized harmony or create one for the current symbol
// without updating the realized harmony
//---------------------------------------------------------
RealizedHarmony& Harmony::realizedHarmony()
{
return _realizedHarmony;
}
//---------------------------------------------------------
// generateDescription
// generate new chord description from _textName
// add to chord list using private id
//---------------------------------------------------------
const ChordDescription* Harmony::generateDescription()
{
ChordList* cl = score()->style().chordList();
ChordDescription cd(_textName);
cd.complete(_parsedForm, cl);
// remove parsed chord from description
// so we will only match it literally in the future
cd.parsedChords.clear();
return &*cl->insert(cd.id, cd);
}
//---------------------------------------------------------
// layout
//---------------------------------------------------------
void Harmony::layout()
{
if (!parent()) {
setPos(0.0, 0.0);
setOffset(0.0, 0.0);
layout1();
return;
}
//if (isStyled(Pid::OFFSET))
// setOffset(propertyDefault(Pid::OFFSET).toPointF());
if (placeBelow())
rypos() = staff() ? staff()->height() : 0.0;
else
rypos() = 0.0;
layout1();
qreal yy = ipos().y();
qreal xx = 0.0;
if (parent()->isFretDiagram()) {
if (isStyled(Pid::ALIGN))
setAlign(Align::HCENTER | Align::BASELINE);
yy = -score()->styleP(Sid::harmonyFretDist);
}
qreal hb = lineHeight() - TextBase::baseLine();
if (align() & Align::BOTTOM)
yy -= hb;
else if (align() & Align::VCENTER) {
yy -= hb;
yy += (height() * .5);
}
else if (align() & Align::BASELINE) {
}
else { // Align::TOP
yy -= hb;
yy += height();
}
qreal cw = symWidth(SymId::noteheadBlack);
if (align() & Align::RIGHT) {
xx += cw;
xx -= width();
}
else if (align() & Align::HCENTER) {
if (parent()->isFretDiagram()) {
FretDiagram* fd = toFretDiagram(parent());
xx += fd->centerX();
xx -= width() * .5;
}
else {
xx += (cw * .5);
xx -= (width() * .5);
}
}
setPos(xx, yy);
}
//---------------------------------------------------------
// layout1
//---------------------------------------------------------
void Harmony::layout1()
{
if (isLayoutInvalid())
createLayout();
if (textBlockList().empty())
textBlockList().append(TextBlock());
calculateBoundingRect(); // for normal symbols this is called in layout: computeMinWidth()
if (hasFrame())
layoutFrame();
score()->addRefresh(canvasBoundingRect());
}
//---------------------------------------------------------
// calculateBoundingRect
//---------------------------------------------------------
void Harmony::calculateBoundingRect()
{
if (textList.empty())
TextBase::layout1();
else {
QRectF bb;
for (const TextSegment* ts : textList)
bb |= ts->tightBoundingRect().translated(ts->x, ts->y);
setbbox(bb);
for (int i = 0; i < rows(); ++i) {
TextBlock& t = textBlockList()[i];
// when MS switch to editing Harmony MS draws text defined by textBlockList().
// When MS switches back to normal state it draws text from textList
// To correct placement of text in editing we need to layout textBlockList() elements
t.layout(this);
for (auto& s : t.fragments()) {
s.pos = { 0, 0 };
}
}
}
}
//---------------------------------------------------------
// draw
//---------------------------------------------------------
void Harmony::draw(QPainter* painter) const
{
// painter->setPen(curColor());
if (textList.empty()) {
TextBase::draw(painter);
return;
}
if (hasFrame()) {
if (frameWidth().val() != 0.0) {
QColor color = frameColor();
QPen pen(color, frameWidth().val() * spatium(), Qt::SolidLine,
Qt::SquareCap, Qt::MiterJoin);
painter->setPen(pen);
}
else
painter->setPen(Qt::NoPen);
QColor bg(bgColor());
painter->setBrush(bg.alpha() ? QBrush(bg) : Qt::NoBrush);
if (circle())
painter->drawArc(frame, 0, 5760);
else {
int r2 = frameRound();
if (r2 > 99)
r2 = 99;
painter->drawRoundedRect(frame, frameRound(), r2);
}
}
painter->setBrush(Qt::NoBrush);
QColor color = textColor();
painter->setPen(color);
for (const TextSegment* ts : textList) {
QFont f(ts->font);
f.setPointSizeF(f.pointSizeF() * MScore::pixelRatio);
painter->setFont(f);
painter->drawText(QPointF(ts->x, ts->y), ts->text);
}
}
//---------------------------------------------------------
// drawEditMode
//---------------------------------------------------------
void Harmony::drawEditMode(QPainter* p, EditData& ed)
{
TextBase::drawEditMode(p, ed);
QColor originalColor = color();
if (showSpell) {
setColor(QColor(Qt::red));
setSelected(false);
}
QPointF pos(canvasPos());
p->translate(pos);
TextBase::draw(p);
p->translate(-pos);
if (showSpell) {
setColor(originalColor);
setSelected(true);
}
}
//---------------------------------------------------------
// TextSegment
//---------------------------------------------------------
TextSegment::TextSegment(const QString& s, const QFont& f, qreal x, qreal y)
{
set(s, f, x, y);
select = false;
}
//---------------------------------------------------------
// width
//---------------------------------------------------------
qreal TextSegment::width() const
{
QFontMetricsF fm(font, MScore::paintDevice());
#if 1
return fm.width(text);
#else
qreal w = 0.0;
foreach(QChar c, text) {
// if we calculate width by character, at least skip high surrogates
if (c.isHighSurrogate())
continue;
w += fm.width(c);
}
return w;
#endif
}
//---------------------------------------------------------
// boundingRect
//---------------------------------------------------------
QRectF TextSegment::boundingRect() const
{
QFontMetricsF fm(font, MScore::paintDevice());
return fm.boundingRect(text);
}
//---------------------------------------------------------
// tightBoundingRect
//---------------------------------------------------------
QRectF TextSegment::tightBoundingRect() const
{
QFontMetricsF fm(font, MScore::paintDevice());
return fm.tightBoundingRect(text);
}
//---------------------------------------------------------
// set
//---------------------------------------------------------
void TextSegment::set(const QString& s, const QFont& f, qreal _x, qreal _y)
{
font = f;
x = _x;
y = _y;
setText(s);
}
//---------------------------------------------------------
// render
//---------------------------------------------------------
void Harmony::render(const QString& s, qreal& x, qreal& y)
{
int fontIdx = 0;
if (!s.isEmpty()) {
QFont f = _harmonyType != HarmonyType::ROMAN ? fontList[fontIdx] : font();
TextSegment* ts = new TextSegment(s, f, x, y);
textList.append(ts);
x += ts->width();
}
}
//---------------------------------------------------------
// render
//---------------------------------------------------------
void Harmony::render(const QList<RenderAction>& renderList, qreal& x, qreal& y, int tpc, NoteSpellingType noteSpelling, NoteCaseType noteCase)
{
ChordList* chordList = score()->style().chordList();
QStack<QPointF> stack;
int fontIdx = 0;
qreal _spatium = spatium();
qreal mag = magS();
// qDebug("===");
for (const RenderAction& a : renderList) {
// a.print();
if (a.type == RenderAction::RenderActionType::SET) {
TextSegment* ts = new TextSegment(fontList[fontIdx], x, y);
ChordSymbol cs = chordList->symbol(a.text);
if (cs.isValid()) {
ts->font = fontList[cs.fontIdx];
ts->setText(cs.value);
}
else
ts->setText(a.text);
if (_harmonyType == HarmonyType::NASHVILLE) {
qreal nmag = chordList->nominalMag();
ts->font.setPointSizeF(ts->font.pointSizeF() * nmag);
}
textList.append(ts);
x += ts->width();
}
else if (a.type == RenderAction::RenderActionType::MOVE) {
x += a.movex * mag * _spatium * .2;
y += a.movey * mag * _spatium * .2;
}
else if (a.type == RenderAction::RenderActionType::PUSH)
stack.push(QPointF(x,y));
else if (a.type == RenderAction::RenderActionType::POP) {
if (!stack.empty()) {
QPointF pt = stack.pop();
x = pt.x();
y = pt.y();
}
else
qDebug("RenderAction::RenderActionType::POP: stack empty");
}
else if (a.type == RenderAction::RenderActionType::NOTE) {
QString c;
int acc;
if (tpcIsValid(tpc))
tpc2name(tpc, noteSpelling, noteCase, c, acc);
else if (_function.size() > 0)
c = _function.at(_function.size() - 1);
TextSegment* ts = new TextSegment(fontList[fontIdx], x, y);
QString lookup = "note" + c;
ChordSymbol cs = chordList->symbol(lookup);
if (!cs.isValid())
cs = chordList->symbol(c);
if (cs.isValid()) {
ts->font = fontList[cs.fontIdx];
ts->setText(cs.value);
}
else {
ts->setText(c);
}
textList.append(ts);
x += ts->width();
}
else if (a.type == RenderAction::RenderActionType::ACCIDENTAL) {
QString c;
QString acc;
QString context = "accidental";
if (tpcIsValid(tpc))
tpc2name(tpc, noteSpelling, noteCase, c, acc);
else if (_function.size() > 1)
acc = _function.at(0);
// German spelling - use special symbol for accidental in TPC_B_B
// to allow it to be rendered as either Bb or B
if (tpc == Tpc::TPC_B_B && noteSpelling == NoteSpellingType::GERMAN)
context = "german_B";
if (acc != "") {
TextSegment* ts = new TextSegment(fontList[fontIdx], x, y);
QString lookup = context + acc;
ChordSymbol cs = chordList->symbol(lookup);
if (!cs.isValid())
cs = chordList->symbol(acc);
if (cs.isValid()) {
ts->font = fontList[cs.fontIdx];
ts->setText(cs.value);
}
else
ts->setText(acc);
textList.append(ts);
x += ts->width();
}
}
else
qDebug("unknown render action %d", static_cast<int>(a.type));
}
}
//---------------------------------------------------------
// render
// construct Chord Symbol
//---------------------------------------------------------
void Harmony::render()
{
int capo = score()->styleI(Sid::capoPosition);
ChordList* chordList = score()->style().chordList();
fontList.clear();
for (const ChordFont& cf : chordList->fonts) {
QFont ff(font());
ff.setPointSizeF(ff.pointSizeF() * cf.mag);
if (!(cf.family.isEmpty() || cf.family == "default"))
ff.setFamily(cf.family);
fontList.append(ff);
}
if (fontList.empty())
fontList.append(font());
for (const TextSegment* s : textList)
delete s;
textList.clear();
qreal x = 0.0, y = 0.0;
determineRootBaseSpelling();
if (_leftParen)
render("( ", x, y);
if (_rootTpc != Tpc::TPC_INVALID) {
// render root
render(chordList->renderListRoot, x, y, _rootTpc, _rootSpelling, _rootRenderCase);
// render extension
const ChordDescription* cd = getDescription();
if (cd)
render(cd->renderList, x, y, 0);
}
else if (_harmonyType == HarmonyType::NASHVILLE) {
// render function
render(chordList->renderListFunction, x, y, _rootTpc, _rootSpelling, _rootRenderCase);
qreal adjust = chordList->nominalAdjust();
y += adjust * magS() * spatium() * .2;
// render extension
const ChordDescription* cd = getDescription();
if (cd)
render(cd->renderList, x, y, 0);
}
else {
render(_textName, x, y);
}
// render bass
if (_baseTpc != Tpc::TPC_INVALID)
render(chordList->renderListBase, x, y, _baseTpc, _baseSpelling, _baseRenderCase);
if (_rootTpc != Tpc::TPC_INVALID && capo > 0 && capo < 12) {
int tpcOffset[] = { 0, 5, -2, 3, -4, 1, 6, -1, 4, -3, 2, -5 };
int capoRootTpc = _rootTpc + tpcOffset[capo];
int capoBassTpc = _baseTpc;
if (capoBassTpc != Tpc::TPC_INVALID)
capoBassTpc += tpcOffset[capo];
/*
* For guitarists, avoid x and bb in Root or Bass,
* and also avoid E#, B#, Cb and Fb in Root.
*/
if (capoRootTpc < 8 || (capoBassTpc != Tpc::TPC_INVALID && capoBassTpc < 6)) {
capoRootTpc += 12;
if (capoBassTpc != Tpc::TPC_INVALID)
capoBassTpc += 12;
}
else if (capoRootTpc > 24 || (capoBassTpc != Tpc::TPC_INVALID && capoBassTpc > 26)) {
capoRootTpc -= 12;
if (capoBassTpc != Tpc::TPC_INVALID)
capoBassTpc -= 12;
}
render("(", x, y);
render(chordList->renderListRoot, x, y, capoRootTpc, _rootSpelling, _rootRenderCase);
// render extension
const ChordDescription* cd = getDescription();
if (cd)
render(cd->renderList, x, y, 0);
if (capoBassTpc != Tpc::TPC_INVALID)
render(chordList->renderListBase, x, y, capoBassTpc, _baseSpelling, _baseRenderCase);
render(")", x, y);
}
if (_rightParen)
render(" )", x, y);
}
//---------------------------------------------------------
// spatiumChanged
//---------------------------------------------------------
void Harmony::spatiumChanged(qreal oldValue, qreal newValue)
{
TextBase::spatiumChanged(oldValue, newValue);
render();
}
//---------------------------------------------------------
// localSpatiumChanged
//---------------------------------------------------------
void Harmony::localSpatiumChanged(qreal oldValue, qreal newValue)
{
TextBase::localSpatiumChanged(oldValue, newValue);
render();
}
//---------------------------------------------------------
// extensionName
//---------------------------------------------------------
const QString& Harmony::extensionName() const
{
return _textName;
}
//---------------------------------------------------------
// xmlKind
//---------------------------------------------------------
QString Harmony::xmlKind() const
{
const ChordDescription* cd = descr();
return cd ? cd->xmlKind : QString();
}
//---------------------------------------------------------
// musicXmlText
//---------------------------------------------------------
QString Harmony::musicXmlText() const
{
const ChordDescription* cd = descr();
return cd ? cd->xmlText : QString();
}
//---------------------------------------------------------
// xmlSymbols
//---------------------------------------------------------
QString Harmony::xmlSymbols() const
{
const ChordDescription* cd = descr();
return cd ? cd->xmlSymbols : QString();
}
//---------------------------------------------------------
// xmlParens
//---------------------------------------------------------
QString Harmony::xmlParens() const
{
const ChordDescription* cd = descr();
return cd ? cd->xmlParens : QString();
}
//---------------------------------------------------------
// xmlDegrees
//---------------------------------------------------------
QStringList Harmony::xmlDegrees() const
{
const ChordDescription* cd = descr();
return cd ? cd->xmlDegrees : QStringList();
}
//---------------------------------------------------------
// degree
//---------------------------------------------------------
HDegree Harmony::degree(int i) const
{
return _degreeList.value(i);
}
//---------------------------------------------------------
// addDegree
//---------------------------------------------------------
void Harmony::addDegree(const HDegree& d)
{
_degreeList << d;
}
//---------------------------------------------------------
// numberOfDegrees
//---------------------------------------------------------
int Harmony::numberOfDegrees() const
{
return _degreeList.size();
}
//---------------------------------------------------------
// clearDegrees
//---------------------------------------------------------
void Harmony::clearDegrees()
{
_degreeList.clear();
}
//---------------------------------------------------------
// degreeList
//---------------------------------------------------------
const QList<HDegree>& Harmony::degreeList() const
{
return _degreeList;
}
//---------------------------------------------------------
// parsedForm
//---------------------------------------------------------
const ParsedChord* Harmony::parsedForm()
{
if (!_parsedForm) {
ChordList* cl = score()->style().chordList();
_parsedForm = new ParsedChord();
_parsedForm->parse(_textName, cl, false);
}
return _parsedForm;
}
//---------------------------------------------------------
// setHarmonyType
//---------------------------------------------------------
void Harmony::setHarmonyType(HarmonyType val)
{
_harmonyType = val;
setPlacement(Placement(propertyDefault(Pid::PLACEMENT).toInt()));
switch (_harmonyType) {
case HarmonyType::STANDARD:
initTid(Tid::HARMONY_A);
break;
case HarmonyType::ROMAN:
initTid(Tid::HARMONY_ROMAN);
break;
case HarmonyType::NASHVILLE:
initTid(Tid::HARMONY_NASHVILLE);
break;
}
// TODO: convert text
}
//---------------------------------------------------------
// userName
//---------------------------------------------------------
QString Harmony::userName() const
{
switch (_harmonyType) {
case HarmonyType::ROMAN:
return QObject::tr("Roman numeral");
case HarmonyType::NASHVILLE:
return QObject::tr("Nashville number");
case HarmonyType::STANDARD:
break;
}
return Element::userName();
}
//---------------------------------------------------------
// accessibleInfo
//---------------------------------------------------------
QString Harmony::accessibleInfo() const
{
return QString("%1: %2").arg(userName()).arg(harmonyName());
}
//---------------------------------------------------------
// screenReaderInfo
//---------------------------------------------------------
QString Harmony::screenReaderInfo() const
{
QString rez = userName();
switch (_harmonyType) {
case HarmonyType::ROMAN: {
QString aux = _textName;
bool hasUpper = aux.contains('I') || aux.contains('V');
bool hasLower = aux.contains('i') || aux.contains('v');
if (hasLower && !hasUpper)
rez = QString("%1 %2").arg(rez).arg(QObject::tr("lower case"));
aux = aux.toLower();
static std::vector<std::pair<QString, QString>> rnaReplacements {
{ "vii", "7" },
{ "vi", "6" },
{ "iv", "4" },
{ "v", "5" },
{ "iii", "3" },
{ "ii", "2" },
{ "i", "1" },
};
static std::vector<std::pair<QString, QString>> symbolReplacements {
{ "b", "" },
{ "h", "" },
{ "#", "" },
{ "bb", "𝄫" },
{ "##", "𝄪" },
// TODO: use SMuFL glyphs and translate
//{ "o", ""},
//{ "0", ""},
//{ "\+", ""},
//{ "\^", ""},
};
for (auto const &r : rnaReplacements)
aux.replace(r.first, r.second);
for (auto const &r : symbolReplacements) {
// only replace when not preceded by backslash
QString s = "(?<!\\\\)" + r.first;
QRegularExpression re(s);
aux.replace(re, r.second);
}
// construct string one character at a time
for (auto c : aux)
rez = QString("%1 %2").arg(rez).arg(c);
}
return rez;
case HarmonyType::NASHVILLE:
if (!_function.isEmpty())
rez = QString("%1 %2").arg(rez).arg(_function);
break;
case HarmonyType::STANDARD:
default:
if (_rootTpc != Tpc::TPC_INVALID)
rez = QString("%1 %2").arg(rez).arg(tpc2name(_rootTpc, NoteSpellingType::STANDARD, NoteCaseType::AUTO, true));
}
if (const_cast<Harmony*>(this)->parsedForm() && !hTextName().isEmpty()) {
QString aux = const_cast<Harmony*>(this)->parsedForm()->handle();
aux = aux.replace("#", QObject::tr("")).replace("<", "");
QString extension = "";
for (QString s : aux.split(">", QString::SkipEmptyParts)) {
if (!s.contains("blues"))
s.replace("b", QObject::tr(""));
extension += s + " ";
}
rez = QString("%1 %2").arg(rez).arg(extension);
}
else {
rez = QString("%1 %2").arg(rez).arg(hTextName());
}
if (_baseTpc != Tpc::TPC_INVALID)
rez = QString("%1 / %2").arg(rez).arg(tpc2name(_baseTpc, NoteSpellingType::STANDARD, NoteCaseType::AUTO, true));
return rez;
}
//---------------------------------------------------------
// acceptDrop
//---------------------------------------------------------
bool Harmony::acceptDrop(EditData& data) const
{
Element* e = data.dropElement;
if (e->isFretDiagram()) {
return true;
}
else if (e->isSymbol() || e->isFSymbol()) {
// symbols can be added in edit mode
if (data.getData(this))
return true;
else
return false;
}
return false;
}
//---------------------------------------------------------
// drop
//---------------------------------------------------------
Element* Harmony::drop(EditData& data)
{
Element* e = data.dropElement;
if (e->isFretDiagram()) {
FretDiagram* fd = toFretDiagram(e);
fd->setParent(parent());
fd->setTrack(track());
score()->undoAddElement(fd);
}
else if (e->isSymbol() || e->isFSymbol()) {
TextBase::drop(data);
layout1();
e = 0; // cannot select
}
else {
qWarning("Harmony: cannot drop <%s>\n", e->name());
delete e;
e = 0;
}
return e;
}
//---------------------------------------------------------
// getProperty
//---------------------------------------------------------
QVariant Harmony::getProperty(Pid pid) const
{
switch (pid) {
case Pid::PLAY:
return QVariant(_play);
break;
case Pid::HARMONY_TYPE:
return QVariant(int(_harmonyType));
break;
case Pid::HARMONY_VOICE_LITERAL:
return _realizedHarmony.literal();
break;
case Pid::HARMONY_VOICING:
return int(_realizedHarmony.voicing());
break;
case Pid::HARMONY_DURATION:
return int(_realizedHarmony.duration());
break;
default:
return TextBase::getProperty(pid);
}
}
//---------------------------------------------------------
// setProperty
//---------------------------------------------------------
bool Harmony::setProperty(Pid pid, const QVariant& v)
{
switch (pid) {
case Pid::PLAY:
setPlay(v.toBool());
break;
case Pid::HARMONY_TYPE:
setHarmonyType(HarmonyType(v.toInt()));
break;
case Pid::HARMONY_VOICE_LITERAL:
_realizedHarmony.setLiteral(v.toBool());
break;
case Pid::HARMONY_VOICING:
_realizedHarmony.setVoicing(Voicing(v.toInt()));
break;
case Pid::HARMONY_DURATION:
_realizedHarmony.setDuration(HDuration(v.toInt()));
break;
default:
if (TextBase::setProperty(pid, v)) {
if (pid == Pid::TEXT)
setHarmony(v.toString());
render();
break;
}
return false;
}
return true;
}
//---------------------------------------------------------
// propertyDefault
//---------------------------------------------------------
QVariant Harmony::propertyDefault(Pid id) const
{
QVariant v;
switch (id) {
case Pid::HARMONY_TYPE:
v = int(HarmonyType::STANDARD);
break;
case Pid::SUB_STYLE: {
switch (_harmonyType) {
case HarmonyType::STANDARD:
v = int(Tid::HARMONY_A);
break;
case HarmonyType::ROMAN:
v = int(Tid::HARMONY_ROMAN);
break;
case HarmonyType::NASHVILLE:
v = int(Tid::HARMONY_NASHVILLE);
break;
}
}
break;
case Pid::OFFSET:
if (parent() && parent()->isFretDiagram()) {
v = QVariant(QPointF(0.0, 0.0));
break;
}
// fall-through
default:
v = TextBase::propertyDefault(id);
break;
}
return v;
}
//---------------------------------------------------------
// getPropertyStyle
//---------------------------------------------------------
Sid Harmony::getPropertyStyle(Pid pid) const
{
if (pid == Pid::OFFSET) {
if (parent() && parent()->isFretDiagram())
return Sid::NOSTYLE;
else if (tid() == Tid::HARMONY_A)
return placeAbove() ? Sid::chordSymbolAPosAbove : Sid::chordSymbolAPosBelow;
else if (tid() == Tid::HARMONY_B)
return placeAbove() ? Sid::chordSymbolBPosAbove : Sid::chordSymbolBPosBelow;
else if (tid() == Tid::HARMONY_ROMAN)
return placeAbove() ? Sid::romanNumeralPosAbove : Sid::romanNumeralPosBelow;
else if (tid() == Tid::HARMONY_NASHVILLE)
return placeAbove() ? Sid::nashvilleNumberPosAbove : Sid::nashvilleNumberPosBelow;
}
if (pid == Pid::PLACEMENT) {
switch (_harmonyType) {
case HarmonyType::STANDARD:
return Sid::harmonyPlacement;
case HarmonyType::ROMAN:
return Sid::romanNumeralPlacement;
case HarmonyType::NASHVILLE:
return Sid::nashvilleNumberPlacement;
}
}
return TextBase::getPropertyStyle(pid);
}
}