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:
regulus79
2025-04-29 17:56:13 -04:00
committed by GitHub
parent c91c81eee7
commit 679f9b848e
40 changed files with 868 additions and 422 deletions

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 630 B

View File

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

View File

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

View File

@@ -75,6 +75,8 @@ private:
QStaticText m_staticTextName;
void scaleTimemapToFit( float oldMin, float oldMax );
bool isResizableBeforeStart() override { return false; }
} ;

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,10 @@ public:
InlineAutomation()
{
}
DetuningHelper(const DetuningHelper& _copy) :
InlineAutomation(_copy)
{
}
~DetuningHelper() override = default;

View File

@@ -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;
} ;

View File

@@ -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:

View File

@@ -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;
} ;

View File

@@ -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;
};

View File

@@ -51,6 +51,11 @@ public:
gui::ClipView * createView( gui::TrackView * _tv ) override;
PatternClip* clone() override
{
return new PatternClip(*this);
}
private:
friend class PatternClipView;
} ;

View File

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

View File

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

View File

@@ -68,7 +68,6 @@ private:
SampleThumbnail m_sampleThumbnail;
QPixmap m_paintPixmap;
long m_paintPixmapXPosition;
bool splitClip( const TimePos pos ) override;
} ;

View File

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

View File

@@ -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() )

View File

@@ -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
*

View File

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

View File

@@ -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())
{

View File

@@ -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"))
{

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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();
}