diff --git a/data/themes/classic/auto_resize.png b/data/themes/classic/auto_resize.png new file mode 100644 index 00000000000..491966a8e6b Binary files /dev/null and b/data/themes/classic/auto_resize.png differ diff --git a/data/themes/classic/auto_resize_disable.png b/data/themes/classic/auto_resize_disable.png new file mode 100644 index 00000000000..e7c6c2c83ff Binary files /dev/null and b/data/themes/classic/auto_resize_disable.png differ diff --git a/data/themes/classic/clear_notes_out_of_bounds.png b/data/themes/classic/clear_notes_out_of_bounds.png new file mode 100644 index 00000000000..d3f27a0f554 Binary files /dev/null and b/data/themes/classic/clear_notes_out_of_bounds.png differ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index a064366a660..a0fb48209a5 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -761,6 +761,7 @@ lmms--gui--ClipView { qproperty-textBackgroundColor: rgba(0, 0, 0, 75); qproperty-textShadowColor: rgb( 0, 0, 0 ); qproperty-gradient: true; /* boolean property, set true to have a gradient */ + qproperty-markerColor: rgb(0, 0, 0); /* finger tip offset of cursor */ qproperty-mouseHotspotHand: 3px 3px; qproperty-mouseHotspotKnife: 0px 0px; diff --git a/data/themes/default/auto_resize.png b/data/themes/default/auto_resize.png new file mode 100644 index 00000000000..52b65742d84 Binary files /dev/null and b/data/themes/default/auto_resize.png differ diff --git a/data/themes/default/auto_resize_disable.png b/data/themes/default/auto_resize_disable.png new file mode 100644 index 00000000000..a997c81b65c Binary files /dev/null and b/data/themes/default/auto_resize_disable.png differ diff --git a/data/themes/default/clear_notes_out_of_bounds.png b/data/themes/default/clear_notes_out_of_bounds.png new file mode 100644 index 00000000000..8387db843aa Binary files /dev/null and b/data/themes/default/clear_notes_out_of_bounds.png differ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 07ee7461731..f7a5acc1441 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -804,6 +804,7 @@ lmms--gui--ClipView { qproperty-textBackgroundColor: rgba(0, 0, 0, 75); qproperty-textShadowColor: rgba(0,0,0,200); qproperty-gradient: false; /* boolean property, set true to have a gradient */ + qproperty-markerColor: rgb(0, 0, 0); /* finger tip offset of cursor */ qproperty-mouseHotspotHand: 7px 2px; qproperty-mouseHotspotKnife: 0px 0px; diff --git a/include/AutomationClip.h b/include/AutomationClip.h index 0b49978c7f7..abc713869a3 100644 --- a/include/AutomationClip.h +++ b/include/AutomationClip.h @@ -68,7 +68,6 @@ class LMMS_EXPORT AutomationClip : public Clip using TimemapIterator = timeMap::const_iterator; AutomationClip( AutomationTrack * _auto_track ); - AutomationClip( const AutomationClip & _clip_to_copy ); ~AutomationClip() override = default; bool addObject( AutomatableModel * _obj, bool _search_dup = true ); @@ -90,7 +89,7 @@ class LMMS_EXPORT AutomationClip : public Clip void setTension( QString _new_tension ); TimePos timeMapLength() const; - void updateLength(); + void updateLength() override; TimePos putValue( const TimePos & time, @@ -196,12 +195,22 @@ class LMMS_EXPORT AutomationClip : public Clip static int quantization() { return s_quantization; } static void setQuantization(int q) { s_quantization = q; } + AutomationClip* clone() override + { + return new AutomationClip(*this); + } + + void clearObjects() { m_objects.clear(); } + public slots: void clear(); void objectDestroyed( lmms::jo_id_t ); void flipY( int min, int max ); void flipY(); - void flipX( int length = -1 ); + void flipX(int start = -1, int end = -1); + +protected: + AutomationClip( const AutomationClip & _clip_to_copy ); private: void cleanObjects(); diff --git a/include/AutomationClipView.h b/include/AutomationClipView.h index bdd2f056872..e55c4422877 100644 --- a/include/AutomationClipView.h +++ b/include/AutomationClipView.h @@ -75,6 +75,8 @@ protected slots: QStaticText m_staticTextName; void scaleTimemapToFit( float oldMin, float oldMax ); + + bool isResizableBeforeStart() override { return false; } } ; diff --git a/include/AutomationEditor.h b/include/AutomationEditor.h index 61f1cb7914d..c7eaca8aa0f 100644 --- a/include/AutomationEditor.h +++ b/include/AutomationEditor.h @@ -74,6 +74,7 @@ class AutomationEditor : public QWidget, public JournallingObject Q_PROPERTY(QColor ghostNoteColor MEMBER m_ghostNoteColor) Q_PROPERTY(QColor detuningNoteColor MEMBER m_detuningNoteColor) Q_PROPERTY(QColor ghostSampleColor MEMBER m_ghostSampleColor) + Q_PROPERTY(QColor outOfBoundsShade MEMBER m_outOfBoundsShade) public: void setCurrentClip(AutomationClip * new_clip); void setGhostMidiClip(MidiClip* newMidiClip); @@ -291,6 +292,7 @@ protected slots: QColor m_ghostNoteColor; QColor m_detuningNoteColor; QColor m_ghostSampleColor; + QColor m_outOfBoundsShade; SampleThumbnail m_sampleThumbnail; diff --git a/include/Clip.h b/include/Clip.h index a520ad4e470..706b982c80f 100644 --- a/include/Clip.h +++ b/include/Clip.h @@ -100,12 +100,28 @@ class LMMS_EXPORT Clip : public Model, public JournallingObject * resized by clicking and dragging its edge. * */ - inline void setAutoResize( const bool r ) + inline void setResizable( const bool r ) + { + m_resizable = r; + } + + inline const bool getResizable() const + { + return m_resizable; + } + + /*! \brief Set whether a clip has been resized yet by the user or the knife tool. + * + * If a clip has been resized previously, it will not automatically + * resize when editing it. + * + */ + void setAutoResize(const bool r) { m_autoResize = r; } - inline const bool getAutoResize() const + bool getAutoResize() const { return m_autoResize; } @@ -115,6 +131,7 @@ class LMMS_EXPORT Clip : public Model, public JournallingObject virtual void movePosition( const TimePos & pos ); virtual void changeLength( const TimePos & length ); + virtual void updateLength() {}; virtual gui::ClipView * createView( gui::TrackView * tv ) = 0; @@ -137,6 +154,12 @@ class LMMS_EXPORT Clip : public Model, public JournallingObject // Will copy the state of a clip to another clip static void copyStateTo( Clip *src, Clip *dst ); + /** + * Creates a copy of this clip + * @return pointer to the new clip object + */ + virtual Clip* clone() = 0; + public slots: void toggleMute(); @@ -147,6 +170,8 @@ public slots: void destroyedClip(); void colorChanged(); +protected: + Clip(const Clip& other); private: Track * m_track; @@ -158,7 +183,8 @@ public slots: BoolModel m_mutedModel; BoolModel m_soloModel; - bool m_autoResize; + bool m_resizable = true; + bool m_autoResize = true; bool m_selectViewOnCreate; diff --git a/include/ClipView.h b/include/ClipView.h index 14898db65b4..6e904b856ad 100644 --- a/include/ClipView.h +++ b/include/ClipView.h @@ -63,6 +63,7 @@ class ClipView : public selectableObject, public ModelView Q_PROPERTY( QColor textShadowColor READ textShadowColor WRITE setTextShadowColor ) Q_PROPERTY( QColor patternClipBackground READ patternClipBackground WRITE setPatternClipBackground ) Q_PROPERTY( bool gradient READ gradient WRITE setGradient ) + Q_PROPERTY(QColor markerColor READ markerColor WRITE setMarkerColor) // We have to use a QSize here because using QPoint isn't supported. // width -> x, height -> y Q_PROPERTY( QSize mouseHotspotHand MEMBER m_mouseHotspotHand ) @@ -94,6 +95,7 @@ class ClipView : public selectableObject, public ModelView QColor textBackgroundColor() const; QColor textShadowColor() const; QColor patternClipBackground() const; + QColor markerColor() const; bool gradient() const; void setMutedColor( const QColor & c ); void setMutedBackgroundColor( const QColor & c ); @@ -103,6 +105,7 @@ class ClipView : public selectableObject, public ModelView void setTextShadowColor( const QColor & c ); void setPatternClipBackground(const QColor& c); void setGradient( const bool & b ); + void setMarkerColor(const QColor& c); // access needsUpdate member variable bool needsUpdate(); @@ -121,10 +124,8 @@ class ClipView : public selectableObject, public ModelView // some metadata to be written to the clipboard. static void remove( QVector clipvs ); static void toggleMute( QVector clipvs ); - static void mergeClips(QVector clipvs); - // Returns true if selection can be merged and false if not - static bool canMergeSelection(QVector clipvs); + void toggleSelectedAutoResize(); QColor getColorForDisplay( QColor ); @@ -147,8 +148,7 @@ public slots: Cut, Copy, Paste, - Mute, - Merge + Mute }; TrackView * m_trackView; @@ -176,7 +176,7 @@ public slots: } bool unquantizedModHeld( QMouseEvent * me ); - TimePos quantizeSplitPos( TimePos, bool shiftMode ); + TimePos quantizeSplitPos(TimePos); float pixelsPerBar(); @@ -224,6 +224,7 @@ protected slots: QColor m_textShadowColor; QColor m_patternClipBackground; bool m_gradient; + QColor m_markerColor; QSize m_mouseHotspotHand; // QSize must be used because QPoint QSize m_mouseHotspotKnife; // isn't supported by property system QCursor m_cursorHand; @@ -244,8 +245,24 @@ protected slots: TimePos draggedClipPos( QMouseEvent * me ); int knifeMarkerPos( QMouseEvent * me ); void setColor(const std::optional& color); - //! Return true iff the clip could be split. Currently only implemented for samples - virtual bool splitClip( const TimePos pos ){ return false; }; + + //! Returns whether the user can left-resize this clip so that the start of the clip bounds is before the start of the clip content. + virtual bool isResizableBeforeStart() { return true; }; + /** + * Split this Clip into two clips + * @param pos the position of the split, relative to the start of the clip + * @return true if the clip could be split + */ + bool splitClip(const TimePos pos); + /** + * Destructively split this Clip into two clips. If the clip type does not implement this feature, it will default to normal splitting. + * @param pos the position of the split, relative to the start of the clip + * @return true if the clip could be split + */ + virtual bool destructiveSplitClip(const TimePos pos) + { + return splitClip(pos); + } void updateCursor(QMouseEvent * me); } ; diff --git a/include/DetuningHelper.h b/include/DetuningHelper.h index da8eb598350..b767080823a 100644 --- a/include/DetuningHelper.h +++ b/include/DetuningHelper.h @@ -39,6 +39,10 @@ class DetuningHelper : public InlineAutomation InlineAutomation() { } + DetuningHelper(const DetuningHelper& _copy) : + InlineAutomation(_copy) + { + } ~DetuningHelper() override = default; diff --git a/include/InlineAutomation.h b/include/InlineAutomation.h index 3e27e137bc5..5928b2db453 100644 --- a/include/InlineAutomation.h +++ b/include/InlineAutomation.h @@ -32,22 +32,24 @@ namespace lmms { -class InlineAutomation : public FloatModel, public sharedObject +class InlineAutomation : public FloatModel { public: InlineAutomation() : - FloatModel(), - sharedObject(), - m_autoClip( nullptr ) + FloatModel() { } + InlineAutomation(const InlineAutomation& _copy) : + FloatModel(_copy.value(), _copy.minValue(), _copy.maxValue(), _copy.step()), + m_autoClip(_copy.m_autoClip->clone()) + { + m_autoClip->clearObjects(); + m_autoClip->addObject(this); + } + ~InlineAutomation() override { - if( m_autoClip ) - { - delete m_autoClip; - } } virtual float defaultValue() const = 0; @@ -81,10 +83,10 @@ class InlineAutomation : public FloatModel, public sharedObject { if( m_autoClip == nullptr ) { - m_autoClip = new AutomationClip( nullptr ); + m_autoClip = std::make_unique(nullptr); m_autoClip->addObject( this ); } - return m_autoClip; + return m_autoClip.get(); } void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override; @@ -92,7 +94,7 @@ class InlineAutomation : public FloatModel, public sharedObject private: - AutomationClip * m_autoClip; + std::unique_ptr m_autoClip; } ; diff --git a/include/MidiClip.h b/include/MidiClip.h index f3150ba6f24..022f5c558ed 100644 --- a/include/MidiClip.h +++ b/include/MidiClip.h @@ -53,12 +53,11 @@ class LMMS_EXPORT MidiClip : public Clip } ; MidiClip( InstrumentTrack* instrumentTrack ); - MidiClip( const MidiClip& other ); ~MidiClip() override; void init(); - void updateLength(); + void updateLength() override; // note management Note * addNote( const Note & _new_note, const bool _quant_pos = true ); @@ -114,6 +113,11 @@ class LMMS_EXPORT MidiClip : public Clip gui::ClipView * createView( gui::TrackView * _tv ) override; + MidiClip* clone() override + { + return new MidiClip(*this); + } + using Model::dataChanged; @@ -124,6 +128,7 @@ public slots: void clear(); protected: + MidiClip( const MidiClip& other ); void updatePatternTrack(); protected slots: diff --git a/include/MidiClipView.h b/include/MidiClipView.h index 4285bf9da0f..9f18947554b 100644 --- a/include/MidiClipView.h +++ b/include/MidiClipView.h @@ -63,6 +63,11 @@ class MidiClipView : public ClipView QColor const & getMutedNoteBorderColor() const { return m_mutedNoteBorderColor; } void setMutedNoteBorderColor(QColor const & color) { m_mutedNoteBorderColor = color; } + // Returns true if selection can be merged and false if not + static bool canMergeSelection(QVector clipvs); + static void mergeClips(QVector clipvs); + static void bulkClearNotesOutOfBounds(QVector clipvs); + public slots: lmms::MidiClip* getMidiClip(); void update() override; @@ -76,6 +81,7 @@ protected slots: void resetName(); void changeName(); void transposeSelection(); + void clearNotesOutOfBounds(); protected: @@ -103,6 +109,10 @@ protected slots: QStaticText m_staticTextName; bool m_legacySEPattern; + + bool isResizableBeforeStart() override { return false; } + + bool destructiveSplitClip(const TimePos pos) override; } ; diff --git a/include/Note.h b/include/Note.h index 08cbce3dbeb..7d79eeecd9c 100644 --- a/include/Note.h +++ b/include/Note.h @@ -26,6 +26,7 @@ #ifndef LMMS_NOTE_H #define LMMS_NOTE_H +#include #include #include @@ -107,6 +108,8 @@ class LMMS_EXPORT Note : public SerializingObject Note( const Note & note ); ~Note() override; + Note& operator=(const Note& note); + // Note types enum class Type { @@ -236,7 +239,7 @@ class LMMS_EXPORT Note : public SerializingObject DetuningHelper * detuning() const { - return m_detuning; + return m_detuning.get(); } bool hasDetuningInfo() const; bool withinRange(int tickStart, int tickEnd) const; @@ -262,7 +265,7 @@ class LMMS_EXPORT Note : public SerializingObject panning_t m_panning; TimePos m_length; TimePos m_pos; - DetuningHelper * m_detuning; + std::unique_ptr m_detuning; Type m_type = Type::Regular; }; diff --git a/include/PatternClip.h b/include/PatternClip.h index 968a0b198dc..0be21740739 100644 --- a/include/PatternClip.h +++ b/include/PatternClip.h @@ -51,6 +51,11 @@ class PatternClip : public Clip gui::ClipView * createView( gui::TrackView * _tv ) override; + PatternClip* clone() override + { + return new PatternClip(*this); + } + private: friend class PatternClipView; } ; diff --git a/include/PianoRoll.h b/include/PianoRoll.h index fb175c374f7..746de66f3ad 100644 --- a/include/PianoRoll.h +++ b/include/PianoRoll.h @@ -90,6 +90,7 @@ class PianoRoll : public QWidget Q_PROPERTY(int ghostNoteOpacity MEMBER m_ghostNoteOpacity) Q_PROPERTY(bool ghostNoteBorders MEMBER m_ghostNoteBorders) Q_PROPERTY(QColor backgroundShade MEMBER m_backgroundShade) + Q_PROPERTY(QColor outOfBoundsShade MEMBER m_outOfBoundsShade) /* white key properties */ Q_PROPERTY(int whiteKeyWidth MEMBER m_whiteKeyWidth) @@ -515,6 +516,7 @@ protected slots: bool m_noteBorders; bool m_ghostNoteBorders; QColor m_backgroundShade; + QColor m_outOfBoundsShade; /* white key properties */ int m_whiteKeyWidth; QColor m_whiteKeyActiveTextColor; diff --git a/include/SampleClip.h b/include/SampleClip.h index 3beca338bcd..cbd3ac5d5df 100644 --- a/include/SampleClip.h +++ b/include/SampleClip.h @@ -49,7 +49,6 @@ class SampleClip : public Clip public: SampleClip(Track* track, Sample sample, bool isPlaying); SampleClip(Track* track); - SampleClip( const SampleClip& orig ); ~SampleClip() override; SampleClip& operator=( const SampleClip& that ) = delete; @@ -81,6 +80,11 @@ class SampleClip : public Clip void setIsPlaying(bool isPlaying); void setSampleBuffer(std::shared_ptr sb); + SampleClip* clone() override + { + return new SampleClip(*this); + } + public slots: void setSampleFile(const QString& sf); void updateLength(); @@ -88,6 +92,8 @@ public slots: void playbackPositionChanged(); void updateTrackClips(); +protected: + SampleClip( const SampleClip& orig ); private: Sample m_sample; diff --git a/include/SampleClipView.h b/include/SampleClipView.h index 14f9a8235e2..4ab4d77e7a5 100644 --- a/include/SampleClipView.h +++ b/include/SampleClipView.h @@ -68,7 +68,6 @@ public slots: SampleThumbnail m_sampleThumbnail; QPixmap m_paintPixmap; long m_paintPixmapXPosition; - bool splitClip( const TimePos pos ) override; } ; diff --git a/src/core/AutomatableModel.cpp b/src/core/AutomatableModel.cpp index e006be651a4..fa523e106d6 100644 --- a/src/core/AutomatableModel.cpp +++ b/src/core/AutomatableModel.cpp @@ -754,7 +754,7 @@ float AutomatableModel::globalAutomationValueAt( const TimePos& time ) if( latestClip ) { // scale/fit the value appropriately and return it - const float value = latestClip->valueAt( time - latestClip->startPosition() ); + const float value = latestClip->valueAt(time - latestClip->startPosition() + latestClip->startTimeOffset()); const float scaled_value = scaledValue( value ); return fittedValue( scaled_value ); } diff --git a/src/core/AutomationClip.cpp b/src/core/AutomationClip.cpp index ef57a60d50a..ba2ffe1f38e 100644 --- a/src/core/AutomationClip.cpp +++ b/src/core/AutomationClip.cpp @@ -65,13 +65,13 @@ AutomationClip::AutomationClip( AutomationTrack * _auto_track ) : switch( getTrack()->trackContainer()->type() ) { case TrackContainer::Type::Pattern: - setAutoResize( true ); + setResizable(false); break; case TrackContainer::Type::Song: // move down default: - setAutoResize( false ); + setResizable(true); break; } } @@ -81,14 +81,17 @@ AutomationClip::AutomationClip( AutomationTrack * _auto_track ) : AutomationClip::AutomationClip( const AutomationClip & _clip_to_copy ) : - Clip( _clip_to_copy.m_autoTrack ), + Clip(_clip_to_copy), #if (QT_VERSION < QT_VERSION_CHECK(5,14,0)) m_clipMutex(QMutex::Recursive), #endif m_autoTrack( _clip_to_copy.m_autoTrack ), m_objects( _clip_to_copy.m_objects ), m_tension( _clip_to_copy.m_tension ), - m_progressionType( _clip_to_copy.m_progressionType ) + m_progressionType(_clip_to_copy.m_progressionType), + m_dragging(false), + m_isRecording(_clip_to_copy.m_isRecording), + m_lastRecordedValue(0) { // Locks the mutex of the copied AutomationClip to make sure it // doesn't change while it's being copied @@ -106,13 +109,13 @@ AutomationClip::AutomationClip( const AutomationClip & _clip_to_copy ) : switch( getTrack()->trackContainer()->type() ) { case TrackContainer::Type::Pattern: - setAutoResize( true ); + setResizable(false); break; case TrackContainer::Type::Song: // move down default: - setAutoResize( false ); + setResizable(true); break; } } @@ -225,8 +228,15 @@ TimePos AutomationClip::timeMapLength() const void AutomationClip::updateLength() { - // Do not resize down in case user manually extended up - changeLength(std::max(length(), timeMapLength())); + // Technically it only matters if the clip has been resized from the right, but this + // checks if it has been resized from either direction. + if (getAutoResize()) + { + // Using 1 bar as the min length for an un-resized clip. + // This does not prevent the user from resizing the clip to be less than a bar later on. + changeLength(std::max(TimePos::ticksPerBar(), static_cast(timeMapLength()))); + setStartTimeOffset(TimePos(0)); + } } @@ -253,6 +263,7 @@ TimePos AutomationClip::putValue( cleanObjects(); TimePos newTime = quantPos ? Note::quantized(time, quantization()) : time; + newTime = std::max(TimePos(0), newTime); // Create a node or replace the existing one on newTime m_timeMap[newTime] = AutomationNode(this, value, newTime); @@ -308,6 +319,7 @@ TimePos AutomationClip::putValues( cleanObjects(); TimePos newTime = quantPos ? Note::quantized(time, quantization()) : time; + newTime = std::max(TimePos(0), newTime); // Create a node or replace the existing one on newTime m_timeMap[newTime] = AutomationNode(this, inValue, outValue, newTime); @@ -455,12 +467,12 @@ void AutomationClip::recordValue(TimePos time, float value) if( value != m_lastRecordedValue ) { - putValue( time, value, true ); + putValue(time - startTimeOffset(), value, true); m_lastRecordedValue = value; } - else if( valueAt( time ) != value ) + else if( valueAt(time - startTimeOffset()) != value ) { - removeNode(time); + removeNode(time - startTimeOffset()); } } @@ -721,90 +733,61 @@ void AutomationClip::flipY() -void AutomationClip::flipX(int length) +void AutomationClip::flipX(int start, int end) { QMutexLocker m(&m_clipMutex); - timeMap::const_iterator it = m_timeMap.lowerBound(0); + timeMap::const_iterator firstIterator = m_timeMap.lowerBound(0); + + if (firstIterator == m_timeMap.end()) { return; } - if (it == m_timeMap.end()) { return; } + if (start == -1 && end == -1) { start = 0; end = length() - startTimeOffset(); } + else if (!(end >= 0 && start >= 0 && end > start)) { return; } // Temporary map where we will store the flipped version // of our clip timeMap tempMap; - float tempValue = 0; - float tempOutValue = 0; - - // We know the QMap isn't empty, making this safe: - float realLength = m_timeMap.lastKey(); - - // If we have a positive length, we want to flip the area covered by that - // length, even if it goes beyond the clip. A negative length means that - // we just want to flip the nodes we have - if (length >= 0 && length != realLength) + for (auto it = m_timeMap.begin(); it != m_timeMap.end(); ++it) { - // If length to be flipped is bigger than the real length - if (realLength < length) + if (POS(it) < start || POS(it) > end) { - // We are flipping an area that goes beyond the last node. So we add a node to the - // beginning of the flipped timeMap representing the value of the end of the area - tempValue = valueAt(length); - tempMap[0] = AutomationNode(this, tempValue, 0); - - // Now flip the nodes we have in relation to the length - do - { - // We swap the inValue and outValue when flipping horizontally - tempValue = OUTVAL(it); - tempOutValue = INVAL(it); - auto newTime = TimePos(length - POS(it)); - - tempMap[newTime] = AutomationNode(this, tempValue, tempOutValue, newTime); - - ++it; - } while (it != m_timeMap.end()); + tempMap[POS(it)] = *it; } - else // If the length to be flipped is smaller than the real length + else { - do + // If the first node in the clip is not at the start, it can break things when clipping, so + // we have to set its in value to 0. + if (it == firstIterator && POS(firstIterator) > 0) { - TimePos newTime; - - // Only flips the length to be flipped and keep the remaining values in place - // We also only swap the inValue and outValue if we are flipping the node - if (POS(it) <= length) - { - newTime = length - POS(it); - tempValue = OUTVAL(it); - tempOutValue = INVAL(it); - } - else - { - newTime = POS(it); - tempValue = INVAL(it); - tempOutValue = OUTVAL(it); - } - - tempMap[newTime] = AutomationNode(this, tempValue, tempOutValue, newTime); - - ++it; - } while (it != m_timeMap.end()); + tempMap[end - (POS(it) - start)] = AutomationNode(this, OUTVAL(it), 0, end - (POS(it) - start)); + } + else + { + tempMap[end - (POS(it) - start)] = AutomationNode(this, OUTVAL(it), INVAL(it), end - (POS(it) - start)); + } } } - else // Length to be flipped is the same as the real length - { - do - { - // Swap the inValue and outValue - tempValue = OUTVAL(it); - tempOutValue = INVAL(it); - auto newTime = TimePos(realLength - POS(it)); - tempMap[newTime] = AutomationNode(this, tempValue, tempOutValue, newTime); - - ++it; - } while (it != m_timeMap.end()); + if (m_timeMap.contains(start) && m_timeMap.contains(end)) + { + tempMap[start] = AutomationNode(this, m_timeMap[start].getInValue(), m_timeMap[end].getInValue(), start); + tempMap[end] = AutomationNode(this, m_timeMap[start].getOutValue(), m_timeMap[end].getOutValue(), end); + } + else if (m_timeMap.contains(start)) + { + tempMap[start] = AutomationNode(this, m_timeMap[start].getInValue(), valueAt(end), start); + tempMap[end] = AutomationNode(this, m_timeMap[start].getOutValue(), valueAt(end), end); + } + else if (m_timeMap.contains(end)) + { + tempMap[start] = AutomationNode(this, valueAt(start), m_timeMap[end].getInValue(), start); + tempMap[end] = AutomationNode(this, valueAt(start), m_timeMap[end].getOutValue(), end); + } + else + { + tempMap[start] = AutomationNode(this, valueAt(start), valueAt(end), start); + tempMap[end] = AutomationNode(this, valueAt(start), valueAt(end), end); } m_timeMap.clear(); @@ -830,6 +813,8 @@ void AutomationClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "prog", QString::number( static_cast(progressionType()) ) ); _this.setAttribute( "tens", QString::number( getTension() ) ); _this.setAttribute( "mute", QString::number( isMuted() ) ); + _this.setAttribute("off", startTimeOffset()); + _this.setAttribute("autoresize", QString::number(getAutoResize())); if (const auto& c = color()) { @@ -880,6 +865,8 @@ void AutomationClip::loadSettings( const QDomElement & _this ) "prog" ).toInt() ) ); setTension( _this.attribute( "tens" ) ); setMuted(_this.attribute( "mute", QString::number( false ) ).toInt() ); + setAutoResize(_this.attribute("autoresize").toInt()); + setStartTimeOffset(_this.attribute("off").toInt()); for( QDomNode node = _this.firstChild(); !node.isNull(); node = node.nextSibling() ) diff --git a/src/core/Clip.cpp b/src/core/Clip.cpp index b18391df169..ea7d1f933e9 100644 --- a/src/core/Clip.cpp +++ b/src/core/Clip.cpp @@ -61,7 +61,30 @@ Clip::Clip( Track * track ) : } - +/*! \brief Copy a Clip + * + * Creates a duplicate clip of the one provided. + * + * \param other The clip object which will be copied. + */ +Clip::Clip(const Clip& other): + Model(other.m_track), + m_track(other.m_track), + m_name(other.m_name), + m_startPosition(other.m_startPosition), + m_length(other.m_length), + m_startTimeOffset(other.m_startTimeOffset), + m_mutedModel(other.m_mutedModel.value(), this, tr( "Mute" )), + m_resizable(other.m_resizable), + m_autoResize(other.m_autoResize), + m_selectViewOnCreate{other.m_selectViewOnCreate}, + m_color(other.m_color) +{ + if (getTrack()) + { + getTrack()->addClip(this); + } +} /*! \brief Destroy a Clip * diff --git a/src/core/Note.cpp b/src/core/Note.cpp index ed3a00f1017..167d75f3073 100644 --- a/src/core/Note.cpp +++ b/src/core/Note.cpp @@ -46,12 +46,11 @@ Note::Note( const TimePos & length, const TimePos & pos, m_volume(std::clamp(volume, MinVolume, MaxVolume)), m_panning(std::clamp(panning, PanningLeft, PanningRight)), m_length( length ), - m_pos( pos ), - m_detuning( nullptr ) + m_pos( pos ) { - if( detuning ) + if (detuning) { - m_detuning = sharedObject::ref( detuning ); + m_detuning = std::make_unique(*detuning); } else { @@ -74,24 +73,41 @@ Note::Note( const Note & note ) : m_panning( note.m_panning ), m_length( note.m_length ), m_pos( note.m_pos ), - m_detuning(nullptr), m_type(note.m_type) { - if( note.m_detuning ) + if (note.m_detuning) { - m_detuning = sharedObject::ref( note.m_detuning ); + m_detuning = std::make_unique(*note.m_detuning); } } +Note& Note::operator=(const Note& note) +{ + m_selected = note.m_selected; + m_oldKey = note.m_oldKey; + m_oldPos = note.m_oldPos; + m_oldLength = note.m_oldLength; + m_isPlaying = note.m_isPlaying; + m_key = note.m_key; + m_volume = note.m_volume; + m_panning = note.m_panning; + m_length = note.m_length; + m_pos = note.m_pos; + m_type = note.m_type; + + if (note.m_detuning) + { + m_detuning = std::make_unique(*note.m_detuning); + } + + return *this; +} + Note::~Note() { - if( m_detuning ) - { - sharedObject::unref( m_detuning ); - } } @@ -218,7 +234,7 @@ void Note::createDetuning() { if( m_detuning == nullptr ) { - m_detuning = new DetuningHelper; + m_detuning = std::make_unique(); (void) m_detuning->automationClip(); m_detuning->setRange( -MaxDetuning, MaxDetuning, 0.5f ); m_detuning->automationClip()->setProgressionType( AutomationClip::ProgressionType::Linear ); diff --git a/src/core/PatternClip.cpp b/src/core/PatternClip.cpp index 15a1d1f543d..01bd4833639 100644 --- a/src/core/PatternClip.cpp +++ b/src/core/PatternClip.cpp @@ -45,7 +45,7 @@ PatternClip::PatternClip(Track* track) : changeLength( TimePos( t, 0 ) ); restoreJournallingState(); } - setAutoResize( false ); + setResizable(true); } void PatternClip::saveSettings(QDomDocument& doc, QDomElement& element) @@ -62,6 +62,7 @@ void PatternClip::saveSettings(QDomDocument& doc, QDomElement& element) element.setAttribute( "len", length() ); element.setAttribute("off", startTimeOffset()); element.setAttribute( "muted", isMuted() ); + element.setAttribute("autoresize", QString::number(getAutoResize())); if (const auto& c = color()) { element.setAttribute("color", c->name()); @@ -79,6 +80,7 @@ void PatternClip::loadSettings(const QDomElement& element) movePosition( element.attribute( "pos" ).toInt() ); } changeLength( element.attribute( "len" ).toInt() ); + setAutoResize(element.attribute("autoresize").toInt()); setStartTimeOffset(element.attribute("off").toInt()); if (static_cast(element.attribute("muted").toInt()) != isMuted()) { diff --git a/src/core/SampleClip.cpp b/src/core/SampleClip.cpp index 5ef001e20d1..06e1af99df5 100644 --- a/src/core/SampleClip.cpp +++ b/src/core/SampleClip.cpp @@ -70,13 +70,13 @@ SampleClip::SampleClip(Track* _track, Sample sample, bool isPlaying) switch( getTrack()->trackContainer()->type() ) { case TrackContainer::Type::Pattern: - setAutoResize( true ); + setResizable(false); break; case TrackContainer::Type::Song: // move down default: - setAutoResize( false ); + setResizable(true); break; } updateTrackClips(); @@ -88,8 +88,48 @@ SampleClip::SampleClip(Track* track) } SampleClip::SampleClip(const SampleClip& orig) : - SampleClip(orig.getTrack(), orig.m_sample, orig.m_isPlaying) + Clip(orig), + m_sample(std::move(orig.m_sample)), + m_isPlaying(orig.m_isPlaying) { + saveJournallingState( false ); + setSampleFile( "" ); + restoreJournallingState(); + + // we need to receive bpm-change-events, because then we have to + // change length of this Clip + connect( Engine::getSong(), SIGNAL(tempoChanged(lmms::bpm_t)), + this, SLOT(updateLength()), Qt::DirectConnection ); + connect( Engine::getSong(), SIGNAL(timeSignatureChanged(int,int)), + this, SLOT(updateLength())); + + //playbutton clicked or space key / on Export Song set isPlaying to false + connect( Engine::getSong(), SIGNAL(playbackStateChanged()), + this, SLOT(playbackPositionChanged()), Qt::DirectConnection ); + //care about loops and jumps + connect( Engine::getSong(), SIGNAL(updateSampleTracks()), + this, SLOT(playbackPositionChanged()), Qt::DirectConnection ); + //care about mute Clips + connect( this, SIGNAL(dataChanged()), this, SLOT(playbackPositionChanged())); + //care about mute track + connect( getTrack()->getMutedModel(), SIGNAL(dataChanged()), + this, SLOT(playbackPositionChanged()), Qt::DirectConnection ); + //care about Clip position + connect( this, SIGNAL(positionChanged()), this, SLOT(updateTrackClips())); + + switch( getTrack()->trackContainer()->type() ) + { + case TrackContainer::Type::Pattern: + setResizable(false); + break; + + case TrackContainer::Type::Song: + // move down + default: + setResizable(true); + break; + } + updateTrackClips(); } @@ -267,6 +307,7 @@ void SampleClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) _this.setAttribute( "muted", isMuted() ); _this.setAttribute( "src", sampleFile() ); _this.setAttribute( "off", startTimeOffset() ); + _this.setAttribute("autoresize", QString::number(getAutoResize())); if( sampleFile() == "" ) { QString s; @@ -315,6 +356,7 @@ void SampleClip::loadSettings( const QDomElement & _this ) changeLength( _this.attribute( "len" ).toInt() ); setMuted( _this.attribute( "muted" ).toInt() ); setStartTimeOffset( _this.attribute( "off" ).toInt() ); + setAutoResize(_this.attribute("autoresize").toInt()); if (_this.hasAttribute("color")) { diff --git a/src/core/Song.cpp b/src/core/Song.cpp index ea60e349bb6..6cfa20addff 100644 --- a/src/core/Song.cpp +++ b/src/core/Song.cpp @@ -290,7 +290,7 @@ void Song::processNextBuffer() } else if (m_playMode == PlayMode::MidiClip && m_loopMidiClip && !loopEnabled) { - enforceLoop(TimePos{0}, m_midiClipToPlay->length()); + enforceLoop(-m_midiClipToPlay->startTimeOffset(), m_midiClipToPlay->length() - m_midiClipToPlay->startTimeOffset()); } // Handle loop points, and inform VST plugins of the loop status @@ -660,7 +660,14 @@ void Song::stop() switch (timeline.stopBehaviour()) { case Timeline::StopBehaviour::BackToZero: - getPlayPos().setTicks(0); + if (m_playMode == PlayMode::MidiClip) + { + getPlayPos().setTicks(std::max(0, -m_midiClipToPlay->startTimeOffset())); + } + else + { + getPlayPos().setTicks(0); + } m_elapsedMilliSeconds[static_cast(m_playMode)] = 0; break; diff --git a/src/core/TrackContainer.cpp b/src/core/TrackContainer.cpp index d4120e76174..c92d6edf080 100644 --- a/src/core/TrackContainer.cpp +++ b/src/core/TrackContainer.cpp @@ -297,9 +297,9 @@ AutomatedValueMap TrackContainer::automatedValuesFromTracks(const TrackList &tra if (! p->hasAutomation()) { continue; } - TimePos relTime = time - p->startPosition(); - if (! p->getAutoResize()) { - relTime = std::min(relTime, p->length()); + TimePos relTime = time - p->startPosition() - p->startTimeOffset(); + if (p->getResizable()) { + relTime = std::min(static_cast(relTime), p->length() - p->startTimeOffset()); } float value = p->valueAt(relTime); diff --git a/src/gui/clips/AutomationClipView.cpp b/src/gui/clips/AutomationClipView.cpp index e098710d9c7..89fbd4f8e9b 100644 --- a/src/gui/clips/AutomationClipView.cpp +++ b/src/gui/clips/AutomationClipView.cpp @@ -23,6 +23,8 @@ */ #include "AutomationClipView.h" +#include + #include #include #include @@ -37,6 +39,8 @@ #include "StringPairDrag.h" #include "TextFloat.h" #include "Track.h" +#include "TrackContainerView.h" +#include "TrackView.h" #include "Engine.h" @@ -149,7 +153,7 @@ void AutomationClipView::flipY() void AutomationClipView::flipX() { - m_clip->flipX( m_clip->length() ); + m_clip->flipX(std::max(0, -m_clip->startTimeOffset()), std::max(0, m_clip->length() - m_clip->startTimeOffset())); update(); } @@ -183,6 +187,7 @@ void AutomationClipView::constructContextMenu( QMenu * _cm ) _cm->addAction( embed::getIconPixmap( "flip_x" ), tr( "Flip Horizontally (Visible)" ), this, SLOT(flipX())); + if (!m_clip->m_objects.empty()) { _cm->addSeparator(); @@ -207,6 +212,8 @@ void AutomationClipView::constructContextMenu( QMenu * _cm ) void AutomationClipView::mouseDoubleClickEvent( QMouseEvent * me ) { + if (m_trackView->trackContainerView()->knifeMode()) { return; } + if(me->button() != Qt::LeftButton) { me->ignore(); @@ -269,6 +276,7 @@ void AutomationClipView::paintEvent( QPaintEvent * ) const float y_scale = max - min; const float h = ( height() - 2 * BORDER_WIDTH ) / y_scale; const float ppTick = ppb / TimePos::ticksPerBar(); + const int offset = m_clip->startTimeOffset() * ppTick; p.translate( 0.0f, max * height() / y_scale - BORDER_WIDTH ); p.scale( 1.0f, -h ); @@ -289,7 +297,7 @@ void AutomationClipView::paintEvent( QPaintEvent * ) { if( it+1 == m_clip->getTimeMap().end() ) { - const float x1 = POS(it) * ppTick; + const float x1 = POS(it) * ppTick + offset; const auto x2 = (float)(width() - BORDER_WIDTH); if( x1 > ( width() - BORDER_WIDTH ) ) break; // We are drawing the space after the last node, so we use the outValue @@ -317,20 +325,19 @@ void AutomationClipView::paintEvent( QPaintEvent * ) : INVAL(it + 1); QPainterPath path; - QPointF origin = QPointF(POS(it) * ppTick, 0.0f); - path.moveTo( origin ); - path.moveTo(QPointF(POS(it) * ppTick,values[0])); + QPointF origin = QPointF(POS(it) * ppTick + offset, 0.0f); + path.moveTo(origin); + path.moveTo(QPointF(POS(it) * ppTick + offset, values[0])); for (int i = POS(it) + 1; i < POS(it + 1); i++) { - float x = i * ppTick; - if( x > ( width() - BORDER_WIDTH ) ) break; + float x = i * ppTick + offset; + if(x > (width() - BORDER_WIDTH)) break; float value = values[i - POS(it)]; - path.lineTo( QPointF( x, value ) ); - + path.lineTo(QPointF(x, value)); } - path.lineTo((POS(it + 1)) * ppTick, nextValue); - path.lineTo((POS(it + 1)) * ppTick, 0.0f); - path.lineTo( origin ); + path.lineTo((POS(it + 1)) * ppTick + offset, nextValue); + path.lineTo((POS(it + 1)) * ppTick + offset, 0.0f); + path.lineTo(origin); if( gradient() ) { @@ -355,10 +362,10 @@ void AutomationClipView::paintEvent( QPaintEvent * ) const int bx = BORDER_WIDTH + static_cast(ppb * b) - 2; //top line - p.drawLine(bx, BORDER_WIDTH, bx, BORDER_WIDTH + lineSize); + p.drawLine(bx + offset, BORDER_WIDTH, bx + offset, BORDER_WIDTH + lineSize); //bottom line - p.drawLine(bx, rect().bottom() - (lineSize + BORDER_WIDTH), bx, rect().bottom() - BORDER_WIDTH); + p.drawLine(bx + offset, rect().bottom() - (lineSize + BORDER_WIDTH), bx + offset, rect().bottom() - BORDER_WIDTH); } // recording icon for when recording automation @@ -388,6 +395,12 @@ void AutomationClipView::paintEvent( QPaintEvent * ) p.drawPixmap( spacing, height() - ( size + spacing ), embed::getIconPixmap( "muted", size, size ) ); } + + if (m_marker) + { + p.setPen(markerColor()); + p.drawLine(m_markerPos, rect().bottom(), m_markerPos, rect().top()); + } p.end(); @@ -484,5 +497,4 @@ void AutomationClipView::scaleTimemapToFit( float oldMin, float oldMax ) m_clip->generateTangents(); } - } // namespace lmms::gui diff --git a/src/gui/clips/ClipView.cpp b/src/gui/clips/ClipView.cpp index 6c3953cf59a..89d5a5e79c7 100644 --- a/src/gui/clips/ClipView.cpp +++ b/src/gui/clips/ClipView.cpp @@ -101,6 +101,7 @@ ClipView::ClipView( Clip * clip, m_textShadowColor( 0, 0, 0 ), m_patternClipBackground( 0, 0, 0 ), m_gradient( true ), + m_markerColor(0, 0, 0), m_mouseHotspotHand( 0, 0 ), m_mouseHotspotKnife( 0, 0 ), m_cursorHand( QCursor( embed::getIconPixmap( "hand" ) ) ), @@ -232,6 +233,9 @@ QColor ClipView::patternClipBackground() const bool ClipView::gradient() const { return m_gradient; } +QColor ClipView::markerColor() const +{ return m_markerColor; } + //! \brief CSS theming qproperty access method void ClipView::setMutedColor( const QColor & c ) { m_mutedColor = QColor( c ); } @@ -259,6 +263,9 @@ void ClipView::setPatternClipBackground( const QColor & c ) void ClipView::setGradient( const bool & b ) { m_gradient = b; } +void ClipView::setMarkerColor(const QColor & c) +{ m_markerColor = QColor(c); } + // access needsUpdate member variable bool ClipView::needsUpdate() { return m_needsUpdate; } @@ -494,17 +501,14 @@ void ClipView::dropEvent( QDropEvent * de ) */ void ClipView::updateCursor(QMouseEvent * me) { - auto sClip = dynamic_cast(m_clip); - auto pClip = dynamic_cast(m_clip); - // If we are at the edges, use the resize cursor - if (!me->buttons() && !m_clip->getAutoResize() && !isSelected() - && ((me->x() > width() - RESIZE_GRIP_WIDTH) || (me->x() < RESIZE_GRIP_WIDTH && (sClip || pClip)))) + if (!me->buttons() && m_clip->getResizable() && !isSelected() + && ((me->x() > width() - RESIZE_GRIP_WIDTH) || (me->x() < RESIZE_GRIP_WIDTH))) { setCursor(Qt::SizeHorCursor); } // If we are in the middle on knife mode, use the knife cursor - else if (sClip && m_trackView->trackContainerView()->knifeMode() && !isSelected()) + else if (m_trackView->trackContainerView()->knifeMode() && !isSelected()) { setCursor(m_cursorKnife); } @@ -631,11 +635,9 @@ void ClipView::mousePressEvent( QMouseEvent * me ) setInitialOffsets(); if( !fixedClips() && me->button() == Qt::LeftButton ) { - auto sClip = dynamic_cast(m_clip); - auto pClip = dynamic_cast(m_clip); const bool knifeMode = m_trackView->trackContainerView()->knifeMode(); - if (me->modifiers() & KBD_COPY_MODIFIER && !(sClip && knifeMode)) + if (me->modifiers() & KBD_COPY_MODIFIER && !knifeMode) { if( isSelected() ) { @@ -667,7 +669,7 @@ void ClipView::mousePressEvent( QMouseEvent * me ) setInitialPos( me->pos() ); setInitialOffsets(); - if( m_clip->getAutoResize() ) + if (!m_clip->getResizable() && !knifeMode) { // Always move clips that can't be manually resized m_action = Action::Move; setCursor( Qt::SizeAllCursor ); @@ -677,12 +679,12 @@ void ClipView::mousePressEvent( QMouseEvent * me ) m_action = Action::Resize; setCursor( Qt::SizeHorCursor ); } - else if( me->x() < RESIZE_GRIP_WIDTH && (sClip || pClip) ) + else if (me->x() < RESIZE_GRIP_WIDTH) { m_action = Action::ResizeLeft; setCursor( Qt::SizeHorCursor ); } - else if( sClip && knifeMode ) + else if (knifeMode) { m_action = Action::Split; setCursor( m_cursorKnife ); @@ -725,9 +727,21 @@ void ClipView::mousePressEvent( QMouseEvent * me ) } delete m_hint; - QString hint = m_action == Action::Move || m_action == Action::MoveSelection - ? tr( "Press <%1> and drag to make a copy." ) - : tr( "Press <%1> for free resizing." ); + QString hint; + if (m_action == Action::Move || m_action == Action::MoveSelection) + { + hint = tr("Press <%1> and drag to make a copy."); + } + else if (m_action == Action::Split) + { + hint = dynamic_cast(this) + ? tr("Press <%1> or for unquantized splitting.\nPress for destructive splitting.") + : tr("Press <%1> or for unquantized splitting."); + } + else + { + hint = tr("Press <%1> or for unquantized resizing."); + } m_hint = TextFloat::displayMessage( tr( "Hint" ), hint.arg(UI_COPY_KEY), embed::getIconPixmap( "hint" ), 0 ); } @@ -745,12 +759,8 @@ void ClipView::mousePressEvent( QMouseEvent * me ) if (m_action == Action::Split) { m_action = Action::None; - auto sClip = dynamic_cast(m_clip); - if (sClip) - { - setMarkerEnabled( false ); - update(); - } + setMarkerEnabled(false); + update(); } } else if( me->button() == Qt::MiddleButton ) @@ -893,6 +903,7 @@ void ClipView::mouseMoveEvent( QMouseEvent * me ) setInitialPos( m_initialMousePos ); // Don't resize to less than 1 tick m_clip->changeLength( qMax( 1, l ) ); + m_clip->setAutoResize(false); } else if ( me->modifiers() & Qt::ShiftModifier ) { // If shift is held, quantize clip's end position @@ -901,6 +912,7 @@ void ClipView::mouseMoveEvent( QMouseEvent * me ) TimePos min = m_initialClipPos.quantize( snapSize ); if ( min <= m_initialClipPos ) min += snapLength; m_clip->changeLength( qMax(min - m_initialClipPos, end - m_initialClipPos) ); + m_clip->setAutoResize(false); } else { // Otherwise, resize in fixed increments @@ -910,66 +922,70 @@ void ClipView::mouseMoveEvent( QMouseEvent * me ) auto min = TimePos(initialLength % snapLength); if (min < 1) min += snapLength; m_clip->changeLength( qMax( min, initialLength + offset) ); + m_clip->setAutoResize(false); } } else { - auto sClip = dynamic_cast(m_clip); auto pClip = dynamic_cast(m_clip); - if( sClip || pClip ) + + const int x = mapToParent( me->pos() ).x() - m_initialMousePos.x(); + + TimePos t = qMax( 0, (int) + m_trackView->trackContainerView()->currentPosition() + + static_cast( x * TimePos::ticksPerBar() / ppb ) ); + + if (!isResizableBeforeStart()) { - const int x = mapToParent( me->pos() ).x() - m_initialMousePos.x(); - - TimePos t = qMax( 0, (int) - m_trackView->trackContainerView()->currentPosition() + - static_cast( x * TimePos::ticksPerBar() / ppb ) ); - - if( unquantizedModHeld(me) ) - { // We want to preserve this adjusted offset, - // even if the user switches to snapping later - setInitialPos( m_initialMousePos ); - //Don't resize to less than 1 tick - t = qMin( m_initialClipEnd - 1, t); - } - else if( me->modifiers() & Qt::ShiftModifier ) - { // If shift is held, quantize clip's start position - // Don't let the start position move past the end position - TimePos max = m_initialClipEnd.quantize( snapSize ); - if ( max >= m_initialClipEnd ) max -= snapLength; - t = qMin( max, t.quantize( snapSize ) ); + t = std::max(t, static_cast(m_clip->startPosition() + m_clip->startTimeOffset())); + } + + if( unquantizedModHeld(me) ) + { // We want to preserve this adjusted offset, + // even if the user switches to snapping later + setInitialPos( m_initialMousePos ); + //Don't resize to less than 1 tick + t = qMin( m_initialClipEnd - 1, t); + } + else if( me->modifiers() & Qt::ShiftModifier ) + { // If shift is held, quantize clip's start position + // Don't let the start position move past the end position + TimePos max = m_initialClipEnd.quantize( snapSize ); + if ( max >= m_initialClipEnd ) max -= snapLength; + t = qMin( max, t.quantize( snapSize ) ); + } + else + { // Otherwise, resize in fixed increments + // Don't resize to less than 1 tick + TimePos initialLength = m_initialClipEnd - m_initialClipPos; + auto minLength = TimePos(initialLength % snapLength); + if (minLength < 1) minLength += snapLength; + TimePos offset = TimePos(t - m_initialClipPos).quantize( snapSize ); + t = qMin( m_initialClipEnd - minLength, m_initialClipPos + offset ); + } + + TimePos positionOffset = m_clip->startPosition() - t; + if (m_clip->length() + positionOffset >= 1) + { + m_clip->movePosition(t); + m_clip->changeLength(m_clip->length() + positionOffset); + if (pClip) + { + // Modulus the start time offset as we need it only for offsets + // inside the pattern length. This is done to prevent a value overflow. + // The start time offset may still become larger than the pattern length + // whenever the pattern length decreases without a clip resize following. + // To deal safely with it, always modulus before use. + tick_t patternLength = Engine::patternStore()->lengthOfPattern(pClip->patternIndex()) + * TimePos::ticksPerBar(); + TimePos position = (pClip->startTimeOffset() + positionOffset) % patternLength; + pClip->setStartTimeOffset(position); } else - { // Otherwise, resize in fixed increments - // Don't resize to less than 1 tick - TimePos initialLength = m_initialClipEnd - m_initialClipPos; - auto minLength = TimePos(initialLength % snapLength); - if (minLength < 1) minLength += snapLength; - TimePos offset = TimePos(t - m_initialClipPos).quantize( snapSize ); - t = qMin( m_initialClipEnd - minLength, m_initialClipPos + offset ); - } - - TimePos positionOffset = m_clip->startPosition() - t; - if (m_clip->length() + positionOffset >= 1) { - m_clip->movePosition(t); - m_clip->changeLength(m_clip->length() + positionOffset); - if (sClip) - { - sClip->setStartTimeOffset(sClip->startTimeOffset() + positionOffset); - } - else if (pClip) - { - // Modulus the start time offset as we need it only for offsets - // inside the pattern length. This is done to prevent a value overflow. - // The start time offset may still become larger than the pattern length - // whenever the pattern length decreases without a clip resize following. - // To deal safely with it, always modulus before use. - tick_t patternLength = Engine::patternStore()->lengthOfPattern(pClip->patternIndex()) - * TimePos::ticksPerBar(); - TimePos position = (pClip->startTimeOffset() + positionOffset) % patternLength; - pClip->setStartTimeOffset(position); - } + m_clip->setStartTimeOffset(m_clip->startTimeOffset() + positionOffset); } + m_clip->setAutoResize(false); } } s_textFloat->setText( tr( "%1:%2 (%3:%4 to %5:%6)" ). @@ -986,11 +1002,8 @@ void ClipView::mouseMoveEvent( QMouseEvent * me ) } else if( m_action == Action::Split ) { - auto sClip = dynamic_cast(m_clip); - if (sClip) { - setCursor( m_cursorKnife ); - setMarkerPos( knifeMarkerPos( me ) ); - } + setCursor(m_cursorKnife); + setMarkerPos(knifeMarkerPos(me)); update(); } // None of the actions above, we will just handle the cursor @@ -1027,10 +1040,15 @@ void ClipView::mouseReleaseEvent( QMouseEvent * me ) { const float ppb = m_trackView->trackContainerView()->pixelsPerBar(); const TimePos relPos = me->pos().x() * TimePos::ticksPerBar() / ppb; - splitClip(unquantizedModHeld(me) ? - relPos : - quantizeSplitPos(relPos, me->modifiers() & Qt::ShiftModifier) - ); + if (me->modifiers() & Qt::ShiftModifier) + { + destructiveSplitClip(unquantizedModHeld(me) ? relPos : quantizeSplitPos(relPos)); + } + else + { + splitClip(unquantizedModHeld(me) ? relPos : quantizeSplitPos(relPos)); + } + setMarkerEnabled(false); } m_action = Action::None; @@ -1083,15 +1101,6 @@ void ClipView::contextMenuEvent( QContextMenuEvent * cme ) ? tr("Cut") : tr("Cut selection"), [this](){ contextMenuAction( ContextMenuAction::Cut ); } ); - - if (canMergeSelection(selectedClips)) - { - contextMenu.addAction( - embed::getIconPixmap("edit_merge"), - tr("Merge Selection"), - [this]() { contextMenuAction(ContextMenuAction::Merge); } - ); - } } contextMenu.addAction( @@ -1124,6 +1133,12 @@ void ClipView::contextMenuEvent( QContextMenuEvent * cme ) colorMenu.addAction(tr("Pick random"), this, SLOT(randomizeColor())); contextMenu.addMenu(&colorMenu); + contextMenu.addAction( + m_clip->getAutoResize() ? embed::getIconPixmap("auto_resize_disable") : embed::getIconPixmap("auto_resize"), + m_clip->getAutoResize() ? tr("Disable auto-resize") : tr("Enable auto-resize"), + this, &ClipView::toggleSelectedAutoResize + ); + constructContextMenu( &contextMenu ); contextMenu.exec( QCursor::pos() ); @@ -1152,9 +1167,6 @@ void ClipView::contextMenuAction( ContextMenuAction action ) case ContextMenuAction::Mute: toggleMute( active ); break; - case ContextMenuAction::Merge: - mergeClips(active); - break; } } @@ -1238,102 +1250,19 @@ void ClipView::toggleMute( QVector clipvs ) } } -bool ClipView::canMergeSelection(QVector clipvs) +void ClipView::toggleSelectedAutoResize() { - // Can't merge a single Clip - if (clipvs.size() < 2) { return false; } - - // We check if the owner of the first Clip is an Instrument Track - bool isInstrumentTrack = dynamic_cast(clipvs.at(0)->getTrackView()); - - // Then we create a set with all the Clips owners - std::set ownerTracks; - for (auto clipv: clipvs) { ownerTracks.insert(clipv->getTrackView()); } - - // Can merge if there's only one owner track and it's an Instrument Track - return isInstrumentTrack && ownerTracks.size() == 1; -} - -void ClipView::mergeClips(QVector clipvs) -{ - // Get the track that we are merging Clips in - auto track = dynamic_cast(clipvs.at(0)->getTrackView()->getTrack()); - - if (!track) - { - qWarning("Warning: Couldn't retrieve InstrumentTrack in mergeClips()"); - return; - } - - // For Undo/Redo - track->addJournalCheckPoint(); - track->saveJournallingState(false); - - // Find the earliest position of all the selected ClipVs - const auto earliestClipV = std::min_element(clipvs.constBegin(), clipvs.constEnd(), - [](ClipView* a, ClipView* b) - { - return a->getClip()->startPosition() < - b->getClip()->startPosition(); - } - ); - - const TimePos earliestPos = (*earliestClipV)->getClip()->startPosition(); - - // Create a clip where all notes will be added - auto newMidiClip = dynamic_cast(track->createClip(earliestPos)); - if (!newMidiClip) + const bool newState = !m_clip->getAutoResize(); + std::set journaledTracks; + for (auto clipv: getClickedClips()) { - qWarning("Warning: Failed to convert Clip to MidiClip on mergeClips"); - return; + Clip* clip = clipv->getClip(); + if (journaledTracks.insert(clip->getTrack()).second) { clip->getTrack()->addJournalCheckPoint(); } + clip->setAutoResize(newState); + clip->updateLength(); } - - newMidiClip->saveJournallingState(false); - - // Add the notes and remove the Clips that are being merged - for (auto clipv: clipvs) - { - // Convert ClipV to MidiClipView - auto mcView = dynamic_cast(clipv); - - if (!mcView) - { - qWarning("Warning: Non-MidiClip Clip on InstrumentTrack"); - continue; - } - - const NoteVector& currentClipNotes = mcView->getMidiClip()->notes(); - TimePos mcViewPos = mcView->getMidiClip()->startPosition(); - - for (Note* note: currentClipNotes) - { - Note* newNote = newMidiClip->addNote(*note, false); - TimePos originalNotePos = newNote->pos(); - newNote->setPos(originalNotePos + (mcViewPos - earliestPos)); - } - - // We disable the journalling system before removing, so the - // removal doesn't get added to the undo/redo history - clipv->getClip()->saveJournallingState(false); - // No need to check for nullptr because we check while building the clipvs QVector - clipv->remove(); - } - - // Update length since we might have moved notes beyond the end of the MidiClip length - newMidiClip->updateLength(); - // Rearrange notes because we might have moved them - newMidiClip->rearrangeAllNotes(); - // Restore journalling states now that the operation is finished - newMidiClip->restoreJournallingState(); - track->restoreJournallingState(); - // Update song - Engine::getSong()->setModified(); - getGUI()->songEditor()->update(); } - - - /*! \brief How many pixels a bar takes for this ClipView. * * \return the number of pixels per bar. @@ -1440,7 +1369,7 @@ int ClipView::knifeMarkerPos( QMouseEvent * me ) const float ppb = m_trackView->trackContainerView()->pixelsPerBar(); TimePos midiPos = markerPos * TimePos::ticksPerBar() / ppb; //2: Snap to the correct position, based on modifier keys - midiPos = quantizeSplitPos( midiPos, me->modifiers() & Qt::ShiftModifier ); + midiPos = quantizeSplitPos(midiPos); //3: Convert back to a pixel position return midiPos * ppb / TimePos::ticksPerBar(); } @@ -1449,23 +1378,20 @@ int ClipView::knifeMarkerPos( QMouseEvent * me ) -TimePos ClipView::quantizeSplitPos( TimePos midiPos, bool shiftMode ) +TimePos ClipView::quantizeSplitPos(TimePos midiPos) { const float snapSize = getGUI()->songEditor()->m_editor->getSnapSize(); - if ( shiftMode ) - { //If shift is held we quantize the length of the new left clip... - const TimePos leftPos = midiPos.quantize( snapSize ); - //...or right clip... - const TimePos rightOff = m_clip->length() - midiPos; - const TimePos rightPos = m_clip->length() - rightOff.quantize( snapSize ); - //...whichever gives a position closer to the cursor - if (std::abs(leftPos - midiPos) < std::abs(rightPos - midiPos)) { return leftPos; } - else { return rightPos; } - } - else - { - return TimePos(midiPos + m_initialClipPos).quantize( snapSize ) - m_initialClipPos; - } + // quantize the length of the new left clip... + const TimePos leftPos = midiPos.quantize(snapSize); + //...or right clip... + const TimePos rightOff = m_clip->length() - midiPos; + const TimePos rightPos = m_clip->length() - rightOff.quantize(snapSize); + //...or the global gridlines + const TimePos globalPos = TimePos(midiPos + m_initialClipPos).quantize(snapSize) - m_initialClipPos; + //...whichever gives a position closer to the cursor + if (abs(leftPos - midiPos) <= abs(rightPos - midiPos) && abs(leftPos - midiPos) <= abs(globalPos - midiPos)) { return leftPos; } + else if (abs(rightPos - midiPos) <= abs(leftPos - midiPos) && abs(rightPos - midiPos) <= abs(globalPos - midiPos)) { return rightPos; } + else { return globalPos; } } @@ -1515,4 +1441,30 @@ auto ClipView::hasCustomColor() const -> bool return m_clip->color().has_value() || m_clip->getTrack()->color().has_value(); } +bool ClipView::splitClip(const TimePos pos) +{ + const TimePos splitPos = m_initialClipPos + pos; + + // Don't split if we slid off the Clip or if we're on the clip's start/end + // Cutting at exactly the start/end position would create a zero length + // clip (bad), and a clip the same length as the original one (pointless). + if (splitPos <= m_initialClipPos || splitPos >= m_initialClipEnd) { return false; } + + m_clip->getTrack()->addJournalCheckPoint(); + m_clip->getTrack()->saveJournallingState(false); + + auto rightClip = m_clip->clone(); + + m_clip->changeLength(splitPos - m_initialClipPos); + m_clip->setAutoResize(false); + + rightClip->movePosition(splitPos); + rightClip->changeLength(m_initialClipEnd - splitPos); + rightClip->setStartTimeOffset(m_clip->startTimeOffset() - m_clip->length()); + rightClip->setAutoResize(false); + + m_clip->getTrack()->restoreJournallingState(); + return true; +} + } // namespace lmms::gui diff --git a/src/gui/clips/MidiClipView.cpp b/src/gui/clips/MidiClipView.cpp index b735913e4d0..70f41eb89f0 100644 --- a/src/gui/clips/MidiClipView.cpp +++ b/src/gui/clips/MidiClipView.cpp @@ -33,14 +33,18 @@ #include #include #include +#include #include "AutomationEditor.h" #include "ConfigManager.h" #include "DeprecationHelper.h" #include "GuiApplication.h" +#include "InstrumentTrackView.h" #include "MidiClip.h" #include "PianoRoll.h" #include "RenameDialog.h" +#include "SongEditor.h" +#include "TrackContainerView.h" #include "TrackView.h" namespace lmms::gui @@ -215,6 +219,18 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) _cm->addAction( embed::getIconPixmap( "edit_erase" ), tr( "Clear all notes" ), m_clip, SLOT(clear())); + + if (canMergeSelection(getClickedClips())) + { + _cm->addAction( + embed::getIconPixmap("edit_merge"), + tr("Merge Selection"), + [this]() { mergeClips(getClickedClips()); } + ); + } + + _cm->addAction(embed::getIconPixmap("clear_notes_out_of_bounds"), tr("Clear notes out of bounds"), [this]() { bulkClearNotesOutOfBounds(getClickedClips()); }); + if (!isBeat) { _cm->addAction(embed::getIconPixmap("scale"), tr("Transpose"), this, &MidiClipView::transposeSelection); @@ -242,6 +258,168 @@ void MidiClipView::constructContextMenu( QMenu * _cm ) +bool MidiClipView::canMergeSelection(QVector clipvs) +{ + // Can't merge a single Clip + if (clipvs.size() < 2) { return false; } + + // We check if the owner of the first Clip is an Instrument Track + bool isInstrumentTrack = dynamic_cast(clipvs.at(0)->getTrackView()); + + // Then we create a set with all the Clips owners + std::set ownerTracks; + for (auto clipv: clipvs) { ownerTracks.insert(clipv->getTrackView()); } + + // Can merge if there's only one owner track and it's an Instrument Track + return isInstrumentTrack && ownerTracks.size() == 1; +} + +void MidiClipView::mergeClips(QVector clipvs) +{ + // Get the track that we are merging Clips in + auto track = dynamic_cast(clipvs.at(0)->getTrackView()->getTrack()); + + if (!track) + { + qWarning("Warning: Couldn't retrieve InstrumentTrack in mergeClips()"); + return; + } + + // For Undo/Redo + track->addJournalCheckPoint(); + track->saveJournallingState(false); + + // Find the earliest position of all the selected ClipVs + const auto earliestClipV = std::min_element(clipvs.constBegin(), clipvs.constEnd(), + [](ClipView* a, ClipView* b) + { + return a->getClip()->startPosition() < + b->getClip()->startPosition(); + } + ); + const TimePos earliestPos = (*earliestClipV)->getClip()->startPosition(); + + // Find the latest position of all the selected ClipVs + const auto latestClipV = std::max_element(clipvs.constBegin(), clipvs.constEnd(), + [](ClipView* a, ClipView* b) + { + return a->getClip()->endPosition() < + b->getClip()->endPosition(); + } + ); + const TimePos latestPos = (*latestClipV)->getClip()->endPosition(); + + + // Create a clip where all notes will be added + auto newMidiClip = dynamic_cast(track->createClip(earliestPos)); + if (!newMidiClip) + { + qWarning("Warning: Failed to convert Clip to MidiClip on mergeClips"); + return; + } + + newMidiClip->saveJournallingState(false); + + // Add the notes and remove the Clips that are being merged + for (auto clipv: clipvs) + { + // Convert ClipV to MidiClipView + auto mcView = dynamic_cast(clipv); + + if (!mcView) + { + qWarning("Warning: Non-MidiClip Clip on InstrumentTrack"); + continue; + } + + const NoteVector& currentClipNotes = mcView->getMidiClip()->notes(); + TimePos mcViewPos = mcView->getMidiClip()->startPosition() + mcView->getMidiClip()->startTimeOffset(); + + const TimePos clipStartTime = -mcView->getMidiClip()->startTimeOffset(); + const TimePos clipEndTime = mcView->getMidiClip()->length() - mcView->getMidiClip()->startTimeOffset(); + + for (Note* note: currentClipNotes) + { + const TimePos newNoteStart = std::max(note->pos(), clipStartTime); + const TimePos newNoteEnd = std::min(note->endPos(), clipEndTime); + const TimePos newLength = newNoteEnd - newNoteStart; + if (newLength > 0) + { + Note* newNote = newMidiClip->addNote(*note, false); + newNote->setPos(newNoteStart + (mcViewPos - earliestPos)); + newNote->setLength(newLength); + } + } + + // We disable the journalling system before removing, so the + // removal doesn't get added to the undo/redo history + clipv->getClip()->saveJournallingState(false); + // No need to check for nullptr because we check while building the clipvs QVector + clipv->remove(); + } + + // Update length to extend from the start of the first clip to the end of the last clip + newMidiClip->changeLength(latestPos - earliestPos); + newMidiClip->setAutoResize(false); + // Rearrange notes because we might have moved them + newMidiClip->rearrangeAllNotes(); + // Restore journalling states now that the operation is finished + newMidiClip->restoreJournallingState(); + track->restoreJournallingState(); + // Update song + Engine::getSong()->setModified(); + getGUI()->songEditor()->update(); +} + +void MidiClipView::clearNotesOutOfBounds() +{ + m_clip->getTrack()->addJournalCheckPoint(); + m_clip->getTrack()->saveJournallingState(false); + + auto newClip = new MidiClip(static_cast(m_clip->getTrack())); + newClip->setAutoResize(m_clip->getAutoResize()); + newClip->movePosition(m_clip->startPosition()); + + TimePos startBound = -m_clip->startTimeOffset(); + TimePos endBound = m_clip->length() - m_clip->startTimeOffset(); + + for (Note const* note: m_clip->m_notes) + { + const TimePos newNoteStart = std::max(note->pos(), startBound) - startBound; + const TimePos newNoteEnd = std::min(note->endPos(), endBound) - startBound; + const TimePos newLength = newNoteEnd - newNoteStart; + if (newLength > 0) + { + Note newNote = Note{*note}; + newNote.setPos(newNoteStart); + newNote.setLength(newLength); + newClip->addNote(newNote, false); + } + } + newClip->changeLength(m_clip->length()); + newClip->updateLength(); + + remove(); + m_clip->getTrack()->restoreJournallingState(); +} + +void MidiClipView::bulkClearNotesOutOfBounds(QVector clipvs) +{ + for (auto clipv: clipvs) + { + // Convert ClipV to MidiClipView + auto mcView = dynamic_cast(clipv); + if (!mcView) + { + qWarning("Warning: Non-MidiClip Clip on InstrumentTrack"); + continue; + } + mcView->clearNotesOutOfBounds(); + } + Engine::getSong()->setModified(); + getGUI()->songEditor()->update(); +} + void MidiClipView::mousePressEvent( QMouseEvent * _me ) { @@ -299,6 +477,8 @@ void MidiClipView::mousePressEvent( QMouseEvent * _me ) void MidiClipView::mouseDoubleClickEvent(QMouseEvent *_me) { + if (m_trackView->trackContainerView()->knifeMode()) { return; } + if( _me->button() != Qt::LeftButton ) { _me->ignore(); @@ -442,11 +622,12 @@ void MidiClipView::paintEvent( QPaintEvent * ) // Compute pixels per bar const int baseWidth = fixedClips() ? parentWidget()->width() - 2 * BORDER_WIDTH : width() - BORDER_WIDTH; - const float pixelsPerBar = baseWidth / (float) m_clip->length().getBar(); + const float pixelsPerBar = 1.0f * baseWidth / m_clip->length() * TimePos::ticksPerBar(); + + const int offset = m_clip->startTimeOffset(); // Length of one bar/beat in the [0,1] x [0,1] coordinate system - const float barLength = 1. / m_clip->length().getBar(); - const float tickLength = barLength / TimePos::ticksPerBar(); + const float tickLength = 1.0f / m_clip->length(); const int x_base = BORDER_WIDTH; @@ -608,7 +789,7 @@ void MidiClipView::paintEvent( QPaintEvent * ) int mappedNoteKey = currentNote->key() - minKey; int invertedMappedNoteKey = adjustedNoteRange - mappedNoteKey - 1; - float const noteStartX = currentNote->pos() * tickLength; + float const noteStartX = (currentNote->pos() + offset) * tickLength; float const noteLength = currentNote->length() * tickLength; float const noteStartY = invertedMappedNoteKey * noteHeight; @@ -633,14 +814,15 @@ void MidiClipView::paintEvent( QPaintEvent * ) const int lineSize = 3; p.setPen( c.darker( 200 ) ); - for( bar_t t = 1; t < m_clip->length().getBar(); ++t ) + for(float t = (offset % TimePos::ticksPerBar()) * pixelsPerBar / TimePos::ticksPerBar(); t < m_clip->length(); t += pixelsPerBar) { - p.drawLine( x_base + static_cast( pixelsPerBar * t ) - 1, - BORDER_WIDTH, x_base + static_cast( - pixelsPerBar * t ) - 1, BORDER_WIDTH + lineSize ); - p.drawLine( x_base + static_cast( pixelsPerBar * t ) - 1, + p.drawLine( x_base + t - 1, + BORDER_WIDTH, + x_base + t - 1, + BORDER_WIDTH + lineSize ); + p.drawLine( x_base + t - 1, rect().bottom() - ( lineSize + BORDER_WIDTH ), - x_base + static_cast( pixelsPerBar * t ) - 1, + x_base + t - 1, rect().bottom() - BORDER_WIDTH ); } @@ -670,9 +852,80 @@ void MidiClipView::paintEvent( QPaintEvent * ) p.drawPixmap( spacing, height() - ( size + spacing ), embed::getIconPixmap( "muted", size, size ) ); } + + if (m_marker) + { + p.setPen(markerColor()); + p.drawLine(m_markerPos, rect().bottom(), m_markerPos, rect().top()); + } painter.drawPixmap( 0, 0, m_paintPixmap ); } +bool MidiClipView::destructiveSplitClip(const TimePos pos) +{ + const TimePos splitPos = m_initialClipPos + pos; + const TimePos internalSplitPos = pos - m_clip->startTimeOffset(); + + // Don't split if we slid off the Clip or if we're on the clip's start/end + // Cutting at exactly the start/end position would create a zero length + // clip (bad), and a clip the same length as the original one (pointless). + if (splitPos <= m_initialClipPos || splitPos >= m_initialClipEnd) { return false; } + + m_clip->getTrack()->addJournalCheckPoint(); + m_clip->getTrack()->saveJournallingState(false); + + auto leftClip = m_clip->clone(); + leftClip->clearNotes(); + auto rightClip = m_clip->clone(); + rightClip->clearNotes(); + + for (Note const* note : m_clip->m_notes) + { + if (note->pos() >= internalSplitPos) + { + auto movedNote = Note{*note}; + movedNote.setPos(note->pos() - internalSplitPos); + rightClip->addNote(movedNote, false); + } + else if (note->endPos() > internalSplitPos) + { + auto movedNote = Note{*note}; + movedNote.setPos(0); + movedNote.setLength(note->endPos() - internalSplitPos); + rightClip->addNote(movedNote, false); + } + } + + for (Note const* note : m_clip->m_notes) + { + if (note->endPos() <= internalSplitPos) + { + leftClip->addNote(*note, false); + } + else if (note->pos() < internalSplitPos) + { + auto movedNote = Note{*note}; + movedNote.setLength(internalSplitPos - note->pos()); + leftClip->addNote(movedNote, false); + } + } + + leftClip->movePosition(m_initialClipPos); + leftClip->setAutoResize(false); + leftClip->changeLength(splitPos - m_initialClipPos); + leftClip->setStartTimeOffset(m_clip->startTimeOffset()); + + rightClip->movePosition(splitPos); + rightClip->setAutoResize(false); + rightClip->changeLength(m_initialClipEnd - splitPos); + rightClip->setStartTimeOffset(0); + + remove(); + m_clip->getTrack()->restoreJournallingState(); + return true; +} + + } // namespace lmms::gui diff --git a/src/gui/clips/PatternClipView.cpp b/src/gui/clips/PatternClipView.cpp index bf12440c722..e30c26c1ec8 100644 --- a/src/gui/clips/PatternClipView.cpp +++ b/src/gui/clips/PatternClipView.cpp @@ -34,6 +34,8 @@ #include "PatternClip.h" #include "PatternStore.h" #include "RenameDialog.h" +#include "TrackContainerView.h" +#include "TrackView.h" namespace lmms::gui { @@ -70,6 +72,8 @@ void PatternClipView::constructContextMenu(QMenu* _cm) void PatternClipView::mouseDoubleClickEvent(QMouseEvent*) { + if (m_trackView->trackContainerView()->knifeMode()) { return; } + openInPatternEditor(); } @@ -155,6 +159,12 @@ void PatternClipView::paintEvent(QPaintEvent*) embed::getIconPixmap( "muted", size, size ) ); } + if (m_marker) + { + p.setPen(markerColor()); + p.drawLine(m_markerPos, rect().bottom(), m_markerPos, rect().top()); + } + p.end(); painter.drawPixmap( 0, 0, m_paintPixmap ); @@ -195,5 +205,4 @@ void PatternClipView::update() ClipView::update(); } - } // namespace lmms::gui diff --git a/src/gui/clips/SampleClipView.cpp b/src/gui/clips/SampleClipView.cpp index a420d271a5e..b281e5304db 100644 --- a/src/gui/clips/SampleClipView.cpp +++ b/src/gui/clips/SampleClipView.cpp @@ -37,6 +37,7 @@ #include "SampleThumbnail.h" #include "Song.h" #include "StringPairDrag.h" +#include "TrackContainerView.h" #include "TrackView.h" namespace lmms::gui @@ -185,6 +186,8 @@ void SampleClipView::mouseReleaseEvent(QMouseEvent *_me) void SampleClipView::mouseDoubleClickEvent( QMouseEvent * ) { + if (m_trackView->trackContainerView()->knifeMode()) { return; } + const QString selectedAudioFile = SampleLoader::openAudioFile(); if (selectedAudioFile.isEmpty()) { return; } @@ -327,6 +330,7 @@ void SampleClipView::paintEvent( QPaintEvent * pe ) if ( m_marker ) { + p.setPen(markerColor()); p.drawLine(m_markerPos, rect().bottom(), m_markerPos, rect().top()); } // recording sample tracks is not possible at the moment @@ -371,35 +375,4 @@ void SampleClipView::setAutomationGhost() aEditor->setFocus(); } -//! Split this Clip. -/*! \param pos the position of the split, relative to the start of the clip */ -bool SampleClipView::splitClip( const TimePos pos ) -{ - setMarkerEnabled( false ); - - const TimePos splitPos = m_initialClipPos + pos; - - //Don't split if we slid off the Clip or if we're on the clip's start/end - //Cutting at exactly the start/end position would create a zero length - //clip (bad), and a clip the same length as the original one (pointless). - if ( splitPos > m_initialClipPos && splitPos < m_initialClipEnd ) - { - m_clip->getTrack()->addJournalCheckPoint(); - m_clip->getTrack()->saveJournallingState( false ); - - auto rightClip = new SampleClip(*m_clip); - - m_clip->changeLength( splitPos - m_initialClipPos ); - - rightClip->movePosition( splitPos ); - rightClip->changeLength( m_initialClipEnd - splitPos ); - rightClip->setStartTimeOffset( m_clip->startTimeOffset() - m_clip->length() ); - - m_clip->getTrack()->restoreJournallingState(); - return true; - } - else { return false; } -} - - } // namespace lmms::gui diff --git a/src/gui/editors/AutomationEditor.cpp b/src/gui/editors/AutomationEditor.cpp index 1239da55ed6..4b5e00844a3 100644 --- a/src/gui/editors/AutomationEditor.cpp +++ b/src/gui/editors/AutomationEditor.cpp @@ -102,7 +102,8 @@ AutomationEditor::AutomationEditor() : m_scaleColor(Qt::SolidPattern), m_crossColor(0, 0, 0), m_backgroundShade(0, 0, 0), - m_ghostNoteColor(0, 0, 0) + m_ghostNoteColor(0, 0, 0), + m_outOfBoundsShade(0, 0, 0, 128) { connect( this, SIGNAL(currentClipChanged()), this, SLOT(updateAfterClipChange()), @@ -182,6 +183,7 @@ void AutomationEditor::setCurrentClip(AutomationClip * new_clip ) if (m_clip != nullptr) { connect(m_clip, SIGNAL(dataChanged()), this, SLOT(update())); + connect(m_clip, &AutomationClip::lengthChanged, this, qOverload<>(&QWidget::update)); } emit currentClipChanged(); @@ -1380,6 +1382,22 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) drawAutomationTangents(p, it); } } + + // draw clip bounds overlay + p.fillRect( + xCoordOfTick(m_clip->length() - m_clip->startTimeOffset()), + TOP_MARGIN, + width() - 10, + grid_bottom, + m_outOfBoundsShade + ); + p.fillRect( + 0, + TOP_MARGIN, + xCoordOfTick(-m_clip->startTimeOffset()), + grid_bottom, + m_outOfBoundsShade + ); } else { @@ -1396,7 +1414,7 @@ void AutomationEditor::paintEvent(QPaintEvent * pe ) } // TODO: Get this out of paint event - int l = validClip() ? (int) m_clip->length() : 0; + int l = validClip() ? (int) m_clip->length() - m_clip->startTimeOffset() : 0; // reset scroll-range if( m_leftRightScroll->maximum() != l ) diff --git a/src/gui/editors/PianoRoll.cpp b/src/gui/editors/PianoRoll.cpp index 63d2a81a62c..3e2624a5436 100644 --- a/src/gui/editors/PianoRoll.cpp +++ b/src/gui/editors/PianoRoll.cpp @@ -211,6 +211,7 @@ PianoRoll::PianoRoll() : m_noteBorders( true ), m_ghostNoteBorders( true ), m_backgroundShade( 0, 0, 0 ), + m_outOfBoundsShade(0, 0, 0, 128), m_whiteKeyWidth(WHITE_KEY_WIDTH), m_blackKeyWidth(BLACK_KEY_WIDTH) { @@ -854,7 +855,8 @@ void PianoRoll::setCurrentMidiClip( MidiClip* newMidiClip ) return; } - m_leftRightScroll->setValue( 0 ); + // Scroll horizontally to the start of the clip, minus a bar for aesthetics. + m_leftRightScroll->setValue(std::max(0, -m_midiClip->startTimeOffset() - TimePos::ticksPerBar())); // determine the central key so that we can scroll to it int central_key = 0; @@ -874,6 +876,13 @@ void PianoRoll::setCurrentMidiClip( MidiClip* newMidiClip ) m_startKey = qBound(0, central_key, NumKeys); } + // Make sure the playhead position isn't out of the clip bounds. + Engine::getSong()->getPlayPos(Song::PlayMode::MidiClip).setTicks(std::clamp( + Engine::getSong()->getPlayPos(Song::PlayMode::MidiClip).getTicks(), + std::max(0, -m_midiClip->startTimeOffset()), + m_midiClip->length() - m_midiClip->startTimeOffset() + )); + // resizeEvent() does the rest for us (scrolling, range-checking // of start-notes and so on...) resizeEvent( nullptr ); @@ -891,6 +900,7 @@ void PianoRoll::setCurrentMidiClip( MidiClip* newMidiClip ) connect(m_midiClip->instrumentTrack()->microtuner()->keymapModel(), SIGNAL(dataChanged()), this, SLOT(update())); connect(m_midiClip->instrumentTrack()->microtuner()->keyRangeImportModel(), SIGNAL(dataChanged()), this, SLOT(update())); + connect(m_midiClip, &MidiClip::lengthChanged, this, qOverload<>(&QWidget::update)); update(); emit currentMidiClipChanged(); @@ -3194,6 +3204,12 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) // G-1 is one of the widest; plus one pixel margin for the shadow QRect const boundingRect = fontMetrics.boundingRect(QString("G-1")) + QMargins(0, 0, 1, 0); + auto xCoordOfTick = [this](int tick) { + return m_whiteKeyWidth + ( + (tick - m_currentPosition) * m_ppb / TimePos::ticksPerBar() + ); + }; + // Order of drawing // - vertical quantization lines // - piano roll + horizontal key lines @@ -3268,11 +3284,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) // allow quantization grid up to 1/32 for normal notes else if (q < 6) { q = 6; } } - auto xCoordOfTick = [this](int tick) { - return m_whiteKeyWidth + ( - (tick - m_currentPosition) * m_ppb / TimePos::ticksPerBar() - ); - }; + p.setPen(m_lineColor); for (tick = m_currentPosition - m_currentPosition % q, x = xCoordOfTick(tick); @@ -3702,13 +3714,25 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) } } + // draw clip bounds + p.fillRect( + xCoordOfTick(m_midiClip->length() - m_midiClip->startTimeOffset()), + PR_TOP_MARGIN, + width() - 10, + noteEditBottom(), + m_outOfBoundsShade + ); + p.fillRect( + 0, + PR_TOP_MARGIN, + xCoordOfTick(-m_midiClip->startTimeOffset()), + noteEditBottom(), + m_outOfBoundsShade + ); + // -- Knife tool (draw cut line) if (m_action == Action::Knife && m_knifeDown) { - auto xCoordOfTick = [this](int tick) { - return m_whiteKeyWidth + ( - (tick - m_currentPosition) * m_ppb / TimePos::ticksPerBar()); - }; int x1 = xCoordOfTick(m_knifeStartTickPos); int y1 = y_base - (m_knifeStartKey - m_startKey + 1) * m_keyLineHeight; int x2 = xCoordOfTick(m_knifeEndTickPos); @@ -3787,7 +3811,7 @@ void PianoRoll::paintEvent(QPaintEvent * pe ) p.drawRect(x + m_whiteKeyWidth, y, w, h); // TODO: Get this out of paint event - int l = ( hasValidMidiClip() )? (int) m_midiClip->length() : 0; + int l = ( hasValidMidiClip() )? (int) m_midiClip->length() - m_midiClip->startTimeOffset() : 0; // reset scroll-range if( m_leftRightScroll->maximum() != l ) diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 72ee28bc82a..49f620b689c 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -959,7 +959,7 @@ SongEditorWindow::SongEditorWindow(Song* song) : m_editModeGroup = new ActionGroup(this); m_drawModeAction = m_editModeGroup->addAction(embed::getIconPixmap("edit_draw"), tr("Draw mode")); - m_knifeModeAction = m_editModeGroup->addAction(embed::getIconPixmap("edit_knife"), tr("Knife mode (split sample clips)")); + m_knifeModeAction = m_editModeGroup->addAction(embed::getIconPixmap("edit_knife"), tr("Knife mode (split clips)")); m_selectModeAction = m_editModeGroup->addAction(embed::getIconPixmap("edit_select"), tr("Edit mode (select and move)")); m_drawModeAction->setChecked(true); diff --git a/src/tracks/InstrumentTrack.cpp b/src/tracks/InstrumentTrack.cpp index 0d97d500cc8..8370807aecd 100644 --- a/src/tracks/InstrumentTrack.cpp +++ b/src/tracks/InstrumentTrack.cpp @@ -751,7 +751,7 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, TimePos cur_start = _start; if( _clip_num < 0 ) { - cur_start -= c->startPosition(); + cur_start -= c->startPosition() + c->startTimeOffset(); } // get all notes from the given clip... @@ -762,25 +762,33 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, // very effective algorithm for playing notes that are // posated within the current sample-frame - if( cur_start > 0 ) { - // skip notes which are posated before start-bar - while( nit != notes.end() && ( *nit )->pos() < cur_start ) + // skip notes which end before start-bar + while( nit != notes.end() && ( *nit )->endPos() < cur_start ) { ++nit; } } - while (nit != notes.end() && (*nit)->pos() == cur_start) + while (nit != notes.end() && (*nit)->pos() < c->length() - c->startTimeOffset()) { const auto currentNote = *nit; + // Skip any notes note at the current time pos or not overlapping with the start. + if (!(currentNote->pos() == cur_start + || (cur_start == -c->startTimeOffset() && (*nit)->pos() < cur_start && (*nit)->endPos() > cur_start))) + { + ++nit; + continue; + } + // Calculate the overlap of the note over the clip end. + const auto noteOverlap = std::max(0, currentNote->endPos() - (c->length() - c->startTimeOffset())); // If the note is a Step Note, frames will be 0 so the NotePlayHandle // plays for the whole length of the sample const auto noteFrames = currentNote->type() == Note::Type::Step ? 0 - : currentNote->length().frames(frames_per_tick); + : (currentNote->endPos() - cur_start - noteOverlap) * frames_per_tick; NotePlayHandle* notePlayHandle = NotePlayHandleManager::acquire(this, _offset, noteFrames, *currentNote); notePlayHandle->setPatternTrack(pattern_track); @@ -789,7 +797,7 @@ bool InstrumentTrack::play( const TimePos & _start, const fpp_t _frames, { // then set song-global offset of clip in order to // properly perform the note detuning - notePlayHandle->setSongGlobalParentOffset( c->startPosition() ); + notePlayHandle->setSongGlobalParentOffset( c->startPosition() + c->startTimeOffset()); } Engine::audioEngine()->addPlayHandle( notePlayHandle ); diff --git a/src/tracks/MidiClip.cpp b/src/tracks/MidiClip.cpp index ab532168746..55229507c82 100644 --- a/src/tracks/MidiClip.cpp +++ b/src/tracks/MidiClip.cpp @@ -48,16 +48,20 @@ MidiClip::MidiClip( InstrumentTrack * _instrument_track ) : if (_instrument_track->trackContainer() == Engine::patternStore()) { resizeToFirstTrack(); + setResizable(false); + } + else + { + setResizable(true); } init(); - setAutoResize( true ); } MidiClip::MidiClip( const MidiClip& other ) : - Clip( other.m_instrumentTrack ), + Clip(other), m_instrumentTrack( other.m_instrumentTrack ), m_clipType( other.m_clipType ), m_steps( other.m_steps ) @@ -71,13 +75,13 @@ MidiClip::MidiClip( const MidiClip& other ) : switch( getTrack()->trackContainer()->type() ) { case TrackContainer::Type::Pattern: - setAutoResize( true ); + setResizable(false); break; case TrackContainer::Type::Song: // move down default: - setAutoResize( false ); + setResizable(true); break; } } @@ -145,18 +149,24 @@ void MidiClip::updateLength() return; } - tick_t max_length = TimePos::ticksPerBar(); - - for (const auto& note : m_notes) + // If the clip has already been manually resized, don't automatically resize it. + // Unless we are in a pattern, where you can't resize stuff manually + if (getAutoResize() || !getResizable()) { - if (note->length() > 0) + tick_t max_length = TimePos::ticksPerBar(); + + for (const auto& note : m_notes) { - max_length = std::max(max_length, note->endPos()); + if (note->length() > 0) + { + max_length = std::max(max_length, note->endPos()); + } } + changeLength( TimePos( max_length ).nextFullBar() * + TimePos::ticksPerBar() ); + setStartTimeOffset(TimePos(0)); + updatePatternTrack(); } - changeLength( TimePos( max_length ).nextFullBar() * - TimePos::ticksPerBar() ); - updatePatternTrack(); } @@ -416,6 +426,8 @@ void MidiClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) { _this.setAttribute( "type", static_cast(m_clipType) ); _this.setAttribute( "name", name() ); + _this.setAttribute("autoresize", QString::number(getAutoResize())); + _this.setAttribute("off", startTimeOffset()); if (const auto& c = color()) { @@ -435,6 +447,7 @@ void MidiClip::saveSettings( QDomDocument & _doc, QDomElement & _this ) } _this.setAttribute( "muted", isMuted() ); _this.setAttribute( "steps", m_steps ); + _this.setAttribute( "len", length() ); // now save settings of all notes for (auto& note : m_notes) @@ -488,7 +501,20 @@ void MidiClip::loadSettings( const QDomElement & _this ) } checkType(); - updateLength(); + + int len = _this.attribute("len").toInt(); + if (len <= 0) + { + // TODO: Handle with an upgrade method + updateLength(); + } + else + { + changeLength(len); + } + + setAutoResize(_this.attribute("autoresize").toInt()); + setStartTimeOffset(_this.attribute("off").toInt()); emit dataChanged(); }