mirror of
https://github.com/LMMS/lmms.git
synced 2025-12-23 22:58:33 -05:00
Add splitting (and resizing) to all types of clips (#7477)
Allow for splitting and resizing all types of clips (automation, MIDI, sample, pattern, etc) using the knife tool in the Song Editor. --------- Co-authored-by: szeli1 <143485814+szeli1@users.noreply.github.com> Co-authored-by: Dalton Messmer <messmer.dalton@gmail.com>
This commit is contained in:
BIN
data/themes/classic/auto_resize.png
Normal file
BIN
data/themes/classic/auto_resize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 920 B |
BIN
data/themes/classic/auto_resize_disable.png
Normal file
BIN
data/themes/classic/auto_resize_disable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
data/themes/classic/clear_notes_out_of_bounds.png
Normal file
BIN
data/themes/classic/clear_notes_out_of_bounds.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -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;
|
||||
|
||||
BIN
data/themes/default/auto_resize.png
Normal file
BIN
data/themes/default/auto_resize.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 633 B |
BIN
data/themes/default/auto_resize_disable.png
Normal file
BIN
data/themes/default/auto_resize_disable.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 656 B |
BIN
data/themes/default/clear_notes_out_of_bounds.png
Normal file
BIN
data/themes/default/clear_notes_out_of_bounds.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 630 B |
@@ -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;
|
||||
|
||||
@@ -68,7 +68,6 @@ public:
|
||||
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 @@ public:
|
||||
void setTension( QString _new_tension );
|
||||
|
||||
TimePos timeMapLength() const;
|
||||
void updateLength();
|
||||
void updateLength() override;
|
||||
|
||||
TimePos putValue(
|
||||
const TimePos & time,
|
||||
@@ -196,12 +195,22 @@ public:
|
||||
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();
|
||||
|
||||
@@ -75,6 +75,8 @@ private:
|
||||
|
||||
QStaticText m_staticTextName;
|
||||
void scaleTimemapToFit( float oldMin, float oldMax );
|
||||
|
||||
bool isResizableBeforeStart() override { return false; }
|
||||
} ;
|
||||
|
||||
|
||||
|
||||
@@ -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 @@ private:
|
||||
QColor m_ghostNoteColor;
|
||||
QColor m_detuningNoteColor;
|
||||
QColor m_ghostSampleColor;
|
||||
QColor m_outOfBoundsShade;
|
||||
|
||||
SampleThumbnail m_sampleThumbnail;
|
||||
|
||||
|
||||
@@ -100,12 +100,28 @@ public:
|
||||
* 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 @@ public:
|
||||
|
||||
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 @@ public:
|
||||
// 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 @@ signals:
|
||||
void destroyedClip();
|
||||
void colorChanged();
|
||||
|
||||
protected:
|
||||
Clip(const Clip& other);
|
||||
|
||||
private:
|
||||
Track * m_track;
|
||||
@@ -158,7 +183,8 @@ private:
|
||||
|
||||
BoolModel m_mutedModel;
|
||||
BoolModel m_soloModel;
|
||||
bool m_autoResize;
|
||||
bool m_resizable = true;
|
||||
bool m_autoResize = true;
|
||||
|
||||
bool m_selectViewOnCreate;
|
||||
|
||||
|
||||
@@ -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 @@ public:
|
||||
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 @@ public:
|
||||
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 @@ public:
|
||||
// some metadata to be written to the clipboard.
|
||||
static void remove( QVector<ClipView *> clipvs );
|
||||
static void toggleMute( QVector<ClipView *> clipvs );
|
||||
static void mergeClips(QVector<ClipView*> clipvs);
|
||||
|
||||
// Returns true if selection can be merged and false if not
|
||||
static bool canMergeSelection(QVector<ClipView*> clipvs);
|
||||
void toggleSelectedAutoResize();
|
||||
|
||||
QColor getColorForDisplay( QColor );
|
||||
|
||||
@@ -147,8 +148,7 @@ protected:
|
||||
Cut,
|
||||
Copy,
|
||||
Paste,
|
||||
Mute,
|
||||
Merge
|
||||
Mute
|
||||
};
|
||||
|
||||
TrackView * m_trackView;
|
||||
@@ -176,7 +176,7 @@ protected:
|
||||
}
|
||||
|
||||
bool unquantizedModHeld( QMouseEvent * me );
|
||||
TimePos quantizeSplitPos( TimePos, bool shiftMode );
|
||||
TimePos quantizeSplitPos(TimePos);
|
||||
|
||||
float pixelsPerBar();
|
||||
|
||||
@@ -224,6 +224,7 @@ private:
|
||||
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 @@ private:
|
||||
TimePos draggedClipPos( QMouseEvent * me );
|
||||
int knifeMarkerPos( QMouseEvent * me );
|
||||
void setColor(const std::optional<QColor>& 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);
|
||||
} ;
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ public:
|
||||
InlineAutomation()
|
||||
{
|
||||
}
|
||||
DetuningHelper(const DetuningHelper& _copy) :
|
||||
InlineAutomation(_copy)
|
||||
{
|
||||
}
|
||||
|
||||
~DetuningHelper() override = default;
|
||||
|
||||
|
||||
@@ -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<float>()),
|
||||
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 @@ public:
|
||||
{
|
||||
if( m_autoClip == nullptr )
|
||||
{
|
||||
m_autoClip = new AutomationClip( nullptr );
|
||||
m_autoClip = std::make_unique<AutomationClip>(nullptr);
|
||||
m_autoClip->addObject( this );
|
||||
}
|
||||
return m_autoClip;
|
||||
return m_autoClip.get();
|
||||
}
|
||||
|
||||
void saveSettings( QDomDocument & _doc, QDomElement & _parent ) override;
|
||||
@@ -92,7 +94,7 @@ public:
|
||||
|
||||
|
||||
private:
|
||||
AutomationClip * m_autoClip;
|
||||
std::unique_ptr<AutomationClip> m_autoClip;
|
||||
|
||||
} ;
|
||||
|
||||
|
||||
@@ -53,12 +53,11 @@ public:
|
||||
} ;
|
||||
|
||||
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 );
|
||||
@@ -117,6 +116,11 @@ public:
|
||||
|
||||
gui::ClipView * createView( gui::TrackView * _tv ) override;
|
||||
|
||||
MidiClip* clone() override
|
||||
{
|
||||
return new MidiClip(*this);
|
||||
}
|
||||
|
||||
|
||||
using Model::dataChanged;
|
||||
|
||||
@@ -127,6 +131,7 @@ public slots:
|
||||
void clear();
|
||||
|
||||
protected:
|
||||
MidiClip( const MidiClip& other );
|
||||
void updatePatternTrack();
|
||||
|
||||
protected slots:
|
||||
|
||||
@@ -63,6 +63,11 @@ public:
|
||||
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<ClipView*> clipvs);
|
||||
static void mergeClips(QVector<ClipView*> clipvs);
|
||||
static void bulkClearNotesOutOfBounds(QVector<ClipView*> 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 @@ private:
|
||||
QStaticText m_staticTextName;
|
||||
|
||||
bool m_legacySEPattern;
|
||||
|
||||
bool isResizableBeforeStart() override { return false; }
|
||||
|
||||
bool destructiveSplitClip(const TimePos pos) override;
|
||||
} ;
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#ifndef LMMS_NOTE_H
|
||||
#define LMMS_NOTE_H
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -107,6 +108,8 @@ public:
|
||||
Note( const Note & note );
|
||||
~Note() override;
|
||||
|
||||
Note& operator=(const Note& note);
|
||||
|
||||
// Note types
|
||||
enum class Type
|
||||
{
|
||||
@@ -236,7 +239,7 @@ public:
|
||||
|
||||
DetuningHelper * detuning() const
|
||||
{
|
||||
return m_detuning;
|
||||
return m_detuning.get();
|
||||
}
|
||||
bool hasDetuningInfo() const;
|
||||
bool withinRange(int tickStart, int tickEnd) const;
|
||||
@@ -262,7 +265,7 @@ private:
|
||||
panning_t m_panning;
|
||||
TimePos m_length;
|
||||
TimePos m_pos;
|
||||
DetuningHelper * m_detuning;
|
||||
std::unique_ptr<DetuningHelper> m_detuning;
|
||||
|
||||
Type m_type = Type::Regular;
|
||||
};
|
||||
|
||||
@@ -51,6 +51,11 @@ public:
|
||||
|
||||
gui::ClipView * createView( gui::TrackView * _tv ) override;
|
||||
|
||||
PatternClip* clone() override
|
||||
{
|
||||
return new PatternClip(*this);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class PatternClipView;
|
||||
} ;
|
||||
|
||||
@@ -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)
|
||||
@@ -516,6 +517,7 @@ private:
|
||||
bool m_noteBorders;
|
||||
bool m_ghostNoteBorders;
|
||||
QColor m_backgroundShade;
|
||||
QColor m_outOfBoundsShade;
|
||||
/* white key properties */
|
||||
int m_whiteKeyWidth;
|
||||
QColor m_whiteKeyActiveTextColor;
|
||||
|
||||
@@ -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 @@ public:
|
||||
void setIsPlaying(bool isPlaying);
|
||||
void setSampleBuffer(std::shared_ptr<const SampleBuffer> 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;
|
||||
|
||||
@@ -68,7 +68,6 @@ private:
|
||||
SampleThumbnail m_sampleThumbnail;
|
||||
QPixmap m_paintPixmap;
|
||||
long m_paintPixmapXPosition;
|
||||
bool splitClip( const TimePos pos ) override;
|
||||
} ;
|
||||
|
||||
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
|
||||
@@ -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<tick_t>(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 (it == m_timeMap.end()) { return; }
|
||||
if (firstIterator == 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
|
||||
|
||||
if (m_timeMap.contains(start) && m_timeMap.contains(end))
|
||||
{
|
||||
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());
|
||||
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<int>(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() )
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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<DetuningHelper>(*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<DetuningHelper>(*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<DetuningHelper>(*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<DetuningHelper>();
|
||||
(void) m_detuning->automationClip();
|
||||
m_detuning->setRange( -MaxDetuning, MaxDetuning, 0.5f );
|
||||
m_detuning->automationClip()->setProgressionType( AutomationClip::ProgressionType::Linear );
|
||||
|
||||
@@ -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<bool>(element.attribute("muted").toInt()) != isMuted())
|
||||
{
|
||||
|
||||
@@ -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"))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -663,7 +663,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<std::size_t>(m_playMode)] = 0;
|
||||
break;
|
||||
|
||||
|
||||
@@ -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<int>(relTime), p->length() - p->startTimeOffset());
|
||||
}
|
||||
float value = p->valueAt(relTime);
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
*/
|
||||
#include "AutomationClipView.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
@@ -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<int>(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
|
||||
|
||||
@@ -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<SampleClip*>(m_clip);
|
||||
auto pClip = dynamic_cast<PatternClip*>(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<SampleClip*>(m_clip);
|
||||
auto pClip = dynamic_cast<PatternClip*>(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<MidiClipView*>(this)
|
||||
? tr("Press <%1> or <Alt> for unquantized splitting.\nPress <Shift> for destructive splitting.")
|
||||
: tr("Press <%1> or <Alt> for unquantized splitting.");
|
||||
}
|
||||
else
|
||||
{
|
||||
hint = tr("Press <%1> or <Alt> 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<SampleClip*>(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<int>( 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<int>(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<int>( min, initialLength + offset) );
|
||||
m_clip->setAutoResize(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
auto sClip = dynamic_cast<SampleClip*>(m_clip);
|
||||
auto pClip = dynamic_cast<PatternClip*>(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<int>( x * TimePos::ticksPerBar() / ppb ) );
|
||||
|
||||
if (!isResizableBeforeStart())
|
||||
{
|
||||
const int x = mapToParent( me->pos() ).x() - m_initialMousePos.x();
|
||||
t = std::max(t, static_cast<TimePos>(m_clip->startPosition() + m_clip->startTimeOffset()));
|
||||
}
|
||||
|
||||
TimePos t = qMax( 0, (int)
|
||||
m_trackView->trackContainerView()->currentPosition() +
|
||||
static_cast<int>( 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<int>( 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<int>( 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<int>( m_initialClipEnd - minLength, m_initialClipPos + offset );
|
||||
}
|
||||
|
||||
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<int>( 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<int>( max, t.quantize( snapSize ) );
|
||||
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<int>( 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<SampleClip*>(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<ClipView *> clipvs )
|
||||
}
|
||||
}
|
||||
|
||||
bool ClipView::canMergeSelection(QVector<ClipView*> 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<InstrumentTrackView*>(clipvs.at(0)->getTrackView());
|
||||
|
||||
// Then we create a set with all the Clips owners
|
||||
std::set<TrackView*> 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;
|
||||
const bool newState = !m_clip->getAutoResize();
|
||||
std::set<Track*> journaledTracks;
|
||||
for (auto clipv: getClickedClips())
|
||||
{
|
||||
Clip* clip = clipv->getClip();
|
||||
if (journaledTracks.insert(clip->getTrack()).second) { clip->getTrack()->addJournalCheckPoint(); }
|
||||
clip->setAutoResize(newState);
|
||||
clip->updateLength();
|
||||
}
|
||||
}
|
||||
|
||||
void ClipView::mergeClips(QVector<ClipView*> clipvs)
|
||||
{
|
||||
// Get the track that we are merging Clips in
|
||||
auto track = dynamic_cast<InstrumentTrack*>(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<MidiClip*>(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<MidiClipView*>(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
|
||||
|
||||
@@ -33,14 +33,18 @@
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
|
||||
#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<ClipView*> 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<InstrumentTrackView*>(clipvs.at(0)->getTrackView());
|
||||
|
||||
// Then we create a set with all the Clips owners
|
||||
std::set<TrackView*> 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<ClipView*> clipvs)
|
||||
{
|
||||
// Get the track that we are merging Clips in
|
||||
auto track = dynamic_cast<InstrumentTrack*>(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<MidiClip*>(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<MidiClipView*>(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<InstrumentTrack*>(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<ClipView*> clipvs)
|
||||
{
|
||||
for (auto clipv: clipvs)
|
||||
{
|
||||
// Convert ClipV to MidiClipView
|
||||
auto mcView = dynamic_cast<MidiClipView*>(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<int>( pixelsPerBar * t ) - 1,
|
||||
BORDER_WIDTH, x_base + static_cast<int>(
|
||||
pixelsPerBar * t ) - 1, BORDER_WIDTH + lineSize );
|
||||
p.drawLine( x_base + static_cast<int>( 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<int>( 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
@@ -1381,6 +1383,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
|
||||
{
|
||||
@@ -1397,7 +1415,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 )
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -868,7 +869,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;
|
||||
@@ -888,6 +890,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 );
|
||||
@@ -905,6 +914,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();
|
||||
@@ -3209,6 +3219,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
|
||||
@@ -3283,11 +3299,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);
|
||||
@@ -3717,13 +3729,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);
|
||||
@@ -3802,7 +3826,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 )
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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<tick_t>(max_length, note->endPos());
|
||||
if (note->length() > 0)
|
||||
{
|
||||
max_length = std::max<tick_t>(max_length, note->endPos());
|
||||
}
|
||||
}
|
||||
changeLength( TimePos( max_length ).nextFullBar() *
|
||||
TimePos::ticksPerBar() );
|
||||
setStartTimeOffset(TimePos(0));
|
||||
updatePatternTrack();
|
||||
}
|
||||
changeLength( TimePos( max_length ).nextFullBar() *
|
||||
TimePos::ticksPerBar() );
|
||||
updatePatternTrack();
|
||||
}
|
||||
|
||||
|
||||
@@ -435,6 +445,8 @@ void MidiClip::saveSettings( QDomDocument & _doc, QDomElement & _this )
|
||||
{
|
||||
_this.setAttribute( "type", static_cast<int>(m_clipType) );
|
||||
_this.setAttribute( "name", name() );
|
||||
_this.setAttribute("autoresize", QString::number(getAutoResize()));
|
||||
_this.setAttribute("off", startTimeOffset());
|
||||
|
||||
if (const auto& c = color())
|
||||
{
|
||||
@@ -454,6 +466,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)
|
||||
@@ -507,7 +520,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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user