Add Beat Preview to PatternClipView (#7559)

This PR changes the way PatternClips are drawn in include a simple "beat preivew," where each note in any non-empty instrument tracks within the pattern are drawn as short little rectangles on the ClipView. SampleClips and AutomationClips within patterns are not currently supported.

The height of the note boxes changes depending on how many tracks there are in the pattern, and how many of them actually have notes (empty tracks are only drawn half as tall).

There is also some padding at the top and bottom along with a little bit of spacing between each note, both vertically and horizontally. This can be edited in the css, along with the note color.

---------

Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
Co-authored-by: Fawn <rubiefawn@gmail.com>
This commit is contained in:
regulus79
2025-12-02 17:34:50 -05:00
committed by GitHub
parent 58a4a8a909
commit 94f3b9704a
8 changed files with 102 additions and 13 deletions

View File

@@ -210,6 +210,7 @@ void TrackContainer::removeTrack( Track * _track )
{
Engine::getSong()->setModified();
}
emit trackRemoved();
}
}

View File

@@ -583,7 +583,7 @@ void ClipView::paintTextLabel(QString const & text, QPainter & painter)
elidedClipName = text.trimmed();
}
painter.fillRect(QRect(0, 0, width(), fontMetrics.height() + 2 * textTop), textBackgroundColor());
painter.fillRect(QRect(0, 0, fontMetrics.horizontalAdvance(elidedClipName) + 8, fontMetrics.height() + 2 * textTop), textBackgroundColor());
int const finalTextTop = textTop + fontMetrics.ascent();
painter.setPen(textShadowColor());

View File

@@ -533,10 +533,7 @@ void MidiClipView::wheelEvent(QWheelEvent * we)
Engine::getSong()->setModified();
update();
if( getGUI()->pianoRoll()->currentMidiClip() == m_clip )
{
getGUI()->pianoRoll()->update();
}
m_clip->updatePatternTrack();
}
we->accept();
}

View File

@@ -31,8 +31,10 @@
#include "Engine.h"
#include "GuiApplication.h"
#include "MainWindow.h"
#include "MidiClip.h"
#include "PatternClip.h"
#include "PatternStore.h"
#include "PatternTrack.h"
#include "RenameDialog.h"
#include "TrackContainerView.h"
#include "TrackView.h"
@@ -48,6 +50,10 @@ PatternClipView::PatternClipView(Clip* _clip, TrackView* _tv) :
{
connect( _clip->getTrack(), SIGNAL(dataChanged()),
this, SLOT(update()));
connect(Engine::patternStore(), &TrackContainer::trackAdded,
this, &PatternClipView::update);
connect(Engine::patternStore(), &TrackContainer::trackRemoved,
this, &PatternClipView::update);
setStyle( QApplication::style() );
}
@@ -108,6 +114,13 @@ void PatternClipView::paintEvent(QPaintEvent*)
// paint a black rectangle under the clip to prevent glitches with transparent backgrounds
p.fillRect( rect(), QColor( 0, 0, 0 ) );
const int pixelsPerPattern = Engine::patternStore()->lengthOfPattern(m_patternClip->patternIndex()) * pixelsPerBar();
int offset = static_cast<int>(m_patternClip->startTimeOffset() * (pixelsPerBar() / TimePos::ticksPerBar()))
% pixelsPerPattern;
if (offset < 2) {
offset += pixelsPerPattern;
}
if( gradient() )
{
p.fillRect( rect(), lingrad );
@@ -116,15 +129,71 @@ void PatternClipView::paintEvent(QPaintEvent*)
{
p.fillRect( rect(), c );
}
// Draw notes
const int patternIndex = static_cast<PatternTrack*>(m_patternClip->getTrack())->patternIndex();
// Count the number of non-empty instrument tracks.
// Only used midi tracks will be drawn, but empty tracks will still give a bit of empty space for padding.
int numberInstrumentTracksUsed = 0;
for (const auto& track : Engine::patternStore()->tracks())
{
const MidiClip* const mClip = dynamic_cast<MidiClip*>(track->getClip(patternIndex));
if (mClip && mClip->notes().size() > 0)
{
numberInstrumentTracksUsed++;
}
}
const auto totalTracks = Engine::patternStore()->tracks().size();
const auto numberEmptyTracks = totalTracks - numberInstrumentTracksUsed;
const float totalHeight = height() * (1.0f - 2 * m_verticalPadding);
const float totalHeightForEmptyTracks = totalHeight * numberEmptyTracks / totalTracks * m_emptyTrackHeightRatio;
const float totalHeightForTracks = totalHeight - totalHeightForEmptyTracks;
const float trackHeight = numberInstrumentTracksUsed > 0
? totalHeightForTracks / numberInstrumentTracksUsed
: 0.f;
const float emptyTrackHeight = numberEmptyTracks > 0
? totalHeightForEmptyTracks / numberEmptyTracks
: 0.f;
const int verticalNoteSpacing = trackHeight * m_noteVerticalSpacing;
const int horizontalNoteSpacing = pixelsPerBar() / TimePos::stepsPerBar() * m_noteHorizontalSpacing;
float lastY = height() * m_verticalPadding;
for (const auto& track : Engine::patternStore()->tracks())
{
const MidiClip* const mClip = dynamic_cast<MidiClip*>(track->getClip(patternIndex));
if (!mClip || mClip->notes().size() == 0)
{
lastY += emptyTrackHeight;
continue;
}
// Compare how long the clip view is compared to the underlying pattern. First +1 for ceiling, second +1 for possible previous bar.
const int maxPossibleRepetions = getClip()->length() / mClip->length() + 1 + 1;
for (const Note* note : mClip->notes())
{
QRect noteRect = QRect(
note->pos() * pixelsPerBar() / TimePos::ticksPerBar() + offset + horizontalNoteSpacing / 2,
lastY + verticalNoteSpacing / 2,
std::max(1.0f, pixelsPerBar() / TimePos::stepsPerBar() - horizontalNoteSpacing),
std::max(1.0f, trackHeight - verticalNoteSpacing)
);
// Loop through all the possible bars this pattern could affect. Starting at -1 for the possibility of start offset
const auto noteColor = QColor(m_noteColor.red(), m_noteColor.green(), m_noteColor.blue(), std::clamp(note->getVolume() * 255 / 100, 50, 255));
for (int i = -1; i < maxPossibleRepetions - 1; i++)
{
noteRect.moveLeft(note->pos() * pixelsPerBar() / TimePos::ticksPerBar() + offset + i * pixelsPerPattern + horizontalNoteSpacing / 2);
p.fillRect(noteRect, noteColor);
}
}
lastY += trackHeight;
}
// bar lines
const int lineSize = 3;
int pixelsPerPattern = Engine::patternStore()->lengthOfPattern(m_patternClip->patternIndex()) * pixelsPerBar();
int offset = static_cast<int>(m_patternClip->startTimeOffset() * (pixelsPerBar() / TimePos::ticksPerBar()))
% pixelsPerPattern;
if (offset < 2) {
offset += pixelsPerPattern;
}
p.setPen( c.darker( 200 ) );