Skip to content
Snippets Groups Projects
tbeamobject.cpp 12.6 KiB
Newer Older
/***************************************************************************
 *   Copyright (C) 2016-2018 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 "tbeamobject.h"
#include "tmeasureobject.h"
#include "tscoreobject.h"
#include "tstaffitem.h"
#include <music/tnote.h>

#include <QtGui/qguiapplication.h>
#include <QtGui/qpainter.h>
#include <QtGui/qpalette.h>
#include <qmath.h>

#include <QtCore/qdebug.h>

/**
 * Simple structure to describe second beam line (sixteenth)
 * which can be chunked when there are rests or eights in the group
 */
    T16beam(int firstStemNr = 0)
        : startStem(firstStemNr)
    {
    }
    int startStem = -1; /**< Undefined by default (-1) */
    int endStem = -1; /**< When remains undefined (-1) - beam is partial */
    /** @p TRUE when beam is not connected to another stem */
    bool isHalf() { return endStem == -1; }
};

#define MIN_STEM_HEIGHT (4.0) // minimal stem height (distance of note head to staff boundary)
#define BEAM_THICK (0.8) // thickness of a beam
#define HALF_THICK (0.4) // half of beam thickness

TbeamObject::TbeamObject(TnotePair *sn, TmeasureObject *m)
    : QQuickPaintedItem(m->staff())
    , m_measure(m)
    setAcceptHoverEvents(true);
    setRenderTarget(QQuickPaintedItem::FramebufferObject);
    setAntialiasing(true);
    addNote(sn);
    setParent(m_measure->score());
    connect(this, &QQuickPaintedItem::visibleChanged, this, [=] {
        if (isVisible() && count() > 1)
            drawBeam();
    });
    qApp->installEventFilter(this);
    //   qDebug() << "     [BEAM] deleted of id" << (m_notes.isEmpty() ? -1 : first()->index());
    for (TnotePair *np : std::as_const(m_notes)) {
        np->addChange(TnotePair::e_beamChanged);
        np->setBeam(nullptr);
        //     resetBeam(np);
    }
void TbeamObject::addNote(TnotePair *np)
{
    if (np == nullptr)
        return;

    if (np->beam() == nullptr)
        np->setBeam(this);
    else
        qDebug() << "     [BEAM] note" << np->index() << "has already a beam";

    if (m_notes.count() > 1) {
        m_notes.last()->note()->rtm.setBeam(Trhythm::e_beamCont); // no need to be updated
    if (m_notes.isEmpty())
        np->note()->rtm.setBeam(Trhythm::e_beamStart);
    else
        np->note()->rtm.setBeam(Trhythm::e_beamEnd);
    if (np->item()) // mark it changed only when object exists, otherwise its note will be updated during creation
        np->addChange(TnotePair::e_beamChanged);
    m_notes << np;

    if (np->note()->rhythm() == Trhythm::Sixteenth) {
        const Tnote *beforeLastNote = m_notes.count() > 1 ? m_notes[m_notes.count() - 2]->note() : nullptr;
        if (m_16beams.isEmpty() || (beforeLastNote && beforeLastNote->rhythm() != Trhythm::Sixteenth)) {
            // is first in beam or previous note was not a sixteenth
            m_16beams << T16beam(m_notes.count() - 1); // then create new beam segment
        } else if (!m_16beams.isEmpty() && (beforeLastNote && beforeLastNote->rhythm() == Trhythm::Sixteenth)) {
            // there is 16 beam and previous note and this notes are 16th
            m_16beams.last().endStem = m_notes.count() - 1; // then set end stem for it
        }
    }
}

/**
 * With @p stemDirStrength (strength of stem direction) we trying to determine preferred common direction of stems in the group.
 * More far from middle of the staff (18) note is placed - stronger its direction has to be preserved
 * to keep beaming more natural and looking good
 *
 * @p stemsUpPossible keep true when there is enough space between note head and staff border for whole stem
 * if not stems go down.
 * @p stemTop it top stem position of all notes. It is set to middle of the staff if necessary (to look well)
 */
void TbeamObject::prepareBeam()
{
    int stemDirStrength = 0;
    bool stemsUpPossible = true;
    qreal hiNote = 99.0, loNote = 0.0;
    for (TnotePair *np : std::as_const(m_notes)) {
        stemDirStrength += np->item()->notePosY()
            - (m_measure->staff()->upperLine()
               + (m_measure->score()->isPianoStaff() && np->item()->notePosY() > m_measure->staff()->upperLine() + 13.0 ? 26.0 : 4.0));
        if (np->item()->notePosY() < MIN_STEM_HEIGHT)
            stemsUpPossible = false;
        hiNote = qMin(hiNote, np->item()->notePosY());
        loNote = qMax(loNote, np->item()->notePosY());
    }
    bool allStemsDown = !stemsUpPossible;
    qreal minStemHeight = MIN_STEM_HEIGHT + (m_16beams.empty() ? 0.0 : 1.0);
    if (stemDirStrength < 0)
        allStemsDown = true; // stems down are always possible
    qreal stemTop = allStemsDown ? loNote + minStemHeight : hiNote - minStemHeight;
    if (m_measure->score()->isPianoStaff() && !first()->note()->onUpperStaff()) { // when note lays on the lower staff
        qreal lowerMidLine = m_measure->staff()->upperLine() + 26.0;
        if ((allStemsDown && stemTop < lowerMidLine) || (!allStemsDown && stemTop > lowerMidLine))
            stemTop = lowerMidLine; // keep beam on the lower staff middle line
    } else {
        qreal upperMidLine = m_measure->staff()->upperLine() + 4.0;
        if ((allStemsDown && stemTop < upperMidLine) || (!allStemsDown && stemTop > upperMidLine))
            stemTop = upperMidLine; // keep beam on staff middle line
    }
    for (TnotePair *np : std::as_const(m_notes)) {
        np->note()->rtm.setStemDown(allStemsDown);
        np->addChange(TnotePair::e_stemDirChanged);
        np->item()->setStemHeight(qAbs(np->item()->notePosY() - stemTop));
        np->approve();
    }
    update();
 * Polygons are painted, single for 8ths and possible a few for 16ths.
 * Painter canvas orientation depends on stems direction (up or down),
 * for stems-up, top beam is 8ths and bottom are 16ths in contrary to stems-down
void TbeamObject::paint(QPainter *painter)
{
    if (count() > 1) {
        qreal s = first()->note()->rtm.stemDown() ? -1.0 : 1.0; // scale
        qreal t = first()->note()->rtm.stemDown() ? height() : 0.0; // translation
        painter->setPen(Qt::NoPen);
        painter->setBrush(qApp->palette().text().color());
        QPolygonF topBeam;
        qreal endX = last()->item()->stemTop().x() - x() + 0.3;
        topBeam << QPointF(0.0, t) << QPointF(0.0, t + s * BEAM_THICK) << QPointF(endX, t + s * BEAM_THICK) << QPointF(endX, t) << QPointF(0.0, t);
        painter->drawPolygon(topBeam);
        for (int b = 0; b < m_16beams.count(); ++b) {
            T16beam &b16 = m_16beams[b];
            qreal startX = m_notes[b16.startStem]->item()->stemTop().x() - x();
            // 16th beam of fist stem is right-sided (2.0) others are left-sided (-2.0)
            endX = (b16.isHalf() ? startX + BEAM_THICK * (b16.startStem == 0 ? 2.0 : -2.0) : m_notes[b16.endStem]->item()->stemTop().x() - x()) + 0.3;
            QPolygonF polyB;
            polyB << QPointF(startX, t + s * 1.5 * BEAM_THICK) << QPointF(startX, t + s * 2.5 * BEAM_THICK) << QPointF(endX, t + s * 2.5 * BEAM_THICK)
                  << QPointF(endX, t + s * 1.5 * BEAM_THICK) << QPointF(startX, t + s * 1.5 * BEAM_THICK);
            painter->drawPolygon(polyB);
        }
// #################################################################################################
// ###################              PROTECTED           ############################################
// #################################################################################################

/**
 * According to first and last notes in the beam, this method sets item geometry.
 * @p setTextureSize() is called to make texture big enough and avoid pixelization
 */
void TbeamObject::drawBeam()
{
    if (m_notes.isEmpty())
        return;

    auto p1 = first()->item()->stemTop();
    auto p2 = last()->item()->stemTop();
    setWidth(qAbs(p2.x() - p1.x()) + 1.0);
    setHeight(qAbs(p1.y() - p2.y()) + BEAM_THICK * 2.5);
    setX(p1.x());
    setY(qMin(p1.y(), p2.y()) - (first()->note()->rtm.stemDown() ? 2.0 * BEAM_THICK : HALF_THICK));
    setTextureSize(QSize(qCeil(width() * m_measure->staff()->scale()), qCeil(height() * m_measure->staff()->scale())));
    /**
     * @p update() should be called here to invoke @p paint()
     * but it is invoked by Qt itself whenever width, height, x or y change
     */
void TbeamObject::changeStaff(TstaffItem *st)
{
    setParentItem(st);
bool TbeamObject::removeNote(TnotePair *np)
{
    bool deleteBeam = false;
    int noteId = m_notes.indexOf(np);
    if (noteId == -1) {
        qDebug() << "     [BEAM] of note id" << first()->index() << "has no note to remove";
        return false;
    }
    if (noteId > 1) { // there are at least two notes at the beam beginning
        if (count() - noteId > 2) { // split beam
            resetBeam(m_notes.takeAt(noteId));
            int tempCount = count();
            TbeamObject *otherBeam = nullptr;
            for (int n = noteId; n < tempCount; ++n) {
                auto noteForOtherBeam = m_notes.takeAt(noteId);
                resetBeam(noteForOtherBeam);
                if (otherBeam)
                    otherBeam->addNote(noteForOtherBeam);
                    otherBeam = m_measure->score()->getBeam(noteForOtherBeam, m_measure); // new TbeamObject(noteForOtherBeam, m_measure);
            otherBeam->prepareBeam();
            otherBeam->drawBeam();
        } else { // remove rest of notes
            int tempCount = count();
            for (int n = noteId; n < tempCount; ++n)
                resetBeam(m_notes.takeLast());
    } else {
        if (count() - noteId > 2) {
            for (int n = 0; n <= noteId; ++n)
                resetBeam(m_notes.takeFirst());
        } else
            deleteBeam = true;
    if (!deleteBeam && !m_16beams.isEmpty()) { // renew 16th beam(s) state
        m_16beams.clear();
        for (int n = 0; n < count(); ++n) {
            auto nt = m_notes[n];
            if (nt->note()->rhythm() == Trhythm::Sixteenth) {
                if (m_16beams.isEmpty())
                    m_16beams << T16beam(n);
                else {
                    T16beam &last16Beam = m_16beams.last();
                    if (last16Beam.isHalf()) {
                        if (last16Beam.startStem == n - 1)
                            last16Beam.endStem = n;
                        else
                            m_16beams << T16beam(n);
                    } else {
                        if (last16Beam.endStem == n - 1)
                            last16Beam.endStem = n;
                        else
                            m_16beams << T16beam(n);
                    }
                }
            }
        }
    }
    return deleteBeam;
void TbeamObject::setMeasure(TmeasureObject *m)
{
    if (m != m_measure) {
        m_measure = m;
        if (m_measure)
            changeStaff(m_measure->staff());
    }
void TbeamObject::deleteBeam()
{
    m_measure->score()->storeBeam(this);
    for (TnotePair *np : std::as_const(m_notes)) {
        resetBeam(np);
    }
    m_16beams.clear();
    m_notes.clear();
    changeStaff(nullptr);
    m_measure = nullptr;
#if !defined(Q_OS_ANDROID)
bool TbeamObject::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == qApp && event->type() == QEvent::ApplicationPaletteChange) {
        update();
    }
    return QObject::eventFilter(obj, event);
}
#endif
void TbeamObject::resetBeam(TnotePair *noteToRemove)
{
    noteToRemove->note()->rtm.setBeam(Trhythm::e_noBeam); // restore beams
    noteToRemove->addChange(TnotePair::e_beamChanged); // and inform TnotePair about changes (it has to invoke approve to fix them)
    noteToRemove->setBeam(nullptr);