From 94f3b9704a224af9149c6946f80db6c5dce32f37 Mon Sep 17 00:00:00 2001 From: regulus79 <117475203+regulus79@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:34:50 -0500 Subject: [PATCH] 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 Co-authored-by: Fawn --- data/themes/classic/style.css | 7 ++- data/themes/default/style.css | 7 ++- include/PatternClipView.h | 11 +++++ include/TrackContainer.h | 1 + src/core/TrackContainer.cpp | 1 + src/gui/clips/ClipView.cpp | 2 +- src/gui/clips/MidiClipView.cpp | 5 +- src/gui/clips/PatternClipView.cpp | 81 ++++++++++++++++++++++++++++--- 8 files changed, 102 insertions(+), 13 deletions(-) diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index ea19f5ccb..6957c8d6f 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -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 */ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index efe2dc080..7323eb6c9 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -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 */ diff --git a/include/PatternClipView.h b/include/PatternClipView.h index ee313f32a..c45c957ef 100644 --- a/include/PatternClipView.h +++ b/include/PatternClipView.h @@ -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}; } ; diff --git a/include/TrackContainer.h b/include/TrackContainer.h index 01e94df54..797b47289 100644 --- a/include/TrackContainer.h +++ b/include/TrackContainer.h @@ -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); diff --git a/src/core/TrackContainer.cpp b/src/core/TrackContainer.cpp index a2c4ee6c1..5a68ac8db 100644 --- a/src/core/TrackContainer.cpp +++ b/src/core/TrackContainer.cpp @@ -210,6 +210,7 @@ void TrackContainer::removeTrack( Track * _track ) { Engine::getSong()->setModified(); } + emit trackRemoved(); } } diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index a4e702bc9..37ee4c29d 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -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()); diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 2aa1c7f1d..c92215d65 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -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(); } diff --git a/src/gui/clips/PatternClipView.cpp b/src/gui/clips/PatternClipView.cpp index 677728185..1dd8ad683 100644 --- a/src/gui/clips/PatternClipView.cpp +++ b/src/gui/clips/PatternClipView.cpp @@ -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(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(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(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(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(m_patternClip->startTimeOffset() * (pixelsPerBar() / TimePos::ticksPerBar())) - % pixelsPerPattern; - if (offset < 2) { - offset += pixelsPerPattern; - } p.setPen( c.darker( 200 ) );