Skip to content
Snippets Groups Projects
tmainscoreobject.cpp 31 KiB
Newer Older
  • Learn to ignore specific revisions
  • /***************************************************************************
    
     *   Copyright (C) 2017-2021 by Tomasz Bojczuk                             *
    
     *   seelook@gmail.com                                                     *
     *                                                                         *
     *   This program is free software; you can redistribute it and/or modify  *
     *   it under the terms of the GNU General Public License as published by  *
     *   the Free Software Foundation; either version 3 of the License, or     *
     *   (at your option) any later version.                                   *
     *                                                                         *
     *   This program is distributed in the hope that it will be useful,       *
     *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
     *   GNU General Public License for more details.                          *
     *                                                                         *
     *  You should have received a copy of the GNU General Public License      *
     *  along with this program.  If not, see <http://www.gnu.org/licenses/>.  *
     ***************************************************************************/
    
    #include "tmainscoreobject.h"
    
    #include <music/timportscore.h>
    
    #include <music/ttechnical.h>
    #include <qtr.h>
    #include <score/tmeasureobject.h>
    
    #include <score/tscoreobject.h>
    
    #include <score/tstaffitem.h>
    #include <score/tstafflines.h>
    
    #include <taction.h>
    #include <tcolor.h>
    #include <tglobals.h>
    #include <tnootkaqml.h>
    #include <tsound.h>
    
    #include <QtCore/qsettings.h>
    #include <QtCore/qtimer.h>
    
    #include <QtGui/qguiapplication.h>
    #include <QtGui/qpalette.h>
    
    #include <QtQml/qqmlapplicationengine.h>
    
    #include <QtQml/qqmlengine.h>
    
    TmainScoreObject *TmainScoreObject::m_instance = nullptr;
    
    TmainScoreObject::TmainScoreObject(QObject *parent)
        : QObject(parent)
    
        if (m_instance) {
            qDebug() << "TmainScoreObject instance already exists!";
            return;
        }
        m_instance = this;
    
        m_goodNote = new Tnote();
    
        m_showNamesAct = new Taction(tr("Show note names"), QString(), this);
        m_showNamesAct->setCheckable(true);
        m_showNamesAct->setChecked(GLOB->namesOnScore());
        m_showNamesAct->setTip(tr("Shows names of all notes on the staff."));
    
        m_extraAccidsAct = new Taction(tr("Additional accidentals"), QString(), this);
        m_extraAccidsAct->setCheckable(true);
        m_extraAccidsAct->setTip(tr(
            "Shows accidentals from the key signature also next to a note. <b>WARING! It never occurs in real scores - use it only for theoretical purposes.</b>"));
        m_extraAccidsAct->setChecked(false); // never stored - theoretical purposes
    
        m_zoomOutAct = new Taction(tr("Zoom score out"), QStringLiteral("zoom-out"), this);
        m_zoomInAct = new Taction(tr("Zoom score in"), QStringLiteral("zoom-in"), this);
        m_transposeAct = new Taction(qTR("Transpose", "Transpose"), QStringLiteral("transpose"), this);
    
        m_playAct = new Taction(qTR("TtoolBar", "Play"), QStringLiteral("playMelody"), this);
        m_playAct->setBgColor(QColor(0, 255, 0));
        m_openXmlAct = new Taction(qTR("QShortcut", "Open"), QStringLiteral("open"), this);
        connect(m_openXmlAct, &Taction::triggered, this, &TmainScoreObject::openXmlActSlot);
    
        m_saveXmlAct = new Taction(qTR("QShortcut", "Save"), QStringLiteral("save"), this);
        connect(m_saveXmlAct, &Taction::triggered, this, &TmainScoreObject::getMelodyNameSlot);
    
        m_randMelodyAct = new Taction(tr("Generate melody"), QStringLiteral("melody"), this);
        m_randMelodyAct->setBgColor(qApp->palette().highlight().color());
        connect(m_randMelodyAct, &Taction::triggered, this, &TmainScoreObject::randMelodySlot);
        m_randMelodyAct->setTip(tr("Generate a melody with random notes."));
    
        m_melodyActions << m_playAct << m_openXmlAct << m_saveXmlAct << m_randMelodyAct;
    
        m_nextNoteAct = new Taction(tr("Next note"), QString(), this);
        m_prevNoteAct = new Taction(tr("Previous note"), QString(), this);
    
        m_notesMenuAct = new Taction(tr("notes", "musical notes of course") + QStringLiteral("   ⋮"), QStringLiteral("score"), this);
    
        m_reviewModeAct = new Taction(tr("Review mode"), QString(), this);
        m_reviewModeAct->setCheckable(true);
        m_reviewModeAct->setChecked(GLOB->showNotesDiff());
        m_reviewModeAct->setTip(tr("Do not change score when playing on instrument and display bar to see selected and played notes."));
        connect(m_reviewModeAct, &Taction::triggered, this, [=] {
            m_reviewModeAct->setChecked(!m_reviewModeAct->checked());
            GLOB->setShowNotesDiff(m_reviewModeAct->checked());
        });
    
        QQmlComponent actionsComp(NOO->qmlEngine(), this);
        m_openXmlAct->createQmlShortcut(&actionsComp, "StandardKey.Open; enabled: !GLOB.singleNoteMode && !GLOB.isExam");
        m_saveXmlAct->createQmlShortcut(&actionsComp, "StandardKey.Save; enabled: !GLOB.singleNoteMode && !GLOB.isExam");
        m_zoomOutAct->createQmlShortcut(&actionsComp, "StandardKey.ZoomOut; enabled: !GLOB.singleNoteMode");
    
        m_zoomInAct->createShortcutSequence(&actionsComp, "StandardKey.ZoomIn", "!GLOB.singleNoteMode");
    
        // HACK: Create m_playAct action shortcut only when enabled.
        m_randMelodyAct->createQmlShortcut(&actionsComp, "\"Ctrl+M\"; enabled: !GLOB.singleNoteMode && !GLOB.isExam");
        m_nextNoteAct->createQmlShortcut(&actionsComp, "StandardKey.MoveToNextChar");
        m_prevNoteAct->createQmlShortcut(&actionsComp, "StandardKey.MoveToPreviousChar");
    
        connect(SOUND, &Tsound::playingChanged, this, [=] {
            m_playAct->setIconTag(SOUND->playing() ? QLatin1String("stopMelody") : QLatin1String("playMelody"));
            m_playAct->setText(SOUND->playing() ? qTR("QShortcut", "Stop") : qTR("TtoolBar", "Play"));
        });
    
        isExamChangedSlot();
    
        connect(NOO, &TnootkaQML::wantOpenXml, this, &TmainScoreObject::openXmlFileSlot);
    
        qApp->installEventFilter(this);
    
        delete m_goodNote;
        m_instance = nullptr;
    }
    
    void TmainScoreObject::setScoreObject(TscoreObject *scoreObj)
    {
        if (m_scoreObj) {
            qDebug() << "[TmainScoreObject] score object was already set. FIX IT!";
            return;
    
        m_scoreObj = scoreObj;
        m_scoreObj->enableActions();
        connect(m_scoreObj, &TscoreObject::clicked, this, &TmainScoreObject::clicked);
        connect(m_scoreObj, &TscoreObject::readOnlyNoteClicked, this, &TmainScoreObject::readOnlyNoteClicked);
        connect(m_showNamesAct, &Taction::triggered, this, [=] {
            m_showNamesAct->setChecked(!m_showNamesAct->checked());
            m_scoreObj->setShowNoteNames(m_showNamesAct->checked());
        });
        connect(m_extraAccidsAct, &Taction::triggered, this, [=] {
            m_extraAccidsAct->setChecked(!m_extraAccidsAct->checked());
            m_scoreObj->setShowExtraAccids(m_extraAccidsAct->checked());
        });
        connect(m_playAct, &Taction::triggered, this, &TmainScoreObject::playScoreSlot);
        connect(m_zoomOutAct, &Taction::triggered, this, [=] {
            m_scoreObj->setScaleFactor(qMax(0.4, m_scoreObj->scaleFactor() - 0.2));
        });
        connect(m_zoomInAct, &Taction::triggered, this, [=] {
            m_scoreObj->setScaleFactor(qMin(m_scoreObj->scaleFactor() + 0.2, 1.4));
        });
        connect(GLOB, &Tglobals::isExamChanged, this, &TmainScoreObject::isExamChangedSlot);
        connect(m_scoreObj, &TscoreObject::singleNoteChanged, this, &TmainScoreObject::singleModeSlot);
        connect(GLOB, &Tglobals::showEnharmNotesChanged, this, &TmainScoreObject::checkSingleNoteVisibility);
        connect(GLOB, &Tglobals::enableDoubleAccidsChanged, this, &TmainScoreObject::checkSingleNoteVisibility);
        connect(m_scoreObj, &TscoreObject::keySignatureChanged, this, [=] {
            if (GLOB->keySignatureEnabled() && GLOB->showKeyName() && !GLOB->isExam())
                emit keyNameTextChanged();
        });
        connect(GLOB, &Tglobals::keyNameChanged, this, [=] {
            if (GLOB->keySignatureEnabled() && GLOB->showKeyName() && !GLOB->isExam())
                emit keyNameTextChanged();
        });
        connect(m_scoreObj->clearScoreAct(), &Taction::triggered, this, [=] {
            if (!m_scoreObj->singleNote() && !GLOB->isExam()) {
                SOUND->stopPlaying();
                m_title.clear();
                m_composer.clear();
                emit titleChanged();
            }
        });
        m_scoreObj->clearScoreAct()->setBgColor(QColor(255, 140, 0)); // orange
    #if !defined(Q_OS_ANDROID)
        m_scoreActions.prepend(m_reviewModeAct);
        m_scoreActions.prepend(m_scoreObj->editModeAct());
        m_scoreActions << m_scoreObj->insertNoteAct() << m_scoreObj->deleteNoteAct() << m_scoreObj->clearScoreAct() << m_transposeAct << m_notesMenuAct;
    
        m_scoreActions << m_randMelodyAct << m_openXmlAct << m_saveXmlAct;
    
        m_noteActions << m_scoreObj->riseAct() << m_scoreObj->lowerAct() << m_scoreObj->wholeNoteAct() << m_scoreObj->halfNoteAct() << m_scoreObj->quarterNoteAct()
                      << m_scoreObj->eighthNoteAct() << m_scoreObj->sixteenthNoteAct() << m_scoreObj->restNoteAct() << m_scoreObj->dotNoteAct()
                      << m_scoreObj->tieAct();
    
        connect(m_nextNoteAct, &Taction::triggered, this, [=] {
            if (!GLOB->isSingleNote()) {
                auto noteItem = m_scoreObj->selectedItem() ? m_scoreObj->getNext(m_scoreObj->selectedItem()) : m_scoreObj->note(0);
                if (m_scoreObj->readOnly()) {
                    if (noteItem)
                        emit m_scoreObj->readOnlyNoteClicked(noteItem->index());
                } else
                    m_scoreObj->setSelectedItem(noteItem);
            }
        });
        connect(m_prevNoteAct, &Taction::triggered, this, [=] {
            if (!GLOB->isSingleNote()) {
                auto noteItem = m_scoreObj->selectedItem() ? m_scoreObj->getPrev(m_scoreObj->selectedItem()) : m_scoreObj->note(m_scoreObj->notesCount() - 1);
                if (m_scoreObj->readOnly()) {
                    if (noteItem)
                        emit m_scoreObj->readOnlyNoteClicked(noteItem->index());
                } else
                    m_scoreObj->setSelectedItem(noteItem);
            }
        });
        connect(m_scoreObj, &TscoreObject::stavesHeightChanged, this, &TmainScoreObject::checkExtraStaves);
        connect(m_scoreObj, &TscoreObject::meterChanged, this, [=] {
            SOUND->setCurrentMeter(m_scoreObj->meterToInt());
        });
        SOUND->setCurrentMeter(m_scoreObj->meterToInt());
    
        if (!GLOB->isSingleNote() && GLOB->gotIt(QStringLiteral("noteSelected"), true))
            connect(m_scoreObj, &TscoreObject::selectedItemChanged, this, &TmainScoreObject::gotItNoteSelectedSlot);
    
        if (!m_playAct->shortcut() && !GLOB->isSingleNote()) { // HACK: Create play action shortcut only when enabled, otherwise it locks next question shortcut
            QQmlComponent actionsComp(NOO->qmlEngine(), this);
            m_playAct->createQmlShortcut(&actionsComp, "\" \"; enabled: !GLOB.singleNoteMode && !GLOB.isExam");
        }
    
    QString TmainScoreObject::keyNameText() const
    {
        return m_scoreObj ? NOO->majAndMinKeyName(m_scoreObj->keySignature()) : QString();
    
    void TmainScoreObject::setMainScoreItem(QQuickItem *msItem)
    {
        m_mainScoreItem = msItem;
        connect(m_mainScoreItem, &QQuickItem::heightChanged, this, &TmainScoreObject::checkExtraStaves);
    
    int TmainScoreObject::notesCount() const
    {
        return m_scoreObj->notesCount();
    
    void TmainScoreObject::setReadOnly(bool ro)
    {
        if (ro != m_scoreObj->readOnly()) {
            m_scoreObj->setReadOnly(ro);
            m_scoreObj->setAllowAdding(!ro);
            m_notesMenuAct->setEnabled(!ro && m_scoreObj->meter()->meter() != Tmeter::NoMeter);
        }
    
    void TmainScoreObject::clearScore()
    {
        m_scoreObj->clearScore();
        if (m_questionKey) {
            delete m_questionKey;
            m_questionKey = nullptr;
        }
        m_questionMark->setVisible(false);
        m_scoreObj->setBgColor(qApp->palette().base().color());
        if (m_scoreObj->singleNote()) {
            m_scoreObj->note(1)->setTechnical(NO_TECHNICALS);
            m_scoreObj->note(0)->markNoteHead(Qt::transparent);
            m_scoreObj->note(1)->markNoteHead(Qt::transparent);
        }
        showNoteNames(false);
    
    void TmainScoreObject::setKeySignatureEnabled(bool enableKey)
    {
        m_scoreObj->setKeySignatureEnabled(enableKey);
    
    void TmainScoreObject::setKeySignature(const TkeySignature &key)
    {
        m_scoreObj->setKeySignature(static_cast<int>(key.value()));
    
    char TmainScoreObject::keySignatureValue()
    {
        return static_cast<char>(m_scoreObj->keySignature());
    
    int TmainScoreObject::clefType() const
    {
        return static_cast<int>(m_scoreObj->clefType());
    
    void TmainScoreObject::setClef(int clefType)
    {
        m_scoreObj->setClefType(static_cast<Tclef::EclefType>(clefType));
    
    int TmainScoreObject::meter() const
    {
        return m_scoreObj->meterToInt();
    
    void TmainScoreObject::setMeter(int meterType)
    {
        m_scoreObj->setMeter(meterType);
    
    Tnote TmainScoreObject::getNote(int id)
    {
        return m_scoreObj->noteAt(id);
    
    int TmainScoreObject::setSelectedItem(int id)
    {
        auto n = m_scoreObj->note(id);
        int notesSpan = 0;
        m_scoreObj->setSelectedItem(n);
        if (n) {
            notesSpan = 1;
            if (n->note()->isRest()) {
                id++;
                while (id < m_scoreObj->notesCount() && m_scoreObj->noteList().at(id).isRest()) {
                    notesSpan++;
                    id++;
                }
            } else if (n->note()->rtm.tie() == Trhythm::e_tieStart) {
                id++;
                while (id < m_scoreObj->notesCount() && m_scoreObj->noteList().at(id).rtm.tie() && m_scoreObj->noteList().at(id).rtm.tie() != Trhythm::e_tieStart) {
                    notesSpan++;
                    id++;
                }
    
        return notesSpan;
    
    void TmainScoreObject::setTechnical(int noteId, quint32 tech)
    {
        m_scoreObj->setTechnical(noteId, tech);
    
    bool TmainScoreObject::selectInReadOnly() const
    {
        return m_scoreObj->selectInReadOnly();
    
    void TmainScoreObject::setSelectInReadOnly(bool sel)
    {
        m_scoreObj->setSelectInReadOnly(sel);
    
    quint32 TmainScoreObject::technical(int noteId)
    {
        if (noteId >= 0 && noteId < m_scoreObj->notesCount())
            return m_scoreObj->note(noteId)->technical();
        return 255;
    
    void TmainScoreObject::setMelody(Tmelody *mel)
    {
        m_scoreObj->setMelody(mel);
    
    void TmainScoreObject::getMelody(Tmelody *melody)
    {
        m_scoreObj->getMelody(melody);
    
    Taction *TmainScoreObject::clearScoreAct()
    {
        return m_scoreObj ? m_scoreObj->clearScoreAct() : nullptr;
    }
    
    Taction *TmainScoreObject::scoreMenuAct()
    {
        return NOO->scoreAct();
    
    void TmainScoreObject::askQuestion(Tmelody *mel, bool ignoreTechnical, const TkeySignature &melodyKey)
    {
        m_scoreObj->setBgColor(scoreBackgroundColor(GLOB->EquestionColor, 20));
        int transposition = 0;
        auto tempKey = mel->key();
        if (mel->key() != melodyKey) {
            transposition = mel->key().difference(melodyKey);
            mel->setKey(melodyKey);
        }
        m_scoreObj->setMelody(mel, ignoreTechnical, 0, transposition);
        mel->setKey(tempKey);
        m_scoreObj->setReadOnly(true);
        m_questionMark->setVisible(true);
    }
    
    /**
     * We are sure that this kind of questions occurs only in single note mode
     */
    
    void TmainScoreObject::askQuestion(const Tnote &note, quint32 technicalData)
    {
        m_scoreObj->setBgColor(scoreBackgroundColor(GLOB->EquestionColor, 20));
        m_scoreObj->setNote(m_scoreObj->note(1), note);
        m_questionMark->setVisible(true);
        m_scoreObj->note(1)->setTechnical(technicalData);
    
    void TmainScoreObject::askQuestion(const Tnote &note, const TkeySignature &key, quint32 technicalData)
    {
        setKeySignature(key);
        askQuestion(note, technicalData);
    
    void TmainScoreObject::prepareKeyToAnswer(const TkeySignature &fakeKey, const QString &expectKeyName)
    {
        setKeySignature(fakeKey);
        if (!m_questionKey) {
            auto p = qobject_cast<QQuickItem *>(parent()); // parent: MainScore.qml
            auto nameItem = qvariant_cast<QQuickItem *>(p->property("keyName"));
            m_scoreObj->component()->setData(
                "import QtQuick 2.9; Text { horizontalAlignment: Text.AlignHCenter;"
                "font.pixelSize: 12; transformOrigin: Item.TopLeft; scale: 0.17 }",
                QUrl());
            m_questionKey = qobject_cast<QQuickItem *>(m_scoreObj->component()->create());
            if (m_questionKey && nameItem) {
                m_questionKey->setParentItem(nameItem->parentItem());
                m_questionKey->setProperty("text", expectKeyName + QLatin1String("<br>?"));
                m_questionKey->setProperty("color", GLOB->EquestionColor);
                m_questionKey->setX(nameItem->x());
                m_questionKey->setY(nameItem->y());
            }
        }
    
    void TmainScoreObject::showNoteNames(bool showName)
    {
        if (m_scoreObj->singleNote()) {
            m_scoreObj->note(0)->setNoteNameVisible(showName);
            //       m_scoreObj->note(1)->setNoteNameVisible(showName);
        } else {
            m_scoreObj->setShowNoteNames(showName);
        }
    
    void TmainScoreObject::showNoteName(int noteNr, bool showName)
    {
        auto note = m_scoreObj->note(noteNr);
        if (note)
            note->setNoteNameVisible(showName);
    
    void TmainScoreObject::forceAccidental(int accid)
    {
        m_scoreObj->setCursorAlter(accid);
    }
    
    void TmainScoreObject::unLockScore()
    {
        m_scoreObj->setBgColor(scoreBackgroundColor(GLOB->EanswerColor, 20));
        setReadOnly(false);
    
    void TmainScoreObject::lockKeySignature(bool lock)
    {
        m_scoreObj->setKeyReadOnly(lock);
    }
    
    int TmainScoreObject::markNoteHead(const QColor &outColor, int noteNr)
    {
        int markedCount = 0;
        auto noteItem = m_scoreObj->note(noteNr);
        if (noteItem) {
            noteItem->markNoteHead(outColor);
            markedCount = 1;
            if (noteItem->note()->rtm.tie()) {
                noteNr++;
                while (noteNr < m_scoreObj->notesCount() && m_scoreObj->noteList()[noteNr].rtm.tie()
                       && m_scoreObj->noteList()[noteNr].rtm.tie() != Trhythm::e_tieStart) {
                    auto nextNote = m_scoreObj->note(noteNr);
                    if (nextNote) {
                        nextNote->markNoteHead(outColor);
                        noteNr++;
                        markedCount++;
                    } else
                        break;
                }
            } else if (noteItem->note()->isRest()) {
                noteNr++;
                while (noteNr < m_scoreObj->notesCount() && m_scoreObj->noteList()[noteNr].isRest()) {
                    auto nextNote = m_scoreObj->note(noteNr);
                    if (nextNote) {
                        nextNote->markNoteHead(outColor);
                        noteNr++;
                        markedCount++;
                    } else
                        break;
                }
    
        return markedCount;
    
    void TmainScoreObject::correctNote(const Tnote &goodNote, bool corrAccid)
    {
    
    SeeLook's avatar
    SeeLook committed
        if (corrAccid) {
    
            // FIXME probably it is not necessary - animation is universal for any kind of score mistake
    
    SeeLook's avatar
    SeeLook committed
        }
        m_correctNoteId = m_scoreObj->singleNote() ? 0 : (m_scoreObj->selectedItem() ? m_scoreObj->selectedItem()->index() : -1);
        auto noteItem = m_scoreObj->note(m_correctNoteId);
        if (noteItem) {
    
            if (!m_animationObj) {
                QQmlComponent comp(m_scoreObj->qmlEngine(), QUrl(QStringLiteral("qrc:/exam/CorrectNoteAnim.qml")));
                m_animationObj = qobject_cast<QObject *>(comp.create());
                m_animationObj->setParent(this);
                connect(m_animationObj, SIGNAL(finished()), this, SLOT(correctionFinishedSlot()));
                connect(m_animationObj, SIGNAL(applyCorrect()), this, SLOT(applyCorrectSlot()));
            }
            m_animationObj->setProperty("noteHead", QVariant::fromValue(m_scoreObj->noteHead(m_correctNoteId)));
            m_animationObj->setProperty("endY", noteItem->getHeadY(goodNote) - 15.0);
            m_animationObj->setProperty("running", true);
            *m_goodNote = goodNote;
    
    SeeLook's avatar
    SeeLook committed
        }
    
    void TmainScoreObject::correctKeySignature(const TkeySignature &keySign)
    {
        if (m_questionKey) {
            m_questionKey->setProperty("text", keySign.getName() + QLatin1String("<br>!"));
            m_questionKey->setProperty("color", GLOB->EanswerColor);
        }
        if (keySign != keySignatureValue())
            m_scoreObj->setKeySignature(keySign.value());
    
    void TmainScoreObject::saveMusicXml(const QString &fileName, const QString &title, const QString &composer)
    {
        QString fn = fileName;
        if (fileName.isEmpty()) // ask for file name if this parameter was empty
            fn = NOO->getXmlToSave(composer + QLatin1String(" - ") + title);
        if (!fn.isEmpty()) {
            m_scoreObj->saveMusicXml(fn, title, composer, GLOB->transposition());
            if (!title.isEmpty() && title != qTR("MelodyNameDialog", "Nootka melody")) {
                auto lt = GLOB->config->value(QLatin1String("Melody/recentTitles"), QStringList()).toStringList();
                lt.prepend(title);
                lt.removeDuplicates();
                while (lt.size() > 10)
                    lt.removeLast();
                GLOB->config->setValue(QLatin1String("Melody/recentTitles"), lt);
            }
            if (!composer.isEmpty() && composer != QLatin1String("Nootka The Composer")) {
                auto lc = GLOB->config->value(QLatin1String("Melody/recentComposers"), QStringList()).toStringList();
                lc.prepend(composer);
                lc.removeDuplicates();
                while (lc.size() > 10)
                    lc.removeLast();
                GLOB->config->setValue(QLatin1String("Melody/recentComposers"), lc);
            }
            m_title = title;
            m_composer = composer;
            emit titleChanged();
    
    QStringList TmainScoreObject::recentTitles() const
    {
        auto lt = GLOB->config->value(QLatin1String("Melody/recentTitles"), QStringList()).toStringList();
        lt.prepend(QGuiApplication::translate("MelodyNameDialog", "Nootka melody"));
        return lt;
    
    QStringList TmainScoreObject::recentComposers() const
    {
        auto lc = GLOB->config->value(QLatin1String("Melody/recentComposers"), QStringList()).toStringList();
        lc.prepend(QLatin1String("Nootka The Composer"));
        return lc;
    
    // #################################################################################################
    // ###################              PROTECTED           ############################################
    // #################################################################################################
    
    void TmainScoreObject::openXmlActSlot()
    {
        SOUND->stopListen();
        auto xmlFile = NOO->getXmlToOpen();
        openXmlFileSlot(xmlFile);
        SOUND->startListen();
    
    void TmainScoreObject::getMelodyNameSlot()
    {
        emit melodyNameDialog();
    
    void TmainScoreObject::randMelodySlot()
    {
        emit melodyGenerate();
    
    void TmainScoreObject::isExamChangedSlot()
    {
        m_scoreActions.clear();
        if (GLOB->isExam()) {
            m_scoreActions << m_zoomOutAct << m_zoomInAct;
            if (!m_questionMark) {
                m_scoreObj->component()->setData(
                    "import QtQuick 2.9; Text { anchors.centerIn: parent ? parent : undefined; scale: parent ? parent.height / height : 1; text: \"?\"; font { "
                    "family: \"Nootka\"; pixelSize: 20 }}",
                    QUrl());
                m_questionMark = qobject_cast<QQuickItem *>(m_scoreObj->component()->create());
                if (m_questionMark) {
                    m_questionMark->setParentItem(qvariant_cast<QQuickItem *>(qobject_cast<QQuickItem *>(m_scoreObj->parent())->property("bgRect")));
                    m_questionMark->setVisible(false);
    
                    if (m_questionMark)
                        m_questionMark->setProperty("color", scoreBackgroundColor(GLOB->EquestionColor, 40));
    
                }
            }
            singleModeSlot();
        } else {
            m_scoreActions << m_showNamesAct << m_extraAccidsAct << m_zoomOutAct << m_zoomInAct;
    
                delete m_questionMark;
                m_questionMark = nullptr;
    
            if (!GLOB->isSingleNote()) {
                m_openXmlAct->setEnabled(true);
                m_saveXmlAct->setEnabled(true);
                m_randMelodyAct->setEnabled(true);
            }
        }
        if (m_scoreObj) {
    #if !defined(Q_OS_ANDROID)
            m_scoreActions.prepend(m_reviewModeAct);
            m_scoreActions.prepend(m_scoreObj->editModeAct());
            m_scoreActions << m_scoreObj->insertNoteAct() << m_scoreObj->deleteNoteAct() << m_scoreObj->clearScoreAct() << m_transposeAct << m_notesMenuAct;
    
            if (!GLOB->isExam())
                m_scoreActions << m_randMelodyAct << m_openXmlAct << m_saveXmlAct;
    
        }
        emit scoreActionsChanged();
    
    void TmainScoreObject::singleModeSlot()
    {
        if (GLOB->isSingleNote()) {
            m_openXmlAct->setEnabled(false);
            m_saveXmlAct->setEnabled(false);
            m_randMelodyAct->setEnabled(false);
            if (GLOB->isExam()) {
                m_scoreObj->note(1)->setColor(qApp->palette().text().color());
                m_scoreObj->note(2)->setColor(qApp->palette().text().color());
            } else {
                m_scoreObj->note(1)->setColor(GLOB->getEnharmNoteColor());
                m_scoreObj->note(2)->setColor(GLOB->getEnharmNoteColor());
            }
            checkSingleNoteVisibility();
        } else {
            if (!GLOB->isExam()) {
                m_openXmlAct->setEnabled(true);
                m_saveXmlAct->setEnabled(true);
                m_randMelodyAct->setEnabled(true);
                if (!m_playAct->shortcut()) { // HACK: Create play action shortcut only when enabled, otherwise it locks next question shortcut
                    QQmlComponent actionsComp(NOO->qmlEngine(), this);
                    m_playAct->createQmlShortcut(&actionsComp, "\" \"; enabled: !GLOB.singleNoteMode && !GLOB.isExam");
                }
            }
        }
    
    void TmainScoreObject::applyCorrectSlot()
    {
        markNoteHead(GLOB->correctColor(), m_correctNoteId);
    
    void TmainScoreObject::correctionFinishedSlot()
    {
        m_scoreObj->setNote(m_correctNoteId, *m_goodNote);
        m_correctNoteId = -1;
        emit correctionFinished();
    
    void TmainScoreObject::playScoreSlot()
    {
        int countDownDur = 0;
        if (SOUND->tickDuringPlay()) {
            // play from selected note but tick from current bar beginning, so calculate initial duration
            if (m_scoreObj->selectedItem() && m_scoreObj->selectedItem()->index() > 0)
                countDownDur = m_scoreObj->selectedItem()->measure()->durationBefore(m_scoreObj->selectedItem());
        }
        SOUND->playNoteList(m_scoreObj->noteList(), m_scoreObj->selectedItem() ? m_scoreObj->selectedItem()->index() : 0, countDownDur);
    }
    
    void TmainScoreObject::gotItNoteSelectedSlot()
    {
        if (m_scoreObj->selectedItem()) {
            disconnect(m_scoreObj, &TscoreObject::selectedItemChanged, this, &TmainScoreObject::gotItNoteSelectedSlot);
            if (!GLOB->isExam())
                emit wantSelectGotIt();
        }
    
    void TmainScoreObject::openXmlFileSlot(const QString &xmlFile)
    {
        if (!xmlFile.isEmpty()) {
            auto melImport = new TimportScore(xmlFile);
            connect(melImport, &TimportScore::wantDialog, this, [=] {
                auto nootWin = qobject_cast<QQmlApplicationEngine *>(NOO->qmlEngine())->rootObjects().first();
                if (nootWin && QString(nootWin->metaObject()->className()).contains("MainWindow_QMLTYPE")) {
                    QMetaObject::invokeMethod(nootWin, "showDialog", Q_ARG(QVariant, TnootkaQML::ScoreImport));
                    connect(melImport, &TimportScore::importReady, this, &TmainScoreObject::melodyImportSlot);
                }
            });
            connect(melImport, &TimportScore::xmlWasRead, this, [=] {
                if (!IMPORT_SCORE->hasMoreParts()) {
                    NOO->scoreObj()->setMelody(IMPORT_SCORE->mainMelody(), !GLOB->instrument().bandoneon() && !GLOB->instrument().isGuitar());
                    // NOTE use here NOO->scoreObj() because m_scoreObj may be yet unset
                    SOUND->setMetronome(IMPORT_SCORE->mainMelody()->tempo(), IMPORT_SCORE->mainMelody()->beat());
                    m_title = IMPORT_SCORE->mainMelody()->title();
                    m_composer = IMPORT_SCORE->mainMelody()->composer();
                    emit titleChanged();
                    IMPORT_SCORE->deleteLater();
                }
            });
            melImport->runXmlThread();
        }
    }
    
    void TmainScoreObject::melodyImportSlot()
    {
        for (auto mi : IMPORT_SCORE->model()) {
            auto voicePart = qobject_cast<TmelodyPart *>(mi);
            if (voicePart && !voicePart->parts.isEmpty()) {
                for (auto snip : voicePart->parts) {
                    if (snip->selected()) {
                        NOO->scoreObj()->setMelody(snip->melody(), GLOB->instrument().type() != Tinstrument::Bandoneon && !GLOB->instrument().isGuitar());
                        // NOTE use here NOO->scoreObj() because m_scoreObj may be yet unset
                        SOUND->setMetronome(snip->melody()->tempo(), snip->melody()->beat());
                        m_title = snip->melody()->title();
                        m_composer = snip->melody()->composer();
                        emit titleChanged();
                        break;
                    }
                }
    
    QColor TmainScoreObject::scoreBackgroundColor(const QColor &c, int alpha)
    {
        return Tcolor::merge(NOO->alpha(c, alpha), qApp->palette().base().color());
    
    void TmainScoreObject::checkSingleNoteVisibility()
    {
        if (m_scoreObj && m_scoreObj->singleNote()) {
            m_scoreObj->note(1)->setVisible(GLOB->showEnharmNotes() || GLOB->isExam());
            m_scoreObj->note(2)->setVisible(!GLOB->isExam() && GLOB->showEnharmNotes() && GLOB->enableDoubleAccids());
            m_scoreObj->setNote(0, m_scoreObj->noteAt(0)); // refresh
        }
    
    void TmainScoreObject::checkExtraStaves()
    {
        if (m_scoreObj == nullptr || m_mainScoreItem == nullptr)
            return;
    
        auto staff = m_scoreObj->lastStaff();
        int emptyStavesCount = qMax(0, static_cast<int>((m_mainScoreItem->height() - (staff->y() + staff->height() * staff->scale())) / (staff->scale() * 16.0)));
        if (GLOB->isSingleNote())
            emptyStavesCount = 0;
        if (m_emptyStaves.count() != emptyStavesCount) {
            if (m_emptyStaves.count() > emptyStavesCount) { // remove some staff lines
                int toRemove = m_emptyStaves.count() - emptyStavesCount;
                for (int s = 0; s < toRemove; ++s)
                    delete m_emptyStaves.takeLast();
            } else { // add empty staff lines
                for (int s = m_emptyStaves.count(); s < emptyStavesCount; ++s) {
                    auto newEmpty = new TstaffLines(m_mainScoreItem);
                    m_emptyStaves << newEmpty;
                    newEmpty->setTransformOrigin(QQuickItem::Left);
                    newEmpty->setEnabled(false);
                }
    
        if (emptyStavesCount) {
            for (int s = 0; s < emptyStavesCount; ++s) {
                auto emptyStaff = m_emptyStaves[s];
                emptyStaff->setScale(staff->scale());
                emptyStaff->setX((m_scoreObj->clefType() == Tclef::PianoStaffClefs ? 3 : 0.5) * staff->scale());
                emptyStaff->setWidth(staff->width() - (m_scoreObj->clefType() == Tclef::PianoStaffClefs ? 3.5 : 1.0));
                emptyStaff->setStaffScale(staff->scale());
                emptyStaff->setY(staff->y() + (10.0 + staff->height() + s * 16.0) * staff->scale());
            }
    
    #if !defined(Q_OS_ANDROID)
    
    bool TmainScoreObject::eventFilter(QObject *obj, QEvent *event)
    {
        if (obj == qApp && event->type() == QEvent::ApplicationPaletteChange) {
            if (m_questionMark)
                m_questionMark->setProperty("color", scoreBackgroundColor(GLOB->EquestionColor, 40));
        }
        return QObject::eventFilter(obj, event);
    }
    
    #endif