Skip to content
Snippets Groups Projects
tnootkaqml.cpp 33.3 KiB
Newer Older
/***************************************************************************
SeeLook's avatar
SeeLook committed
 *   Copyright (C) 2017-2020 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 "instruments/tbandoneonbg.h"
#include "instruments/tguitarbg.h"
#include "instruments/tpianobg.h"
#include "instruments/tsaxbg.h"
#include "music/timportscore.h"
#include "music/ttuneobject.h"
#include "nootkaconfig.h"
#include "qtr.h"
#include "score/taddnoteitem.h"
#include "score/tdummychord.h"
#include "score/tmelodypreview.h"
#include "score/tnoteitem.h"
#include "tglobals.h"
#include "tmtr.h"
#include "tpath.h"
#if defined(Q_OS_ANDROID)
#include <Android/tandroid.h>
#include <QtCore/qdatetime.h>
#include <QtCore/qfileinfo.h>
#include <QtCore/qrandom.h>
#include <QtCore/qtimer.h>
#include <QtGui/qdesktopservices.h>
#include <QtGui/qpalette.h>
#include <QtQml/qqmlapplicationengine.h>
#include <QtWidgets/qapplication.h>
TnootkaQML *TnootkaQML::m_instance = nullptr;
TnootkaQML::TnootkaQML(QObject *parent)
    : QObject(parent)
    if (m_instance) {
        qDebug() << "TnootkaQML instance already exists!";
        return;
    }
    m_instance = this;

SeeLook's avatar
SeeLook committed
    qRegisterMetaType<Trhythm>();
    qRegisterMetaType<Tmeter>();
    qRegisterMetaType<Tclef>();
    qRegisterMetaType<Ttune>();
    qmlRegisterType<TscoreObject>("Score", 1, 0, "TscoreObject");
    qmlRegisterType<TstaffItem>("Score", 1, 0, "TstaffItem");
    qmlRegisterType<TnoteItem>("Score", 1, 0, "TnoteItem");
    qmlRegisterType<TstaffLines>("Score", 1, 0, "TstaffLines");
    qmlRegisterType<TaddNoteItem>("Score", 1, 0, "TaddNoteItem");
    qmlRegisterType<TmelodyPreview>("Score", 1, 0, "TmelodyPreview");
    qmlRegisterUncreatableType<TmelodyPart>("Score", 1, 0, "TmelodyPart", QStringLiteral("You cannot create an instance of the TcommonInstrument."));
    qmlRegisterType<TdummyChord>("Score", 1, 0, "TdummyChord");

    qmlRegisterUncreatableType<TcommonInstrument>("Nootka",
                                                  1,
                                                  0,
                                                  "TcommonInstrument",
                                                  QStringLiteral("You cannot create an instance of the TcommonInstrument."));
    qmlRegisterType<TguitarBg>("Nootka", 1, 0, "TguitarBg");
    qmlRegisterType<TpianoBg>("Nootka", 1, 0, "TpianoBg");
    qmlRegisterType<TbandoneonBg>("Nootka", 1, 0, "TbandoneonBg");
    qmlRegisterType<TsaxBg>("Nootka", 1, 0, "TsaxBg");
    qmlRegisterType<Taction>("Nootka", 1, 0, "Taction");

    qmlRegisterUncreatableType<TnootkaQML>("Nootka", 1, 0, "Nootka", QStringLiteral("You cannot create an instance of the TnootkaQML."));
    qmlRegisterType<TtuneObject>("Nootka", 1, 0, "TtuneObject");
// #################################################################################################
// ###################       INVOKABLE METHODS          ############################################
// #################################################################################################
QString TnootkaQML::version()
{
    if (qApp->arguments().last().contains(QLatin1String("--no-version")))
        return QString();
    else
        return NOOTKA_VERSION;
Tclef TnootkaQML::clef(int type)
{
    return Tclef(static_cast<Tclef::EclefType>(type));
Tmeter TnootkaQML::meter(int m)
{
    return Tmeter(static_cast<Tmeter::Emeter>(m));
Tnote TnootkaQML::note(int pitch, int octave, int alter, int rhythm, bool rest, bool dot)
{
    return Tnote(static_cast<char>(pitch),
                 static_cast<char>(octave),
                 static_cast<char>(alter),
                 Trhythm(static_cast<Trhythm::Erhythm>(rhythm), rest, dot, false));
Tnote TnootkaQML::note(const Tnote &n, int rhythm, bool rest, bool dot)
{
    return Tnote(n, Trhythm(static_cast<Trhythm::Erhythm>(rhythm), rest, dot, false));
Tnote TnootkaQML::note(const Tnote &n, const Trhythm &r)
{
    return Tnote(n, r);
Tnote TnootkaQML::note(int chroma, bool sharp)
{
    Tnote n(static_cast<short>(chroma));
    if (!sharp && (n.alter() != 0 || (n.alter() == 0 && n.note() != 3 && n.note() != 7))) // but skip e and b - otherwise they become fb or cb
        n = n.showWithFlat();
    return n;
Tnote TnootkaQML::setUpperStaff(Tnote n, bool onUpper)
{
    n.setOnUpperStaff(onUpper);
    return n;
Tnote TnootkaQML::emptyNote()
{
    return Tnote();
Tnote TnootkaQML::transpose(Tnote n, int semitones)
{
    n.transpose(semitones);
    return n;
Trhythm TnootkaQML::rhythm(int rtm, bool rest, bool dot, bool triplet)
{
    return Trhythm(static_cast<Trhythm::Erhythm>(rtm), rest, dot, triplet);
/**
 * It returns glyphs of 'Nootka' font in contrary to TnoteObject::getHeadText()
 */
QString TnootkaQML::rhythmText(const Trhythm &r)
{
    if (r.rhythm() == Trhythm::NoRhythm)
        return QStringLiteral("z"); // just black note-head
    QString out;
    if (r.isRest())
        out = QString(QChar(0xe106 + static_cast<int>(r.rhythm())));
    else
        out = QString(QChar(66 + static_cast<int>(r.rhythm())));
    if (r.hasDot())
        out += QStringLiteral(".");
    return out;
}

QString TnootkaQML::noteName(const Tnote &n, int style, bool showOctave)
{
    // Tnote::toText() method returns only names in user preferred style according to settings
    // To cheat it and force note name in any given style we are resetting pointer of is7th_B,
    // then Tnote skips filtering of a style during name generation.
    auto tmpPtr = TnameStyleFilter::is7th_B();
    TnameStyleFilter::setStyleFilter(nullptr, TnameStyleFilter::solfegeStyle());
    auto name = n.toText(static_cast<Tnote::EnameStyle>(style), showOctave);
    TnameStyleFilter::setStyleFilter(tmpPtr, TnameStyleFilter::solfegeStyle()); // restore is7th_B settings
    return name;
 * So far this method doesn't cheat @p TnameStyleFilter, so improper style for 7th note will be fixed
QString TnootkaQML::styledName(const Tnote &n, int style, bool showOctave)
{
    auto tmpStyle = Tnote::defaultStyle;
    Tnote::defaultStyle = static_cast<Tnote::EnameStyle>(style);
    auto name = n.styledName(showOctave);
    Tnote::defaultStyle = tmpStyle;
    return name;
QString TnootkaQML::majorKeyName(int key)
{
    return TkeySignature(static_cast<char>(key)).getMajorName();
QString TnootkaQML::minorKeyName(int key)
{
    return TkeySignature(static_cast<char>(key)).getMinorName();
QString TnootkaQML::majAndMinKeyName(int key)
{
    return majorKeyName(key) + QLatin1String("<br>") + minorKeyName(key);
QStringList TnootkaQML::keyComboModel()
{
    QStringList model;
    for (int i = -7; i < 8; i++) {
        TkeySignature k(i);
        model << QLatin1String("(") + k.accidNumber() + QLatin1String(") ") + k.getMajorName() + QLatin1String(" / ") + k.getMinorName();
    }
    return model;
/**
 * Returns difference in semitones between keys @p key1 and @p key2
 * expressed in values [-7 to 7] [7b to 7#]
 */
int TnootkaQML::keysDiff(int key1, int key2)
{
    TkeySignature k1(static_cast<char>(key1));
    return k1.difference(TkeySignature(static_cast<char>(key2)));
bool TnootkaQML::isAndroid()
{
#if defined(Q_OS_ANDROID)
    return true;
bool TnootkaQML::isWindows()
{
#if defined(Q_OS_WIN)
    return true;
bool TnootkaQML::isMac()
{
#if defined(Q_OS_MACOS)
    return true;
QStringList TnootkaQML::guitarTunings()
{
    QStringList tunList;
    int start = static_cast<int>(Ttune::Standard_EADGBE);
    for (int t = start; t < start + 5; ++t)
        tunList << Ttune::definedName(static_cast<Ttune::Etunings>(t));
    tunList << QApplication::translate("InstrumentPage", "Custom tuning");
    return tunList;
QStringList TnootkaQML::bassTunings()
{
    QStringList tunList;
    int start = static_cast<int>(Ttune::Bass4_EADG);
    for (int t = start; t < start + 4; ++t)
        tunList << Ttune::definedName(static_cast<Ttune::Etunings>(t));
    tunList << QApplication::translate("InstrumentPage", "Custom tuning");
    return tunList;
QStringList TnootkaQML::ukuleleTunings()
{
    QStringList tunList;
    tunList << Ttune::definedName(Ttune::Ukulele_GCEA) << Ttune::definedName(Ttune::Ukulele_Raised);
    tunList << qTR("InstrumentPage", "Custom tuning");
    return tunList;
}

/**
 * Tuning names text model - for instruments with tunings
 */
QStringList TnootkaQML::tuningModel(int instr)
{
    switch (static_cast<Tinstrument::Etype>(instr)) {
    case Tinstrument::ClassicalGuitar:
    case Tinstrument::ElectricGuitar:
        return guitarTunings();
    case Tinstrument::BassGuitar:
        return bassTunings();
    case Tinstrument::Ukulele:
        return ukuleleTunings();
    default:
        return QStringList();
    }
Ttune TnootkaQML::tuning(int tuningType)
{
    if (tuningType > -1) {
        if (tuningType == 0)
            return Ttune::stdTune;
        if (tuningType < 5)
            return Ttune::tunes[tuningType - 1];
        if (tuningType > 99 && tuningType < 104)
            return Ttune::bassTunes[tuningType - 100];
        if (tuningType == 110)
            return Ttune::ukuleleGCEA;
        if (tuningType == 111)
            return Ttune::ukuleleRaised;
    }
    return Ttune();
}

/**
 * When third note @s3 is valid the tuning represents real guitar tuning
 * otherwise it is an instrument scale
 */
Ttune TnootkaQML::tuning(const Tnote &s1, const Tnote &s2, const Tnote &s3, const Tnote &s4, const Tnote &s5, const Tnote &s6)
{
    return Ttune(QApplication::translate("InstrumentPage", "Custom tuning"), s1, s2, s3, s4, s5, s6, s3.isValid() ? Ttune::Custom : Ttune::Scale);
Ttune TnootkaQML::defaultScale(int instr)
{
    switch (static_cast<Tinstrument::Etype>(instr)) {
    case Tinstrument::ClassicalGuitar:
    case Tinstrument::ElectricGuitar:
    case Tinstrument::BassGuitar:
        return Ttune::bassTunes[0];
        return tuning(Tnote(-11), Tnote(49), Tnote(), Tnote(), Tnote(), Tnote());
    case Tinstrument::Bandoneon:
        return tuning(Tnote(-11), Tnote(48), Tnote(), Tnote(), Tnote(), Tnote());
    case Tinstrument::AltSax:
    case Tinstrument::TenorSax:
        return tuning(Tnote(11), Tnote(49), Tnote(), Tnote(), Tnote(), Tnote());
    case Tinstrument::Ukulele:
    default: // NoInstrument and any unexpected case
        return tuning(Tnote(10), Tnote(54), Tnote(), Tnote(), Tnote(), Tnote());
    }
Tinstrument TnootkaQML::instr(int type)
{
    return Tinstrument(static_cast<Tinstrument::Etype>(type < 0 || type > INSTR_COUNT - 1 ? 0 : type));
QString TnootkaQML::getXmlToOpen()
{
    QString openFile;
#if defined(Q_OS_ANDROID)
    if (GLOB->lastXmlDir().isEmpty())
        GLOB->setLastXmlDir(Tandroid::getExternalPath());
    openFile = TfileDialog::getOpenFileName(GLOB->lastXmlDir(), QStringLiteral("xml|musicxml|mxl"));
    openFile = TfileDialog::getOpenFileName(qApp->translate("TmainScoreObject", "Open melody file"),
                                            GLOB->lastXmlDir(),
                                            qApp->translate("TmainScoreObject", "MusicXML file")
                                                + QLatin1String(": *.xml, *.musicxml, *.mxl (*.xml *.musicxml *.mxl);;") + QLatin1String(" *.xml (*.xml);;")
                                                + QLatin1String(" *.musicxml (*.musicxml);;") + qApp->translate("TmainScoreObject", "Compressed MusicXML file")
                                                + QLatin1String(" *.mxl  (*.mxl);;"));
    if (!openFile.isEmpty())
        GLOB->setLastXmlDir(QFileInfo(openFile).absoluteDir().path());
    return openFile;
QString TnootkaQML::getXmlToSave(const QString &fileName)
{
    QString saveFile;
    QString filter;
#if defined(Q_OS_ANDROID)
    if (GLOB->lastXmlDir().isEmpty())
        GLOB->setLastXmlDir(Tandroid::getExternalPath());
    saveFile = TfileDialog::getSaveFileName(GLOB->lastXmlDir() + QLatin1String("/") + fileName, QStringLiteral("musicxml|mxl"));
    saveFile = TfileDialog::getSaveFileName(qApp->translate("TmainScoreObject", "Save melody as:"),
                                            GLOB->lastXmlDir() + QDir::separator() + fileName,
                                            qApp->translate("TmainScoreObject", "Compressed MusicXML file") + QLatin1String(" *.mxl (*.mxl);;")
                                                + qTR("TmainScoreObject", "MusicXML file") + QLatin1String(" (*.musicxml *.xml)"),
EDJLM's avatar
EDJLM committed
                                            &filter);
    if (!saveFile.isEmpty()) {
        GLOB->setLastXmlDir(QFileInfo(saveFile).absoluteDir().path());
        // if the dialog does not retrieve an extension for the file we deduct it from the filter
        if (QFileInfo(saveFile).suffix().isEmpty()) {
            if (filter.endsWith(QLatin1String("(*.mxl)")))
                saveFile += QLatin1String(".mxl");
            else
                saveFile += QLatin1String(".musicxml");
        }
SeeLook's avatar
SeeLook committed

QString TnootkaQML::pix(const QString &imageFileName)
{
    return Tpath::pix(imageFileName);
QString TnootkaQML::pix(const char *imageName, int height)
{
    return pixToHtml(QString(imageName), height);
QString TnootkaQML::TR(const QString &context, const QString &text, const QString &disambiguation, int n)
{
    return qTR(qPrintable(context), qPrintable(text), qPrintable(disambiguation), n);
QString TnootkaQML::getOnlineDoc(const QString &post)
{
    return QString("<p align=\"right\"><a href=\"https://nootka.sourceforge.io/index.php/%1/\">").arg(post)
        + QGuiApplication::translate("ThelpDialogBase", "Open online documentation") + QLatin1String("</a> </p>");
QColor TnootkaQML::alpha(const QColor &c, int a)
{
    return Tcolor::alpha(c, a);
QColor TnootkaQML::randomColor(int alpha, int level)
{
    return QColor(QRandomGenerator::global()->bounded(level), QRandomGenerator::global()->bounded(level), QRandomGenerator::global()->bounded(level), alpha);
QColor TnootkaQML::invert(const QColor &c)
{
    return Tcolor::invert(c);
qreal TnootkaQML::hue(const QColor &c) const
{
    return c.hueF();
qreal TnootkaQML::saturation(const QColor &c) const
{
    return c.saturationF();
qreal TnootkaQML::lightness(const QColor &c) const
{
    return c.saturationF();
int TnootkaQML::factor()
{
#if defined(Q_OS_ANDROID)
    // Set Android font according to screen size/density
    return qRound(Tmtr::fingerPixels() * 0.35 * GLOB->guiScale());
    // but use system font size on desktops
    return qRound(Tmtr::systemFont.pointSize() / 72.0 * qApp->primaryScreen()->logicalDotsPerInch() * GLOB->guiScale());
QString TnootkaQML::fontFamily()
{
    return Tmtr::systemFont.family();
int TnootkaQML::fingerPixels()
{
    return Tmtr::fingerPixels();
}
int TnootkaQML::shortScreenSide()
{
    return Tmtr::shortScreenSide();
}
int TnootkaQML::longScreenSide()
{
    return Tmtr::longScreenSide();
}
QString TnootkaQML::pixToHtml(const QString &pixName, int height)
{
    if (height == 0)
        return QString("<img src=\"%1\">").arg(pixName);
    QPixmap pix;
    if (!pix.load(Tpath::img(qPrintable(pixName))))
        return QString();
    QByteArray byteArray;
    QBuffer buffer(&byteArray);
    pix.scaled(qRound(height * (static_cast<qreal>(pix.width()) / static_cast<qreal>(pix.height()))), height, Qt::KeepAspectRatio, Qt::SmoothTransformation)
        .save(&buffer, "PNG");
    return QString("<img src=\"data:image/png;base64,") + byteArray.toBase64() + "\"/>";
void TnootkaQML::openDocLink(const QString &lnk)
{
    QDesktopServices::openUrl(QUrl(QLatin1String("https://nootka.sourceforge.io/index.php/") + lnk));
qreal TnootkaQML::bound(qreal min, qreal val, qreal max)
{
    return qBound(min, val, max);
void TnootkaQML::setQmlEngine(QQmlEngine *e)
{
    m_qmlEngine = e;
    if (GLOB->isFirstRun) // Wizard - actions are not needed yet
        return;

    connect(GLOB, &Tglobals::newerVersion, this, &TnootkaQML::warnNewerVersionSlot);

    if (m_scoreAct) {
        delete m_scoreAct;
        delete m_settingsAct;
        delete m_levelAct;
        delete m_chartsAct;
        delete m_melodyAct;
        delete m_examAct;
        delete m_aboutAct;
    }
    m_settingsAct = new Taction(QApplication::translate("TtoolBar", "Settings"), QStringLiteral("systemsettings"), this);
    connect(m_settingsAct, &Taction::triggered, this, &TnootkaQML::settingsActTriggered);
    m_settingsAct->setTip(QApplication::translate("TtoolBar", "Application preferences"), QQuickItem::TopRight);
    m_levelAct = new Taction(QApplication::translate("TtoolBar", "Level"), QStringLiteral("levelCreator"), this);
    connect(m_levelAct, &Taction::triggered, this, &TnootkaQML::levelActTriggered);
    m_levelAct->setTip(QApplication::translate("TtoolBar", "Level creator"), QQuickItem::TopRight);
    m_chartsAct = new Taction(QApplication::translate("TtoolBar", "Analyze"), QStringLiteral("charts"), this);
    connect(m_chartsAct, &Taction::triggered, this, &TnootkaQML::chartsActTriggered);
    m_chartsAct->setTip(tr("Analysis of exam results"), QQuickItem::TopRight);
    m_scoreAct = new Taction(QApplication::translate("TmainScoreObject",
                                                     "Score",
                                                     "it could be 'notation', 'staff' or whatever is associated with that 'place to display musical notes' and "
                                                     "this the name is quite short and looks well."),
                             QStringLiteral("score"),
                             this);
    m_scoreAct->setBgColor(qApp->palette().highlight().color());
    connect(m_scoreAct, &Taction::triggered, this, &TnootkaQML::scoreActTriggered);
    m_scoreAct->setTip(QApplication::translate("TmainScoreObject", "Manage and navigate the score."), QQuickItem::TopRight);
    m_melodyAct = new Taction(QApplication::translate("TmainScoreObject", "Melody"), QStringLiteral("melody"), this);
    connect(m_melodyAct, &Taction::triggered, this, &TnootkaQML::melodyActTriggered);
    m_melodyAct->setTip(QApplication::translate("TmainScoreObject", "Open, save, generate and play a melody."), QQuickItem::TopRight);
    m_examAct = new Taction(QApplication::translate("TtoolBar", "Lessons"), QStringLiteral("startExam"), this);
    connect(m_examAct, &Taction::triggered, this, &TnootkaQML::examActTriggered);
    m_examAct->setTip(QApplication::translate("TtoolBar", "Start exercises or an exam"), QQuickItem::TopRight);
    m_aboutAct = new Taction(this);
    connect(m_aboutAct, &Taction::triggered, this, &TnootkaQML::aboutActTriggered);
    m_messageColor = qApp->palette().highlight().color();

    qApp->installEventFilter(this);
/**
 * Opening files from command line argument starts here, but it is a bit clumsy:
 * - for Music XML we are sending @p wantOpenFile() and main score will handle that,
 * - but for exam and level files only @p wantOpenFile() signal is emitted
 *   with 700ms delay to give main window time for initialize
 * - then @p MainWindow.qml handles it and creates @p DialogLoader.qml
 * - 'dialog loader' invokes @p TdialogLoaderObject::openFile() of its object
 * - and there the file is distinguished (exam or level) and appropriate signal is emitted
 * - again, 'dialog loader' handles those signals and creates 'exam executor' or 'level creator' apparently
 */
void TnootkaQML::openFile(const QString &runArg)
{
    if (GLOB->isExam()) {
        qDebug() << "--- Exam or exercise is running. File cannot be opened! ---";
        return;
    }
    if (QFile::exists(runArg)) {
        QFile file(runArg);
        auto ext = QFileInfo(file).suffix();
        if (ext == QLatin1String("xml") || ext == QLatin1String("musicxml") || ext == QLatin1String("mxl")) {
            auto fullPath = QDir(file.fileName()).absolutePath();
            emit wantOpenXml(fullPath);
        } else {
            QTimer::singleShot(700, this, [=] {
                emit GLOB->wantOpenFile(runArg);
            });
        }
void TnootkaQML::setMessageColor(const QColor &mc)
{
    if (m_messageColor != mc) {
        m_messageColor = mc;
        emit messageColorChanged();
    }
void TnootkaQML::setStatusTip(const QString &statusText, int tipPos, bool richText)
{
    if ((GLOB->showHints() && (!m_messageTimer || (m_messageTimer && !m_messageTimer->isActive()))))
        emit statusTip(statusText, tipPos, richText);
void TnootkaQML::showTimeMessage(const QString &message, int time, int pos, bool richText)
{
    if (!m_messageTimer) {
        m_messageTimer = new QTimer(this);
        m_messageTimer->setSingleShot(true);
        connect(m_messageTimer, &QTimer::timeout, this, [=] {
            emit statusTip(QString(), m_messagePos, false);
            QTimer::singleShot(300, this, [=] {
                setMessageColor(qApp->palette().highlight().color());
            }); // restore default status background color
        });
    }
    m_messagePos = pos;
    if (m_messageTimer->isActive())
        m_messageTimer->stop();
    emit statusTip(message, pos, richText);
    m_messageTimer->start(time);
bool TnootkaQML::messageTimerActive() const
{
    return m_messageTimer ? m_messageTimer->isActive() : false;
QString TnootkaQML::qaTypeText(int qaType)
{
    switch (qaType) {
    case 0:
        return QApplication::translate("Texam", "as note on the staff");
    case 1:
        return QApplication::translate("Texam", "as note name");
    case 2:
        return QApplication::translate("Texam", "on instrument");
    case 3:
        return QApplication::translate("Texam", "as played sound");
    default:
        return QString();
    }
QString TnootkaQML::note7translated() const
{
    return QApplication::translate("Notation",
                                   "b",
                                   "Give here a name of 7-th note preferred in your country. But only 'b' or 'h' not 'si' or something worst...");
QString TnootkaQML::keyNameTranslated() const
{
    return QApplication::translate(
        "Notation",
        "letters",
        "DO NOT TRANSLATE IT DIRECTLY. Put here 'letters' or 'solfege' This is country preferred style of naming key signatures. 'letters' means "
        "C-major/a-minor names ('major' & 'minor' also are translated by you), 'solfege' means Do-major/La-minor names");
/**
 * Call method of @p MainWindow.qml to obtain creted popup reference
 * then set name of this unsupported file
 */
void TnootkaQML::warnNewerVersionSlot(const QString &fileName)
{
    QTimer::singleShot(400, this, [=] {
        if (m_warnNewerPopup) { // when popup already exists just append file name to previous name(s)
            m_warnNewerPopup->setProperty("fName", m_warnNewerPopup->property("fName").toString() + QLatin1String("<br>") + fileName);
        } else {
            auto nootWin = qobject_cast<QQmlApplicationEngine *>(m_qmlEngine)->rootObjects().first();
            if (nootWin && QString(nootWin->metaObject()->className()).contains("MainWindow_QMLTYPE")) {
                QVariant popVar;
                QMetaObject::invokeMethod(nootWin, "newerVerPop", Q_RETURN_ARG(QVariant, popVar));
                m_warnNewerPopup = qvariant_cast<QObject *>(popVar);
                if (m_warnNewerPopup) {
                    connect(m_warnNewerPopup, &QQuickItem::destroyed, this, [=] {
                        m_warnNewerPopup = nullptr;
                    });
                    m_warnNewerPopup->setProperty("fName", fileName);
                }
            }
// #################################################################################################
// ###################     CONNECTIONS  NODE            ############################################
// #################################################################################################
void TnootkaQML::noteStarted(const Tnote &n)
{
    Tnote note = n;
    if (m_scoreObject->keySignature() < 0 || (m_scoreObject->keySignature() == 0 && GLOB->GpreferFlats))
        note = note.showWithFlat();
    m_ignoreScore = true;
    if (m_scoreObject->singleNote()) {
        if (!note.isRest()) {
            note.setRhythm(Trhythm::NoRhythm);
            m_scoreObject->setNote(0, note);
        }
    } else {
        if (!GLOB->showNotesDiff()) {
            if (m_scoreObject->selectedItem() == nullptr) {
                m_scoreObject->addNote(note, true);
                m_startedNoteId = -1;
            } else {
                if (!note.isRest()) {
                    auto r = m_scoreObject->selectedItem()->note()->rtm;
                    r.setRest(false);
                    note.setRhythm(r);
                    m_scoreObject->setNote(m_scoreObject->selectedItem(), note);
                }
                m_startedNoteId = selectedNoteId();
            }
        }
    }
    m_ignoreScore = false; // Reset the switch in case it is not consumed
}
SeeLook's avatar
SeeLook committed

void TnootkaQML::noteFinished(const Tnote &n)
{
    Tnote note = n;
    if (m_instrument)
        m_instrument->setNote(note);
    if (m_scoreObject->keySignature() < 0 || (m_scoreObject->keySignature() == 0 && GLOB->GpreferFlats))
        note = note.showWithFlat();
    m_ignoreScore = true;
    if (m_scoreObject->singleNote()) {
        note.setRhythm(Trhythm::NoRhythm);
        m_scoreObject->setNote(0, note);
    } else {
        if (!GLOB->showNotesDiff()) {
            if (m_scoreObject->selectedItem() == nullptr || m_startedNoteId == -1) {
                m_scoreObject->setNote(m_scoreObject->lastNote(), note);
                m_scoreObject->setSelectedItem(nullptr);
            } else if (m_scoreObject->selectedItem() && !note.isRest()) {
                auto r = m_scoreObject->selectedItem()->note()->rtm;
                r.setRest(false);
                note.setRhythm(r);
                m_scoreObject->setNote(m_scoreObject->selectedItem(), note);
    }
    m_ignoreScore = false; // Reset the switch in case it is not consumed
    // TODO Do treat tied notes as a single one?
}

void TnootkaQML::setMainScore(QQuickItem *ms)
{
    if (!m_mainScore) {
        m_mainScore = ms;
        m_scoreObject = qobject_cast<TscoreObject *>(qvariant_cast<QObject *>(m_mainScore->property("scoreObj")));
        connect(m_scoreObject, &TscoreObject::selectedNoteChanged, this, &TnootkaQML::scoreChangedNoteSlot);
        connect(GLOB, &Tglobals::isExamChanged, this, &TnootkaQML::examStartStop);
        if (m_scoreObject && !m_nodeConnected)
            connectInstrument();
    }
}

void TnootkaQML::setInstrument(TcommonInstrument *ci)
{
    if (m_instrument != ci) {
        if (m_instrument != nullptr)
            m_nodeConnected = false; // reset connection of instrument signal when instrument type changed
        m_instrument = ci;
        if (m_scoreObject && !m_nodeConnected && !GLOB->isExam())
            connectInstrument();
    }
}

void TnootkaQML::connectInstrument()
{
    if (m_instrument && !m_nodeConnected) {
        connect(m_instrument, &TcommonInstrument::noteChanged, this, &TnootkaQML::instrumentChangesNoteSlot);
        m_nodeConnected = true;
    }
}

void TnootkaQML::instrumentChangesNoteSlot()
{
    m_ignoreScore = true;
    Tnote noteToPlay = m_instrument->note();
    noteToPlay.transpose(GLOB->transposition());
    emit playNote(noteToPlay);
    Tnote instrNote = m_instrument->note();
    if (m_scoreObject->keySignature() < 0
        || (m_scoreObject->keySignature() == 0 && GLOB->GpreferFlats && !(instrNote.alter() == 0 && (instrNote.note() == 3 || instrNote.note() == 7))))
        instrNote = instrNote.showWithFlat();

    if (m_scoreObject->singleNote()) {
        m_scoreObject->setNote(0, instrNote);
        if (GLOB->instrument().bandoneon())
            m_scoreObject->setTechnical(0, m_instrument->technical());
    } else {
        if (m_scoreObject->selectedItem() == nullptr) {
            auto r = m_scoreObject->workRhythm();
            r.setRest(false);
            auto instNoteNr = instrNote.chromatic();
            // check is note inside score boundaries
            if (instNoteNr < m_scoreObject->lowestNote().chromatic() || instNoteNr > m_scoreObject->highestNote().chromatic()) {
                r.setRest(true);
                instrNote.setNote(0); // invalidate not - it became rest
            }
            instrNote.setRhythm(r);
            m_scoreObject->addNote(instrNote, true);
        } else {
            auto r = m_scoreObject->selectedItem()->note()->rtm;
            r.setRest(false);
            instrNote.setRhythm(r);
            m_scoreObject->setNote(m_scoreObject->selectedItem(), instrNote);
        if (GLOB->instrument().bandoneon()) {
            auto seg = m_scoreObject->selectedItem() ? m_scoreObject->noteSegment(m_scoreObject->selectedItem()->index()) : m_scoreObject->lastSegment();
            Ttechnical instrData(m_instrument->technical());
            if (seg->index() > 0) {
                for (int i = seg->index() - 1; i >= 0; --i) {
                    auto searchNoteData = m_scoreObject->noteSegment(i)->techicalData();
                    if (searchNoteData.bowing()) { // Show bowing but only when it changes comparing to the previously  set bow direction
                        if (searchNoteData.bowing() == instrData.bowing()) // if it is the same - just reset bowing on note data from the instrument
                            instrData.setBowing(Ttechnical::BowUndefined);
                        break;
                    }
                }
            seg->setTechnical(instrData.data());
    }
    m_ignoreScore = false; // Reset the switch in case it is not consumed
void TnootkaQML::examStartStop()
{
    if (GLOB->isExam()) {
        disconnect(m_instrument, &TcommonInstrument::noteChanged, this, &TnootkaQML::instrumentChangesNoteSlot);
        disconnect(m_scoreObject, &TscoreObject::selectedNoteChanged, this, &TnootkaQML::scoreChangedNoteSlot);
    } else {
        m_nodeConnected = false;
        connectInstrument();
        connect(m_scoreObject, &TscoreObject::selectedNoteChanged, this, &TnootkaQML::scoreChangedNoteSlot);
    }
void TnootkaQML::scoreChangedNoteSlot()
{
    if (m_ignoreScore) {
        m_ignoreScore = false;
        return;
    }
    auto scoreNote = m_scoreObject->selectedNote();
    if (m_instrument)
        m_instrument->setNote(scoreNote, getTechicalFromScore());
    if (scoreNote.isValid())
        scoreNote.transpose(GLOB->transposition());
    emit playNote(scoreNote);
int TnootkaQML::selectedNoteId() const
{
    return m_scoreObject->selectedItem() ? m_scoreObject->selectedItem()->index() : -1;
int TnootkaQML::getTechicalFromScore()
{
    quint32 technical = NO_TECHNICALS; // empty by default
    if (GLOB->instrument().bandoneon() && m_scoreObject->selectedItem()) {
        auto selectedSegment = m_scoreObject->noteSegment(m_scoreObject->selectedItem()->index());
        Ttechnical dataToSet = selectedSegment->technical();
        if (!dataToSet.bowing()) { // no bowing, so look up for any previous note with bowing mark
            for (int i = selectedSegment->index(); i >= 0; --i) {
                auto searchNoteData = m_scoreObject->noteSegment(i)->techicalData();
                if (searchNoteData.bowing()) {
                    dataToSet.setBowing(searchNoteData.bowing());
                    break;
                }
            }
        technical = dataToSet.data();
void TnootkaQML::selectPlayingNote(int id)
{
    m_ignoreScore = true;
    m_scoreObject->setSelectedItem(m_scoreObject->note(id));
    if (m_instrument)
        m_instrument->setNote(m_scoreObject->selectedNote(), getTechicalFromScore());
    m_ignoreScore = false; // Reset the switch in case it is not consumed
int TnootkaQML::scoreNotesCount() const
{
    return m_scoreObject->notesCount();
QList<Tnote> &TnootkaQML::scoreNoteList() const
{
    return m_scoreObject->noteList();

bool TnootkaQML::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == qApp && event->type() == QEvent::ApplicationPaletteChange) {
        setMessageColor(qApp->palette().highlight().color());
        m_scoreAct->setBgColor(qApp->palette().highlight().color());
    }
    return QObject::eventFilter(obj, event);
}