/*************************************************************************** * 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 "tnoteitem.h" #include "tnotepair.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 */ class T16beam { public: 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); } TbeamObject::~TbeamObject() { // 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); else 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); }