diff --git a/include/PatternEditor.h b/include/PatternEditor.h index 0ea0061203a..70b60e8ab14 100644 --- a/include/PatternEditor.h +++ b/include/PatternEditor.h @@ -27,19 +27,24 @@ #include "Editor.h" #include "TrackContainerView.h" +#include "AutomatableModel.h" + +class QLabel; +class QScrollBar; namespace lmms { +class IntModel; class PatternStore; namespace gui { +class AutomatableSlider; class ComboBox; class TimeLineWidget; - class PatternEditor : public TrackContainerView { Q_OBJECT @@ -65,18 +70,44 @@ public slots: void cloneClip(); void updateMaxSteps(); +signals: + void zoomLevelChanged(); + void offsetValueChanged(); + void zoomControlsVisibilityChanged(bool show); + +protected: + double getZoom() const + { + // The zoom level is calculated such as exactly one bar is visible when the zoom slider is at its maximum value, + // and the whole pattern is visible when the zoom slider is at its minimum value. + return 1 + m_zoomingModel->value() * (m_maxClipLength / TimePos::ticksPerBar() - 1) + / static_cast(m_zoomingModel->maxValue()); + } + protected slots: void dropEvent(QDropEvent * de ) override; void resizeEvent(QResizeEvent* de) override; void updatePosition(); void updatePixelsPerBar(); + void updateScrollBar(); private: + void wheelEvent(QWheelEvent* we) override; + + IntModel* m_zoomingModel; + QScrollBar* m_leftRightScroll; + PatternStore* m_ps; TimeLineWidget* m_timeLine; int m_trackHeadWidth; tick_t m_maxClipLength; void makeSteps( bool clone ); + +private slots: + void zoomingChanged(); + void horizontalScrollChanged(); + + friend class PatternEditorWindow; }; @@ -89,14 +120,27 @@ Q_OBJECT QSize sizeHint() const override; + double zoomLevel() const + { + return m_editor->getZoom(); + } + + double horizontalScrollValue() const; + PatternEditor* m_editor; public slots: void play() override; void stop() override; + void showZoomControls(bool show); + private: + AutomatableSlider* m_zoomingSlider; ComboBox* m_patternComboBox; + + QAction* m_zoomIconAction; + QAction* m_zoomSliderAction; }; diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index 44ff6fd36e9..e3555970182 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -42,6 +42,7 @@ #include "KeyboardShortcuts.h" #include "lmms_math.h" #include "MidiClipView.h" +#include "PatternEditor.h" #include "PatternClip.h" #include "PatternStore.h" #include "Song.h" @@ -119,6 +120,8 @@ ClipView::ClipView( Clip * clip, connect( m_clip, SIGNAL(lengthChanged()), this, SLOT(updateLength())); connect(getGUI()->songEditor()->m_editor, &SongEditor::pixelsPerBarChanged, this, &ClipView::updateLength); + connect(getGUI()->patternEditor()->m_editor, &PatternEditor::zoomLevelChanged, this, &ClipView::updateLength); + connect(getGUI()->patternEditor()->m_editor, &PatternEditor::offsetValueChanged, this, &ClipView::updatePosition); connect( m_clip, SIGNAL(positionChanged()), this, SLOT(updatePosition())); connect( m_clip, SIGNAL(destroyedClip()), this, SLOT(close())); @@ -314,7 +317,7 @@ void ClipView::updateLength() { if( fixedClips() ) { - setFixedWidth( parentWidget()->width() ); + setFixedWidth(parentWidget()->width() * getGUI()->patternEditor()->zoomLevel()); } else { @@ -337,10 +340,17 @@ void ClipView::updateLength() */ void ClipView::updatePosition() { - m_trackView->getTrackContentWidget()->changePosition(); - // moving a Clip can result in change of song-length etc., - // therefore we update the track-container - m_trackView->trackContainerView()->update(); + if (fixedClips()) + { + move(-parentWidget()->width() * getGUI()->patternEditor()->horizontalScrollValue(), 0); + } + else + { + m_trackView->getTrackContentWidget()->changePosition(); + // moving a Clip can result in change of song-length etc., + // therefore we update the track-container + m_trackView->trackContainerView()->update(); + } } void ClipView::selectColor() diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index 17604eb196a..1cbadcfabd2 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -498,7 +498,8 @@ void MidiClipView::wheelEvent(QWheelEvent * we) const auto pos = we->position().toPoint(); if(m_clip->m_clipType == MidiClip::Type::BeatClip && (fixedClips() || pixelsPerBar() >= 96) && - pos.y() > height() - m_stepBtnOff.height()) + pos.y() > height() - m_stepBtnOff.height() && + !(we->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier))) { // get the step number that was wheeled on and // do calculations in floats to prevent rounding errors... @@ -822,16 +823,46 @@ void MidiClipView::paintEvent( QPaintEvent * ) const int lineSize = 3; p.setPen( c.darker( 200 ) ); - for(float t = (offset % TimePos::ticksPerBar()) * pixelsPerBar / TimePos::ticksPerBar(); t < m_clip->length(); t += pixelsPerBar) + if (fixedClips()) { - p.drawLine( x_base + t - 1, + // We don't draw the bar lines the same way in the pattern editor as the view's lenght and position are + // modified arbitrarily by zoom and scroll values, and the clip always start at t=0 + const int steps = std::max(1, m_clip->m_steps); + const int w = width() - 2 * BORDER_WIDTH; + + for (int step = TimePos::stepsPerBar(); step < steps; step += TimePos::stepsPerBar()) + { + p.drawLine( + BORDER_WIDTH + step * w / static_cast(steps), BORDER_WIDTH, + BORDER_WIDTH + step * w / static_cast(steps), + BORDER_WIDTH + lineSize + ); + p.drawLine( + BORDER_WIDTH + step * w / static_cast(steps), + rect().bottom() - (lineSize + BORDER_WIDTH), + BORDER_WIDTH + step * w / static_cast(steps), + rect().bottom() - BORDER_WIDTH + ); + } + } + else + { + for(float t = (offset % TimePos::ticksPerBar()) * pixelsPerBar / TimePos::ticksPerBar(); t < m_clip->length(); t += pixelsPerBar) + { + p.drawLine( + x_base + t - 1, + BORDER_WIDTH, + x_base + t - 1, + BORDER_WIDTH + lineSize + ); + p.drawLine( x_base + t - 1, - BORDER_WIDTH + lineSize ); - p.drawLine( x_base + t - 1, - rect().bottom() - ( lineSize + BORDER_WIDTH ), + rect().bottom() - (lineSize + BORDER_WIDTH), x_base + t - 1, - rect().bottom() - BORDER_WIDTH ); + rect().bottom() - BORDER_WIDTH + ); + } } // clip name diff --git a/src/gui/editors/PatternEditor.cpp b/src/gui/editors/PatternEditor.cpp index 6c3b66e29b5..a899f3a457e 100644 --- a/src/gui/editors/PatternEditor.cpp +++ b/src/gui/editors/PatternEditor.cpp @@ -25,8 +25,12 @@ #include "PatternEditor.h" #include +#include +#include +#include #include +#include "AutomatableSlider.h" #include "ClipView.h" #include "ComboBox.h" #include "DataFile.h" @@ -48,6 +52,7 @@ namespace lmms::gui PatternEditor::PatternEditor(PatternStore* ps) : TrackContainerView(ps), + m_zoomingModel(new IntModel(0, 0, 100, nullptr, tr("Zoom"))), m_ps(ps), m_trackHeadWidth(ConfigManager::inst()->value("ui", "compacttrackbuttons").toInt() == 1 ? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT + TRACK_OP_WIDTH_COMPACT @@ -60,12 +65,33 @@ PatternEditor::PatternEditor(PatternStore* ps) : Engine::getSong()->getTimeline(Song::PlayMode::Pattern), m_currentPosition, this ); + connect(this, &TrackContainerView::positionChanged, m_timeLine, qOverload<>(&QWidget::update)); connect(m_timeLine->timeline(), &Timeline::positionChanged, this, &PatternEditor::updatePosition); static_cast(layout())->insertWidget(0, m_timeLine); connect(m_ps, &PatternStore::trackUpdated, this, &PatternEditor::updateMaxSteps); + // Set up zooming model + m_zoomingModel->setParent(this); + m_zoomingModel->setJournalling(false); + connect(m_zoomingModel, SIGNAL(dataChanged()), this, SLOT(zoomingChanged())); + + connect(Engine::getSong(), &Song::stopped, this, [this]() + { + // Show zoom controls again as they are hidden during playback + emit zoomControlsVisibilityChanged(m_maxClipLength / TimePos::ticksPerBar() > 1); + }); + + // Set up horizontal scroll bar + m_leftRightScroll = new QScrollBar(Qt::Horizontal, this); + m_leftRightScroll->setMinimum(0); + m_leftRightScroll->setMaximum(0); + m_leftRightScroll->setSingleStep(1); + m_leftRightScroll->setPageStep(m_maxClipLength); + static_cast(layout())->addWidget(m_leftRightScroll); + connect(m_leftRightScroll, SIGNAL(valueChanged(int)), this, SLOT(horizontalScrollChanged())); + setFocusPolicy(Qt::StrongFocus); setFocus(); } @@ -206,7 +232,7 @@ void PatternEditor::updatePixelsPerBar() setPixelsPerBar(m_maxClipLength != 0 ? (width() - m_trackHeadWidth) * TimePos::ticksPerBar() / m_maxClipLength : (width() - m_trackHeadWidth)); - m_timeLine->setPixelsPerBar(pixelsPerBar()); + m_timeLine->setPixelsPerBar(pixelsPerBar() * getZoom()); } void PatternEditor::updateMaxSteps() @@ -223,6 +249,41 @@ void PatternEditor::updateMaxSteps() } } updatePixelsPerBar(); + updateScrollBar(); + + // Show zoom controls if the pattern is longer than one bar, hide them otherwise as they would have no effect anyway + emit zoomControlsVisibilityChanged(m_maxClipLength / TimePos::ticksPerBar() > 1); +} + + +void PatternEditor::updateScrollBar() +{ + m_leftRightScroll->setPageStep(m_maxClipLength / getZoom()); + m_leftRightScroll->setMaximum(m_maxClipLength - m_leftRightScroll->pageStep()); +} + + +void PatternEditor::wheelEvent(QWheelEvent* we) +{ + const auto posX = we->position().toPoint().x(); + if ((we->modifiers() & Qt::ControlModifier) && (posX > m_trackHeadWidth) && !Engine::getSong()->isPlaying()) + { + // move zoom slider (pixelsPerBar will change automatically) + int step = we->modifiers() & Qt::ShiftModifier ? 1 : 5; + // when Alt is pressed, wheelEvent returns delta for x coordinate (mimics horizontal mouse wheel) + int direction = (we->angleDelta().y() + we->angleDelta().x()) > 0 ? 1 : -1; + m_zoomingModel->incValue(step * direction); + } + else if (we->modifiers() & Qt::ShiftModifier) + { + m_leftRightScroll->setValue(m_leftRightScroll->value() - we->angleDelta().y()); + } + else + { + we->ignore(); + return; + } + we->accept(); } @@ -271,6 +332,23 @@ void PatternEditor::cloneClip() } +void PatternEditor::zoomingChanged() +{ + updatePixelsPerBar(); + updateScrollBar(); + + emit zoomLevelChanged(); +} + +void PatternEditor::horizontalScrollChanged() +{ + m_currentPosition = TimePos(m_leftRightScroll->value()); + updatePosition(); + + emit offsetValueChanged(); +} + + PatternEditorWindow::PatternEditorWindow(PatternStore* ps) : @@ -327,6 +405,23 @@ PatternEditorWindow::PatternEditorWindow(PatternStore* ps) : stretch->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); trackAndStepActionsToolBar->addWidget(stretch); + QLabel* zoom_lbl = new QLabel(m_toolBar); + zoom_lbl->setPixmap(embed::getIconPixmap("zoom")); + + // Set slider zoom + m_zoomingSlider = new AutomatableSlider(m_toolBar, tr("Zoom")); + m_zoomingSlider->setModel(m_editor->m_zoomingModel); + m_zoomingSlider->setOrientation(Qt::Horizontal); + m_zoomingSlider->setPageStep(1); + m_zoomingSlider->setFocusPolicy(Qt::NoFocus); + m_zoomingSlider->setFixedSize(100, 26); + m_zoomingSlider->setToolTip(tr("Zoom")); + m_zoomingSlider->setContextMenuPolicy(Qt::NoContextMenu); + connect(m_editor, &PatternEditor::zoomControlsVisibilityChanged, this, &PatternEditorWindow::showZoomControls); + + m_zoomIconAction = trackAndStepActionsToolBar->addWidget(zoom_lbl); + m_zoomSliderAction = trackAndStepActionsToolBar->addWidget(m_zoomingSlider); + showZoomControls( false ); // Step actions trackAndStepActionsToolBar->addAction(embed::getIconPixmap("step_btn_remove"), tr("Remove steps"), @@ -358,15 +453,28 @@ QSize PatternEditorWindow::sizeHint() const } +double PatternEditorWindow::horizontalScrollValue() const +{ + return m_editor->m_leftRightScroll->value() / static_cast(m_editor->m_leftRightScroll->pageStep()); +} + + void PatternEditorWindow::play() { + showZoomControls(false); + if (Engine::getSong()->playMode() != Song::PlayMode::Pattern) { + m_zoomingSlider->setValue(0); Engine::getSong()->playPattern(); } else { Engine::getSong()->togglePause(); + if (Engine::getSong()->isPaused()) + { + showZoomControls(true); + } } } @@ -377,4 +485,14 @@ void PatternEditorWindow::stop() } +void PatternEditorWindow::showZoomControls(bool show) +{ + if (!Engine::getSong()->isPlaying()) + { + m_zoomIconAction->setVisible(show); + m_zoomSliderAction->setVisible(show); + } +} + + } // namespace lmms::gui \ No newline at end of file diff --git a/src/gui/tracks/TrackContentWidget.cpp b/src/gui/tracks/TrackContentWidget.cpp index 086a77c12b7..8f2e4e91973 100644 --- a/src/gui/tracks/TrackContentWidget.cpp +++ b/src/gui/tracks/TrackContentWidget.cpp @@ -241,7 +241,6 @@ void TrackContentWidget::changePosition( const TimePos & newPos ) { if (clipView->getClip()->startPosition().getBar() == curPattern) { - clipView->move(0, clipView->y()); clipView->raise(); clipView->show(); }