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

@@ -792,7 +792,7 @@ lmms--gui--ClipView {
qproperty-selectedColor: rgb( 0, 125, 255 );
qproperty-patternClipBackground: rgb( 80, 80, 80 );
qproperty-textColor: rgb( 255, 255, 255 );
qproperty-textBackgroundColor: rgba(0, 0, 0, 75);
qproperty-textBackgroundColor: rgba(0, 0, 0, 100);
qproperty-textShadowColor: rgb( 0, 0, 0 );
qproperty-gradient: true; /* boolean property, set true to have a gradient */
qproperty-markerColor: rgb(0, 0, 0);
@@ -825,6 +825,11 @@ lmms--gui--AutomationClipView {
/* pattern clip */
lmms--gui--PatternClipView {
background-color: rgb( 128, 182, 175 ); /* default colour for pattern tracks */
qproperty-emptyTrackHeightRatio: 0.5; /* height of empty tracks versus non-empty tracks */
qproperty-verticalPadding: 0.15; /* padding above and below the beat preview on the clip, fraction of the total clip height */
qproperty-noteVerticalSpacing: 0.2; /* spacing above and below each note, fraction of the note height */
qproperty-noteHorizontalSpacing: 0.2; /* spacing left/right of each note, fraction of the note width */
qproperty-noteColor: #ffffff; /* colour of the beat/note rectangles on the clip */
}
/* Subwindows in MDI-Area */

View File

@@ -852,7 +852,7 @@ lmms--gui--ClipView {
qproperty-selectedColor: #006B65;
qproperty-patternClipBackground: #373d48;
qproperty-textColor: #fff;
qproperty-textBackgroundColor: rgba(0, 0, 0, 75);
qproperty-textBackgroundColor: rgba(0, 0, 0, 100);
qproperty-textShadowColor: rgba(0,0,0,200);
qproperty-gradient: false; /* boolean property, set true to have a gradient */
qproperty-markerColor: rgb(0, 0, 0);
@@ -885,6 +885,11 @@ lmms--gui--AutomationClipView {
/* pattern clip */
lmms--gui--PatternClipView {
background-color: #20BDB2; /* default colour for pattern tracks */
qproperty-emptyTrackHeightRatio: 0.5; /* height of empty tracks versus non-empty tracks */
qproperty-verticalPadding: 0.15; /* padding above and below the beat preview on the clip, fraction of the total clip height */
qproperty-noteVerticalSpacing: 0.2; /* spacing above and below each note, fraction of the note height */
qproperty-noteHorizontalSpacing: 0.2; /* spacing left/right of each note, fraction of the note width */
qproperty-noteColor: #ffffff; /* colour of the beat/note rectangles on the clip */
}
/* Subwindows in MDI-Area */

View File

@@ -40,6 +40,11 @@ namespace gui
class PatternClipView : public ClipView
{
Q_OBJECT
Q_PROPERTY(float emptyTrackHeightRatio MEMBER m_emptyTrackHeightRatio)
Q_PROPERTY(float verticalPadding MEMBER m_verticalPadding)
Q_PROPERTY(float noteVerticalSpacing MEMBER m_noteVerticalSpacing)
Q_PROPERTY(float noteHorizontalSpacing MEMBER m_noteHorizontalSpacing)
Q_PROPERTY(QColor noteColor MEMBER m_noteColor)
public:
PatternClipView(Clip* clip, TrackView* tv);
~PatternClipView() override = default;
@@ -65,6 +70,12 @@ private:
QPixmap m_paintPixmap;
QStaticText m_staticTextName;
float m_emptyTrackHeightRatio {0.5f};
float m_verticalPadding {0.15f};
float m_noteVerticalSpacing {0.2f};
float m_noteHorizontalSpacing {0.2f};
QColor m_noteColor {255, 255, 255};
} ;

View File

@@ -99,6 +99,7 @@ public:
signals:
void trackAdded( lmms::Track * _track );
void trackRemoved();
protected:
static AutomatedValueMap automatedValuesFromTracks(const TrackList &tracks, TimePos timeStart, int clipNum = -1);

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 ) );