mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-04 14:18:58 -05:00
1505 lines
43 KiB
C++
1505 lines
43 KiB
C++
/*
|
|
* TrackContentObjectView.cpp - implementation of TrackContentObjectView class
|
|
*
|
|
* Copyright (c) 2004-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
|
|
*
|
|
* This file is part of LMMS - https://lmms.io
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public
|
|
* License along with this program (see COPYING); if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
* Boston, MA 02110-1301 USA.
|
|
*
|
|
*/
|
|
|
|
#include "TrackContentObjectView.h"
|
|
|
|
#include <set>
|
|
|
|
#include <QMenu>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
|
|
#include "AutomationPattern.h"
|
|
#include "Clipboard.h"
|
|
#include "ColorChooser.h"
|
|
#include "ComboBoxModel.h"
|
|
#include "DataFile.h"
|
|
#include "Engine.h"
|
|
#include "embed.h"
|
|
#include "GuiApplication.h"
|
|
#include "InstrumentTrack.h"
|
|
#include "InstrumentTrackView.h"
|
|
#include "Note.h"
|
|
#include "Pattern.h"
|
|
#include "SampleTrack.h"
|
|
#include "Song.h"
|
|
#include "SongEditor.h"
|
|
#include "StringPairDrag.h"
|
|
#include "TextFloat.h"
|
|
#include "TrackContainer.h"
|
|
#include "TrackContainerView.h"
|
|
#include "TrackView.h"
|
|
|
|
|
|
/*! The width of the resize grip in pixels
|
|
*/
|
|
const int RESIZE_GRIP_WIDTH = 4;
|
|
|
|
|
|
/*! A pointer for that text bubble used when moving segments, etc.
|
|
*
|
|
* In a number of situations, LMMS displays a floating text bubble
|
|
* beside the cursor as you move or resize elements of a track about.
|
|
* This pointer keeps track of it, as you only ever need one at a time.
|
|
*/
|
|
TextFloat * TrackContentObjectView::s_textFloat = nullptr;
|
|
|
|
|
|
/*! \brief Create a new trackContentObjectView
|
|
*
|
|
* Creates a new track content object view for the given
|
|
* track content object in the given track view.
|
|
*
|
|
* \param _tco The track content object to be displayed
|
|
* \param _tv The track view that will contain the new object
|
|
*/
|
|
TrackContentObjectView::TrackContentObjectView( TrackContentObject * tco,
|
|
TrackView * tv ) :
|
|
selectableObject( tv->getTrackContentWidget() ),
|
|
ModelView( nullptr, this ),
|
|
m_trackView( tv ),
|
|
m_initialTCOPos( TimePos(0) ),
|
|
m_initialTCOEnd( TimePos(0) ),
|
|
m_tco( tco ),
|
|
m_action( NoAction ),
|
|
m_initialMousePos( QPoint( 0, 0 ) ),
|
|
m_initialMouseGlobalPos( QPoint( 0, 0 ) ),
|
|
m_initialOffsets( QVector<TimePos>() ),
|
|
m_hint( nullptr ),
|
|
m_mutedColor( 0, 0, 0 ),
|
|
m_mutedBackgroundColor( 0, 0, 0 ),
|
|
m_selectedColor( 0, 0, 0 ),
|
|
m_textColor( 0, 0, 0 ),
|
|
m_textShadowColor( 0, 0, 0 ),
|
|
m_BBPatternBackground( 0, 0, 0 ),
|
|
m_gradient( true ),
|
|
m_mouseHotspotHand( 0, 0 ),
|
|
m_mouseHotspotKnife( 0, 0 ),
|
|
m_cursorHand( QCursor( embed::getIconPixmap( "hand" ) ) ),
|
|
m_cursorKnife( QCursor( embed::getIconPixmap( "cursor_knife" ) ) ),
|
|
m_cursorSetYet( false ),
|
|
m_needsUpdate( true )
|
|
{
|
|
if( s_textFloat == nullptr )
|
|
{
|
|
s_textFloat = new TextFloat;
|
|
s_textFloat->setPixmap( embed::getIconPixmap( "clock" ) );
|
|
}
|
|
|
|
setAttribute( Qt::WA_OpaquePaintEvent, true );
|
|
setAttribute( Qt::WA_DeleteOnClose, true );
|
|
setFocusPolicy( Qt::StrongFocus );
|
|
setCursor( m_cursorHand );
|
|
move( 0, 0 );
|
|
show();
|
|
|
|
setFixedHeight( tv->getTrackContentWidget()->height() - 1);
|
|
setAcceptDrops( true );
|
|
setMouseTracking( true );
|
|
|
|
connect( m_tco, SIGNAL( lengthChanged() ),
|
|
this, SLOT( updateLength() ) );
|
|
connect( getGUI()->songEditor()->m_editor->zoomingModel(), SIGNAL( dataChanged() ), this, SLOT( updateLength() ) );
|
|
connect( m_tco, SIGNAL( positionChanged() ),
|
|
this, SLOT( updatePosition() ) );
|
|
connect( m_tco, SIGNAL( destroyedTCO() ), this, SLOT( close() ) );
|
|
setModel( m_tco );
|
|
connect(m_tco, SIGNAL(colorChanged()), this, SLOT(update()));
|
|
|
|
connect(m_trackView->getTrack(), &Track::colorChanged, this, [this]
|
|
{
|
|
// redraw if TCO uses track color
|
|
if (!m_tco->usesCustomClipColor()) { update(); }
|
|
});
|
|
|
|
m_trackView->getTrackContentWidget()->addTCOView( this );
|
|
updateLength();
|
|
updatePosition();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Destroy a trackContentObjectView
|
|
*
|
|
* Destroys the given track content object view.
|
|
*
|
|
*/
|
|
TrackContentObjectView::~TrackContentObjectView()
|
|
{
|
|
delete m_hint;
|
|
// we have to give our track-container the focus because otherwise the
|
|
// op-buttons of our track-widgets could become focus and when the user
|
|
// presses space for playing song, just one of these buttons is pressed
|
|
// which results in unwanted effects
|
|
m_trackView->trackContainerView()->setFocus();
|
|
}
|
|
|
|
|
|
/*! \brief Update a TrackContentObjectView
|
|
*
|
|
* TCO's get drawn only when needed,
|
|
* and when a TCO is updated,
|
|
* it needs to be redrawn.
|
|
*
|
|
*/
|
|
void TrackContentObjectView::update()
|
|
{
|
|
if( !m_cursorSetYet )
|
|
{
|
|
m_cursorHand = QCursor( embed::getIconPixmap( "hand" ), m_mouseHotspotHand.width(), m_mouseHotspotHand.height() );
|
|
m_cursorKnife = QCursor( embed::getIconPixmap( "cursor_knife" ), m_mouseHotspotKnife.width(), m_mouseHotspotKnife.height() );
|
|
setCursor( m_cursorHand );
|
|
m_cursorSetYet = true;
|
|
}
|
|
|
|
if( fixedTCOs() )
|
|
{
|
|
updateLength();
|
|
}
|
|
m_needsUpdate = true;
|
|
selectableObject::update();
|
|
}
|
|
|
|
|
|
|
|
/*! \brief Does this trackContentObjectView have a fixed TCO?
|
|
*
|
|
* Returns whether the containing trackView has fixed
|
|
* TCOs.
|
|
*
|
|
* \todo What the hell is a TCO here - track content object? And in
|
|
* what circumstance are they fixed?
|
|
*/
|
|
bool TrackContentObjectView::fixedTCOs()
|
|
{
|
|
return m_trackView->trackContainerView()->fixedTCOs();
|
|
}
|
|
|
|
|
|
|
|
// qproperty access functions, to be inherited & used by TCOviews
|
|
//! \brief CSS theming qproperty access method
|
|
QColor TrackContentObjectView::mutedColor() const
|
|
{ return m_mutedColor; }
|
|
|
|
QColor TrackContentObjectView::mutedBackgroundColor() const
|
|
{ return m_mutedBackgroundColor; }
|
|
|
|
QColor TrackContentObjectView::selectedColor() const
|
|
{ return m_selectedColor; }
|
|
|
|
QColor TrackContentObjectView::textColor() const
|
|
{ return m_textColor; }
|
|
|
|
QColor TrackContentObjectView::textBackgroundColor() const
|
|
{
|
|
return m_textBackgroundColor;
|
|
}
|
|
|
|
QColor TrackContentObjectView::textShadowColor() const
|
|
{ return m_textShadowColor; }
|
|
|
|
QColor TrackContentObjectView::BBPatternBackground() const
|
|
{ return m_BBPatternBackground; }
|
|
|
|
bool TrackContentObjectView::gradient() const
|
|
{ return m_gradient; }
|
|
|
|
//! \brief CSS theming qproperty access method
|
|
void TrackContentObjectView::setMutedColor( const QColor & c )
|
|
{ m_mutedColor = QColor( c ); }
|
|
|
|
void TrackContentObjectView::setMutedBackgroundColor( const QColor & c )
|
|
{ m_mutedBackgroundColor = QColor( c ); }
|
|
|
|
void TrackContentObjectView::setSelectedColor( const QColor & c )
|
|
{ m_selectedColor = QColor( c ); }
|
|
|
|
void TrackContentObjectView::setTextColor( const QColor & c )
|
|
{ m_textColor = QColor( c ); }
|
|
|
|
void TrackContentObjectView::setTextBackgroundColor( const QColor & c )
|
|
{
|
|
m_textBackgroundColor = c;
|
|
}
|
|
|
|
void TrackContentObjectView::setTextShadowColor( const QColor & c )
|
|
{ m_textShadowColor = QColor( c ); }
|
|
|
|
void TrackContentObjectView::setBBPatternBackground( const QColor & c )
|
|
{ m_BBPatternBackground = QColor( c ); }
|
|
|
|
void TrackContentObjectView::setGradient( const bool & b )
|
|
{ m_gradient = b; }
|
|
|
|
// access needsUpdate member variable
|
|
bool TrackContentObjectView::needsUpdate()
|
|
{ return m_needsUpdate; }
|
|
void TrackContentObjectView::setNeedsUpdate( bool b )
|
|
{ m_needsUpdate = b; }
|
|
|
|
/*! \brief Close a trackContentObjectView
|
|
*
|
|
* Closes a track content object view by asking the track
|
|
* view to remove us and then asking the QWidget to close us.
|
|
*
|
|
* \return Boolean state of whether the QWidget was able to close.
|
|
*/
|
|
bool TrackContentObjectView::close()
|
|
{
|
|
m_trackView->getTrackContentWidget()->removeTCOView( this );
|
|
return QWidget::close();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Removes a trackContentObjectView from its track view.
|
|
*
|
|
* Like the close() method, this asks the track view to remove this
|
|
* track content object view. However, the track content object is
|
|
* scheduled for later deletion rather than closed immediately.
|
|
*
|
|
*/
|
|
void TrackContentObjectView::remove()
|
|
{
|
|
m_trackView->getTrack()->addJournalCheckPoint();
|
|
|
|
// delete ourself
|
|
close();
|
|
m_tco->deleteLater();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Updates a trackContentObjectView's length
|
|
*
|
|
* If this track content object view has a fixed TCO, then we must
|
|
* keep the width of our parent. Otherwise, calculate our width from
|
|
* the track content object's length in pixels adding in the border.
|
|
*
|
|
*/
|
|
void TrackContentObjectView::updateLength()
|
|
{
|
|
if( fixedTCOs() )
|
|
{
|
|
setFixedWidth( parentWidget()->width() );
|
|
}
|
|
else
|
|
{
|
|
setFixedWidth(
|
|
static_cast<int>( m_tco->length() * pixelsPerBar() /
|
|
TimePos::ticksPerBar() ) + 1 /*+
|
|
TCO_BORDER_WIDTH * 2-1*/ );
|
|
}
|
|
m_trackView->trackContainerView()->update();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Updates a trackContentObjectView's position.
|
|
*
|
|
* Ask our track view to change our position. Then make sure that the
|
|
* track view is updated in case this position has changed the track
|
|
* view's length.
|
|
*
|
|
*/
|
|
void TrackContentObjectView::updatePosition()
|
|
{
|
|
m_trackView->getTrackContentWidget()->changePosition();
|
|
// moving a TCO can result in change of song-length etc.,
|
|
// therefore we update the track-container
|
|
m_trackView->trackContainerView()->update();
|
|
}
|
|
|
|
|
|
|
|
|
|
void TrackContentObjectView::selectColor()
|
|
{
|
|
// Get a color from the user
|
|
QColor new_color = ColorChooser( this ).withPalette( ColorChooser::Palette::Track )->getColor( m_tco->color() );
|
|
if (new_color.isValid()) { setColor(&new_color); }
|
|
}
|
|
|
|
|
|
|
|
|
|
void TrackContentObjectView::randomizeColor()
|
|
{
|
|
setColor(&ColorChooser::getPalette(ColorChooser::Palette::Mixer)[rand() % 48]);
|
|
}
|
|
|
|
|
|
|
|
|
|
void TrackContentObjectView::resetColor()
|
|
{
|
|
setColor(nullptr);
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Change color of all selected TCOs
|
|
*
|
|
* \param color The new QColor. Pass nullptr to use the Track's color.
|
|
*/
|
|
void TrackContentObjectView::setColor(const QColor* color)
|
|
{
|
|
std::set<Track*> journaledTracks;
|
|
|
|
auto selectedTCOs = getClickedTCOs();
|
|
for (auto tcov: selectedTCOs)
|
|
{
|
|
auto tco = tcov->getTrackContentObject();
|
|
auto track = tco->getTrack();
|
|
|
|
// TODO journal whole Song or group of TCOs instead of one journal entry for each track
|
|
|
|
// If only one TCO changed, store that in the journal
|
|
if (selectedTCOs.length() == 1)
|
|
{
|
|
tco->addJournalCheckPoint();
|
|
}
|
|
// If multiple TCOs changed, store whole Track in the journal
|
|
// Check if track has been journaled already by trying to add it to the set
|
|
else if (journaledTracks.insert(track).second)
|
|
{
|
|
track->addJournalCheckPoint();
|
|
}
|
|
|
|
if (color)
|
|
{
|
|
tco->useCustomClipColor(true);
|
|
tco->setColor(*color);
|
|
}
|
|
else
|
|
{
|
|
tco->useCustomClipColor(false);
|
|
}
|
|
tcov->update();
|
|
}
|
|
|
|
Engine::getSong()->setModified();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*! \brief Change the trackContentObjectView's display when something
|
|
* being dragged enters it.
|
|
*
|
|
* We need to notify Qt to change our display if something being
|
|
* dragged has entered our 'airspace'.
|
|
*
|
|
* \param dee The QDragEnterEvent to watch.
|
|
*/
|
|
void TrackContentObjectView::dragEnterEvent( QDragEnterEvent * dee )
|
|
{
|
|
TrackContentWidget * tcw = getTrackView()->getTrackContentWidget();
|
|
TimePos tcoPos = TimePos( m_tco->startPosition() );
|
|
|
|
if( tcw->canPasteSelection( tcoPos, dee ) == false )
|
|
{
|
|
dee->ignore();
|
|
}
|
|
else
|
|
{
|
|
StringPairDrag::processDragEnterEvent( dee, "tco_" +
|
|
QString::number( m_tco->getTrack()->type() ) );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Handle something being dropped on this trackContentObjectView.
|
|
*
|
|
* When something has been dropped on this trackContentObjectView, and
|
|
* it's a track content object, then use an instance of our dataFile reader
|
|
* to take the xml of the track content object and turn it into something
|
|
* we can write over our current state.
|
|
*
|
|
* \param de The QDropEvent to handle.
|
|
*/
|
|
void TrackContentObjectView::dropEvent( QDropEvent * de )
|
|
{
|
|
QString type = StringPairDrag::decodeKey( de );
|
|
QString value = StringPairDrag::decodeValue( de );
|
|
|
|
// Track must be the same type to paste into
|
|
if( type != ( "tco_" + QString::number( m_tco->getTrack()->type() ) ) )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Defer to rubberband paste if we're in that mode
|
|
if( m_trackView->trackContainerView()->allowRubberband() == true )
|
|
{
|
|
TrackContentWidget * tcw = getTrackView()->getTrackContentWidget();
|
|
TimePos tcoPos = TimePos( m_tco->startPosition() );
|
|
|
|
if( tcw->pasteSelection( tcoPos, de ) == true )
|
|
{
|
|
de->accept();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Don't allow pasting a tco into itself.
|
|
QObject* qwSource = de->source();
|
|
if( qwSource != nullptr &&
|
|
dynamic_cast<TrackContentObjectView *>( qwSource ) == this )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Copy state into existing tco
|
|
DataFile dataFile( value.toUtf8() );
|
|
TimePos pos = m_tco->startPosition();
|
|
QDomElement tcos = dataFile.content().firstChildElement( "tcos" );
|
|
m_tco->restoreState( tcos.firstChildElement().firstChildElement() );
|
|
m_tco->movePosition( pos );
|
|
AutomationPattern::resolveAllIDs();
|
|
de->accept();
|
|
}
|
|
|
|
|
|
|
|
|
|
/* @brief Chooses the correct cursor to be displayed on the widget
|
|
*
|
|
* @param me The QMouseEvent that is triggering the cursor change
|
|
*/
|
|
void TrackContentObjectView::updateCursor(QMouseEvent * me)
|
|
{
|
|
SampleTCO * sTco = dynamic_cast<SampleTCO*>(m_tco);
|
|
|
|
// If we are at the edges, use the resize cursor
|
|
if ((me->x() > width() - RESIZE_GRIP_WIDTH && !me->buttons() && !m_tco->getAutoResize())
|
|
|| (me->x() < RESIZE_GRIP_WIDTH && !me->buttons() && sTco && !m_tco->getAutoResize()))
|
|
{
|
|
setCursor(Qt::SizeHorCursor);
|
|
}
|
|
// If we are in the middle on knife mode, use the knife cursor
|
|
else if (sTco && m_trackView->trackContainerView()->knifeMode())
|
|
{
|
|
setCursor(m_cursorKnife);
|
|
}
|
|
// If we are in the middle in any other mode, use the hand cursor
|
|
else { setCursor(m_cursorHand); }
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Create a DataFile suitable for copying multiple trackContentObjects.
|
|
*
|
|
* trackContentObjects in the vector are written to the "tcos" node in the
|
|
* DataFile. The trackContentObjectView's initial mouse position is written
|
|
* to the "initialMouseX" node in the DataFile. When dropped on a track,
|
|
* this is used to create copies of the TCOs.
|
|
*
|
|
* \param tcos The trackContectObjects to save in a DataFile
|
|
*/
|
|
DataFile TrackContentObjectView::createTCODataFiles(
|
|
const QVector<TrackContentObjectView *> & tcoViews) const
|
|
{
|
|
Track * t = m_trackView->getTrack();
|
|
TrackContainer * tc = t->trackContainer();
|
|
DataFile dataFile( DataFile::DragNDropData );
|
|
QDomElement tcoParent = dataFile.createElement( "tcos" );
|
|
|
|
typedef QVector<TrackContentObjectView *> tcoViewVector;
|
|
for( tcoViewVector::const_iterator it = tcoViews.begin();
|
|
it != tcoViews.end(); ++it )
|
|
{
|
|
// Insert into the dom under the "tcos" element
|
|
Track* tcoTrack = ( *it )->m_trackView->getTrack();
|
|
int trackIndex = tc->tracks().indexOf( tcoTrack );
|
|
QDomElement tcoElement = dataFile.createElement( "tco" );
|
|
tcoElement.setAttribute( "trackIndex", trackIndex );
|
|
tcoElement.setAttribute( "trackType", tcoTrack->type() );
|
|
tcoElement.setAttribute( "trackName", tcoTrack->name() );
|
|
( *it )->m_tco->saveState( dataFile, tcoElement );
|
|
tcoParent.appendChild( tcoElement );
|
|
}
|
|
|
|
dataFile.content().appendChild( tcoParent );
|
|
|
|
// Add extra metadata needed for calculations later
|
|
int initialTrackIndex = tc->tracks().indexOf( t );
|
|
if( initialTrackIndex < 0 )
|
|
{
|
|
printf("Failed to find selected track in the TrackContainer.\n");
|
|
return dataFile;
|
|
}
|
|
QDomElement metadata = dataFile.createElement( "copyMetadata" );
|
|
// initialTrackIndex is the index of the track that was touched
|
|
metadata.setAttribute( "initialTrackIndex", initialTrackIndex );
|
|
metadata.setAttribute( "trackContainerId", tc->id() );
|
|
// grabbedTCOPos is the pos of the bar containing the TCO we grabbed
|
|
metadata.setAttribute( "grabbedTCOPos", m_tco->startPosition() );
|
|
|
|
dataFile.content().appendChild( metadata );
|
|
|
|
return dataFile;
|
|
}
|
|
|
|
void TrackContentObjectView::paintTextLabel(QString const & text, QPainter & painter)
|
|
{
|
|
if (text.trimmed() == "")
|
|
{
|
|
return;
|
|
}
|
|
|
|
painter.setRenderHint( QPainter::TextAntialiasing );
|
|
|
|
QFont labelFont = this->font();
|
|
labelFont.setHintingPreference( QFont::PreferFullHinting );
|
|
painter.setFont( labelFont );
|
|
|
|
const int textTop = TCO_BORDER_WIDTH + 1;
|
|
const int textLeft = TCO_BORDER_WIDTH + 3;
|
|
|
|
QFontMetrics fontMetrics(labelFont);
|
|
QString elidedPatternName = fontMetrics.elidedText(text, Qt::ElideMiddle, width() - 2 * textLeft);
|
|
|
|
if (elidedPatternName.length() < 2)
|
|
{
|
|
elidedPatternName = text.trimmed();
|
|
}
|
|
|
|
painter.fillRect(QRect(0, 0, width(), fontMetrics.height() + 2 * textTop), textBackgroundColor());
|
|
|
|
int const finalTextTop = textTop + fontMetrics.ascent();
|
|
painter.setPen(textShadowColor());
|
|
painter.drawText( textLeft + 1, finalTextTop + 1, elidedPatternName );
|
|
painter.setPen( textColor() );
|
|
painter.drawText( textLeft, finalTextTop, elidedPatternName );
|
|
}
|
|
|
|
/*! \brief Handle a mouse press on this trackContentObjectView.
|
|
*
|
|
* Handles the various ways in which a trackContentObjectView can be
|
|
* used with a click of a mouse button.
|
|
*
|
|
* * If our container supports rubber band selection then handle
|
|
* selection events.
|
|
* * or if shift-left button, add this object to the selection
|
|
* * or if ctrl-left button, start a drag-copy event
|
|
* * or if just plain left button, resize if we're resizeable
|
|
* * or if ctrl-middle button, mute the track content object
|
|
* * or if middle button, maybe delete the track content object.
|
|
*
|
|
* \param me The QMouseEvent to handle.
|
|
*/
|
|
void TrackContentObjectView::mousePressEvent( QMouseEvent * me )
|
|
{
|
|
// Right now, active is only used on right/mid clicks actions, so we use a ternary operator
|
|
// to avoid the overhead of calling getClickedTCOs when it's not used
|
|
auto active = me->button() == Qt::LeftButton
|
|
? QVector<TrackContentObjectView *>()
|
|
: getClickedTCOs();
|
|
|
|
setInitialPos( me->pos() );
|
|
setInitialOffsets();
|
|
if( !fixedTCOs() && me->button() == Qt::LeftButton )
|
|
{
|
|
SampleTCO * sTco = dynamic_cast<SampleTCO*>( m_tco );
|
|
const bool knifeMode = m_trackView->trackContainerView()->knifeMode();
|
|
|
|
if ( me->modifiers() & Qt::ControlModifier && !(sTco && knifeMode) )
|
|
{
|
|
if( isSelected() )
|
|
{
|
|
m_action = CopySelection;
|
|
}
|
|
else
|
|
{
|
|
m_action = ToggleSelected;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( isSelected() )
|
|
{
|
|
m_action = MoveSelection;
|
|
}
|
|
else
|
|
{
|
|
getGUI()->songEditor()->m_editor->selectAllTcos( false );
|
|
m_tco->addJournalCheckPoint();
|
|
|
|
// Move, Resize and ResizeLeft
|
|
// Split action doesn't disable TCO journalling
|
|
if (m_action == Move || m_action == Resize || m_action == ResizeLeft)
|
|
{
|
|
m_tco->setJournalling(false);
|
|
}
|
|
|
|
setInitialPos( me->pos() );
|
|
setInitialOffsets();
|
|
|
|
if( m_tco->getAutoResize() )
|
|
{ // Always move clips that can't be manually resized
|
|
m_action = Move;
|
|
setCursor( Qt::SizeAllCursor );
|
|
}
|
|
else if( me->x() >= width() - RESIZE_GRIP_WIDTH )
|
|
{
|
|
m_action = Resize;
|
|
setCursor( Qt::SizeHorCursor );
|
|
}
|
|
else if( me->x() < RESIZE_GRIP_WIDTH && sTco )
|
|
{
|
|
m_action = ResizeLeft;
|
|
setCursor( Qt::SizeHorCursor );
|
|
}
|
|
else if( sTco && knifeMode )
|
|
{
|
|
m_action = Split;
|
|
setCursor( m_cursorKnife );
|
|
setMarkerPos( knifeMarkerPos( me ) );
|
|
setMarkerEnabled( true );
|
|
update();
|
|
}
|
|
else
|
|
{
|
|
m_action = Move;
|
|
setCursor( Qt::SizeAllCursor );
|
|
}
|
|
|
|
if( m_action == Move )
|
|
{
|
|
s_textFloat->setTitle( tr( "Current position" ) );
|
|
s_textFloat->setText( QString( "%1:%2" ).
|
|
arg( m_tco->startPosition().getBar() + 1 ).
|
|
arg( m_tco->startPosition().getTicks() %
|
|
TimePos::ticksPerBar() ) );
|
|
}
|
|
else if( m_action == Resize || m_action == ResizeLeft )
|
|
{
|
|
s_textFloat->setTitle( tr( "Current length" ) );
|
|
s_textFloat->setText( tr( "%1:%2 (%3:%4 to %5:%6)" ).
|
|
arg( m_tco->length().getBar() ).
|
|
arg( m_tco->length().getTicks() %
|
|
TimePos::ticksPerBar() ).
|
|
arg( m_tco->startPosition().getBar() + 1 ).
|
|
arg( m_tco->startPosition().getTicks() %
|
|
TimePos::ticksPerBar() ).
|
|
arg( m_tco->endPosition().getBar() + 1 ).
|
|
arg( m_tco->endPosition().getTicks() %
|
|
TimePos::ticksPerBar() ) );
|
|
}
|
|
// s_textFloat->reparent( this );
|
|
// setup text-float as if TCO was already moved/resized
|
|
s_textFloat->moveGlobal( this, QPoint( width() + 2, height() + 2) );
|
|
if ( m_action != Split) { s_textFloat->show(); }
|
|
}
|
|
|
|
delete m_hint;
|
|
QString hint = m_action == Move || m_action == MoveSelection
|
|
? tr( "Press <%1> and drag to make a copy." )
|
|
: tr( "Press <%1> for free resizing." );
|
|
m_hint = TextFloat::displayMessage( tr( "Hint" ), hint.arg(UI_CTRL_KEY),
|
|
embed::getIconPixmap( "hint" ), 0 );
|
|
}
|
|
}
|
|
else if( me->button() == Qt::RightButton )
|
|
{
|
|
if( me->modifiers() & Qt::ControlModifier )
|
|
{
|
|
toggleMute( active );
|
|
}
|
|
else if( me->modifiers() & Qt::ShiftModifier && !fixedTCOs() )
|
|
{
|
|
remove( active );
|
|
}
|
|
if (m_action == Split)
|
|
{
|
|
m_action = NoAction;
|
|
SampleTCO * sTco = dynamic_cast<SampleTCO*>( m_tco );
|
|
if (sTco)
|
|
{
|
|
setMarkerEnabled( false );
|
|
update();
|
|
}
|
|
}
|
|
}
|
|
else if( me->button() == Qt::MidButton )
|
|
{
|
|
if( me->modifiers() & Qt::ControlModifier )
|
|
{
|
|
toggleMute( active );
|
|
}
|
|
else if( !fixedTCOs() )
|
|
{
|
|
remove( active );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Handle a mouse movement (drag) on this trackContentObjectView.
|
|
*
|
|
* Handles the various ways in which a trackContentObjectView can be
|
|
* used with a mouse drag.
|
|
*
|
|
* * If in move mode, move ourselves in the track,
|
|
* * or if in move-selection mode, move the entire selection,
|
|
* * or if in resize mode, resize ourselves,
|
|
* * otherwise ???
|
|
*
|
|
* \param me The QMouseEvent to handle.
|
|
* \todo what does the final else case do here?
|
|
*/
|
|
void TrackContentObjectView::mouseMoveEvent( QMouseEvent * me )
|
|
{
|
|
if( m_action == CopySelection || m_action == ToggleSelected )
|
|
{
|
|
if( mouseMovedDistance( me, 2 ) == true )
|
|
{
|
|
QVector<TrackContentObjectView *> tcoViews;
|
|
if( m_action == CopySelection )
|
|
{
|
|
// Collect all selected TCOs
|
|
QVector<selectableObject *> so =
|
|
m_trackView->trackContainerView()->selectedObjects();
|
|
for( auto it = so.begin(); it != so.end(); ++it )
|
|
{
|
|
TrackContentObjectView * tcov =
|
|
dynamic_cast<TrackContentObjectView *>( *it );
|
|
if( tcov != nullptr )
|
|
{
|
|
tcoViews.push_back( tcov );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
getGUI()->songEditor()->m_editor->selectAllTcos( false );
|
|
tcoViews.push_back( this );
|
|
}
|
|
// Clear the action here because mouseReleaseEvent will not get
|
|
// triggered once we go into drag.
|
|
m_action = NoAction;
|
|
|
|
// Write the TCOs to the DataFile for copying
|
|
DataFile dataFile = createTCODataFiles( tcoViews );
|
|
|
|
// TODO -- thumbnail for all selected
|
|
QPixmap thumbnail = grab().scaled(
|
|
128, 128,
|
|
Qt::KeepAspectRatio,
|
|
Qt::SmoothTransformation );
|
|
new StringPairDrag( QString( "tco_%1" ).arg(
|
|
m_tco->getTrack()->type() ),
|
|
dataFile.toString(), thumbnail, this );
|
|
}
|
|
}
|
|
|
|
if( me->modifiers() & Qt::ControlModifier )
|
|
{
|
|
delete m_hint;
|
|
m_hint = nullptr;
|
|
}
|
|
|
|
const float ppb = m_trackView->trackContainerView()->pixelsPerBar();
|
|
if( m_action == Move )
|
|
{
|
|
TimePos newPos = draggedTCOPos( me );
|
|
|
|
m_tco->movePosition(newPos);
|
|
newPos = m_tco->startPosition(); // Get the real position the TCO was dragged to for the label
|
|
m_trackView->getTrackContentWidget()->changePosition();
|
|
s_textFloat->setText( QString( "%1:%2" ).
|
|
arg( newPos.getBar() + 1 ).
|
|
arg( newPos.getTicks() %
|
|
TimePos::ticksPerBar() ) );
|
|
s_textFloat->moveGlobal( this, QPoint( width() + 2, height() + 2 ) );
|
|
}
|
|
else if( m_action == MoveSelection )
|
|
{
|
|
// 1: Find the position we want to move the grabbed TCO to
|
|
TimePos newPos = draggedTCOPos( me );
|
|
|
|
// 2: Handle moving the other selected TCOs the same distance
|
|
QVector<selectableObject *> so =
|
|
m_trackView->trackContainerView()->selectedObjects();
|
|
QVector<TrackContentObject *> tcos; // List of selected clips
|
|
int leftmost = 0; // Leftmost clip's offset from grabbed clip
|
|
// Populate tcos, find leftmost
|
|
for( QVector<selectableObject *>::iterator it = so.begin();
|
|
it != so.end(); ++it )
|
|
{
|
|
TrackContentObjectView * tcov =
|
|
dynamic_cast<TrackContentObjectView *>( *it );
|
|
if( tcov == nullptr ) { continue; }
|
|
tcos.push_back( tcov->m_tco );
|
|
int index = std::distance( so.begin(), it );
|
|
leftmost = std::min(leftmost, m_initialOffsets[index].getTicks());
|
|
}
|
|
// Make sure the leftmost clip doesn't get moved to a negative position
|
|
if ( newPos.getTicks() + leftmost < 0 ) { newPos = -leftmost; }
|
|
|
|
for( QVector<TrackContentObject *>::iterator it = tcos.begin();
|
|
it != tcos.end(); ++it )
|
|
{
|
|
int index = std::distance( tcos.begin(), it );
|
|
( *it )->movePosition( newPos + m_initialOffsets[index] );
|
|
}
|
|
}
|
|
else if( m_action == Resize || m_action == ResizeLeft )
|
|
{
|
|
const float snapSize = getGUI()->songEditor()->m_editor->getSnapSize();
|
|
// Length in ticks of one snap increment
|
|
const TimePos snapLength = TimePos( (int)(snapSize * TimePos::ticksPerBar()) );
|
|
|
|
if( m_action == Resize )
|
|
{
|
|
// The clip's new length
|
|
TimePos l = static_cast<int>( me->x() * TimePos::ticksPerBar() / ppb );
|
|
|
|
// If the user is holding alt, or pressed ctrl after beginning the drag, don't quantize
|
|
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
|
|
m_tco->changeLength( qMax<int>( 1, l ) );
|
|
}
|
|
else if ( me->modifiers() & Qt::ShiftModifier )
|
|
{ // If shift is held, quantize clip's end position
|
|
TimePos end = TimePos( m_initialTCOPos + l ).quantize( snapSize );
|
|
// The end position has to be after the clip's start
|
|
TimePos min = m_initialTCOPos.quantize( snapSize );
|
|
if ( min <= m_initialTCOPos ) min += snapLength;
|
|
m_tco->changeLength( qMax<int>(min - m_initialTCOPos, end - m_initialTCOPos) );
|
|
}
|
|
else
|
|
{ // Otherwise, resize in fixed increments
|
|
TimePos initialLength = m_initialTCOEnd - m_initialTCOPos;
|
|
TimePos offset = TimePos( l - initialLength ).quantize( snapSize );
|
|
// Don't resize to less than 1 tick
|
|
TimePos min = TimePos( initialLength % snapLength );
|
|
if (min < 1) min += snapLength;
|
|
m_tco->changeLength( qMax<int>( min, initialLength + offset) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SampleTCO * sTco = dynamic_cast<SampleTCO*>( m_tco );
|
|
if( sTco )
|
|
{
|
|
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( 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_initialTCOEnd - 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_initialTCOEnd.quantize( snapSize );
|
|
if ( max >= m_initialTCOEnd ) 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_initialTCOEnd - m_initialTCOPos;
|
|
TimePos minLength = TimePos( initialLength % snapLength );
|
|
if (minLength < 1) minLength += snapLength;
|
|
TimePos offset = TimePos(t - m_initialTCOPos).quantize( snapSize );
|
|
t = qMin<int>( m_initialTCOEnd - minLength, m_initialTCOPos + offset );
|
|
}
|
|
|
|
TimePos oldPos = m_tco->startPosition();
|
|
if( m_tco->length() + ( oldPos - t ) >= 1 )
|
|
{
|
|
m_tco->movePosition( t );
|
|
m_tco->changeLength( m_tco->length() + ( oldPos - t ) );
|
|
sTco->setStartTimeOffset( sTco->startTimeOffset() + ( oldPos - t ) );
|
|
}
|
|
}
|
|
}
|
|
s_textFloat->setText( tr( "%1:%2 (%3:%4 to %5:%6)" ).
|
|
arg( m_tco->length().getBar() ).
|
|
arg( m_tco->length().getTicks() %
|
|
TimePos::ticksPerBar() ).
|
|
arg( m_tco->startPosition().getBar() + 1 ).
|
|
arg( m_tco->startPosition().getTicks() %
|
|
TimePos::ticksPerBar() ).
|
|
arg( m_tco->endPosition().getBar() + 1 ).
|
|
arg( m_tco->endPosition().getTicks() %
|
|
TimePos::ticksPerBar() ) );
|
|
s_textFloat->moveGlobal( this, QPoint( width() + 2, height() + 2) );
|
|
}
|
|
else if( m_action == Split )
|
|
{
|
|
SampleTCO * sTco = dynamic_cast<SampleTCO*>( m_tco );
|
|
if (sTco) {
|
|
setCursor( m_cursorKnife );
|
|
setMarkerPos( knifeMarkerPos( me ) );
|
|
}
|
|
update();
|
|
}
|
|
// None of the actions above, we will just handle the cursor
|
|
else { updateCursor(me); }
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Handle a mouse release on this trackContentObjectView.
|
|
*
|
|
* If we're in move or resize mode, journal the change as appropriate.
|
|
* Then tidy up.
|
|
*
|
|
* \param me The QMouseEvent to handle.
|
|
*/
|
|
void TrackContentObjectView::mouseReleaseEvent( QMouseEvent * me )
|
|
{
|
|
// If the CopySelection was chosen as the action due to mouse movement,
|
|
// it will have been cleared. At this point Toggle is the desired action.
|
|
// An active StringPairDrag will prevent this method from being called,
|
|
// so a real CopySelection would not have occurred.
|
|
if( m_action == CopySelection ||
|
|
( m_action == ToggleSelected && mouseMovedDistance( me, 2 ) == false ) )
|
|
{
|
|
setSelected( !isSelected() );
|
|
}
|
|
else if( m_action == Move || m_action == Resize || m_action == ResizeLeft )
|
|
{
|
|
// TODO: Fix m_tco->setJournalling() consistency
|
|
m_tco->setJournalling( true );
|
|
}
|
|
else if( m_action == Split )
|
|
{
|
|
const float ppb = m_trackView->trackContainerView()->pixelsPerBar();
|
|
const TimePos relPos = me->pos().x() * TimePos::ticksPerBar() / ppb;
|
|
splitTCO(unquantizedModHeld(me) ?
|
|
relPos :
|
|
quantizeSplitPos(relPos, me->modifiers() & Qt::ShiftModifier)
|
|
);
|
|
}
|
|
|
|
m_action = NoAction;
|
|
delete m_hint;
|
|
m_hint = nullptr;
|
|
s_textFloat->hide();
|
|
updateCursor(me);
|
|
selectableObject::mouseReleaseEvent( me );
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Set up the context menu for this trackContentObjectView.
|
|
*
|
|
* Set up the various context menu events that can apply to a
|
|
* track content object view.
|
|
*
|
|
* \param cme The QContextMenuEvent to add the actions to.
|
|
*/
|
|
void TrackContentObjectView::contextMenuEvent( QContextMenuEvent * cme )
|
|
{
|
|
QVector<TrackContentObjectView*> selectedTCOs = getClickedTCOs();
|
|
|
|
// Depending on whether we right-clicked a selection or an individual TCO we will have
|
|
// different labels for the actions.
|
|
bool individualTCO = selectedTCOs.size() <= 1;
|
|
|
|
if( cme->modifiers() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
QMenu contextMenu( this );
|
|
|
|
if( fixedTCOs() == false )
|
|
{
|
|
contextMenu.addAction(
|
|
embed::getIconPixmap( "cancel" ),
|
|
individualTCO
|
|
? tr("Delete (middle mousebutton)")
|
|
: tr("Delete selection (middle mousebutton)"),
|
|
[this](){ contextMenuAction( Remove ); } );
|
|
|
|
contextMenu.addSeparator();
|
|
|
|
contextMenu.addAction(
|
|
embed::getIconPixmap( "edit_cut" ),
|
|
individualTCO
|
|
? tr("Cut")
|
|
: tr("Cut selection"),
|
|
[this](){ contextMenuAction( Cut ); } );
|
|
|
|
if (canMergeSelection(selectedTCOs))
|
|
{
|
|
contextMenu.addAction(
|
|
embed::getIconPixmap("edit_merge"),
|
|
tr("Merge Selection"),
|
|
[this]() { contextMenuAction(Merge); }
|
|
);
|
|
}
|
|
}
|
|
|
|
contextMenu.addAction(
|
|
embed::getIconPixmap( "edit_copy" ),
|
|
individualTCO
|
|
? tr("Copy")
|
|
: tr("Copy selection"),
|
|
[this](){ contextMenuAction( Copy ); } );
|
|
|
|
contextMenu.addAction(
|
|
embed::getIconPixmap( "edit_paste" ),
|
|
tr( "Paste" ),
|
|
[this](){ contextMenuAction( Paste ); } );
|
|
|
|
contextMenu.addSeparator();
|
|
|
|
contextMenu.addAction(
|
|
embed::getIconPixmap( "muted" ),
|
|
(individualTCO
|
|
? tr("Mute/unmute (<%1> + middle click)")
|
|
: tr("Mute/unmute selection (<%1> + middle click)")).arg(UI_CTRL_KEY),
|
|
[this](){ contextMenuAction( Mute ); } );
|
|
|
|
contextMenu.addSeparator();
|
|
|
|
QMenu colorMenu (tr("Clip color"), this);
|
|
colorMenu.setIcon(embed::getIconPixmap("colorize"));
|
|
colorMenu.addAction(tr("Change"), this, SLOT(selectColor()));
|
|
colorMenu.addAction(tr("Reset"), this, SLOT(resetColor()));
|
|
colorMenu.addAction(tr("Pick random"), this, SLOT(randomizeColor()));
|
|
contextMenu.addMenu(&colorMenu);
|
|
|
|
constructContextMenu( &contextMenu );
|
|
|
|
contextMenu.exec( QCursor::pos() );
|
|
}
|
|
|
|
// This method processes the actions from the context menu of the TCO View.
|
|
void TrackContentObjectView::contextMenuAction( ContextMenuAction action )
|
|
{
|
|
QVector<TrackContentObjectView *> active = getClickedTCOs();
|
|
// active will be later used for the remove, copy, cut or toggleMute methods
|
|
|
|
switch( action )
|
|
{
|
|
case Remove:
|
|
remove( active );
|
|
break;
|
|
case Cut:
|
|
cut( active );
|
|
break;
|
|
case Copy:
|
|
copy( active );
|
|
break;
|
|
case Paste:
|
|
paste();
|
|
break;
|
|
case Mute:
|
|
toggleMute( active );
|
|
break;
|
|
case Merge:
|
|
mergeTCOs(active);
|
|
break;
|
|
}
|
|
}
|
|
|
|
QVector<TrackContentObjectView *> TrackContentObjectView::getClickedTCOs()
|
|
{
|
|
// Get a list of selected selectableObjects
|
|
QVector<selectableObject *> sos = getGUI()->songEditor()->m_editor->selectedObjects();
|
|
|
|
// Convert to a list of selected TCOVs
|
|
QVector<TrackContentObjectView *> selection;
|
|
selection.reserve( sos.size() );
|
|
for( auto so: sos )
|
|
{
|
|
TrackContentObjectView *tcov = dynamic_cast<TrackContentObjectView *> ( so );
|
|
if( tcov != nullptr )
|
|
{
|
|
selection.append( tcov );
|
|
}
|
|
}
|
|
|
|
// If we clicked part of the selection, affect all selected clips. Otherwise affect the clip we clicked
|
|
return selection.contains(this)
|
|
? selection
|
|
: QVector<TrackContentObjectView *>( 1, this );
|
|
}
|
|
|
|
void TrackContentObjectView::remove( QVector<TrackContentObjectView *> tcovs )
|
|
{
|
|
for( auto tcov: tcovs )
|
|
{
|
|
// No need to check if it's nullptr because we check when building the QVector
|
|
tcov->remove();
|
|
}
|
|
}
|
|
|
|
void TrackContentObjectView::copy( QVector<TrackContentObjectView *> tcovs )
|
|
{
|
|
// For copyStringPair()
|
|
using namespace Clipboard;
|
|
|
|
// Write the TCOs to a DataFile for copying
|
|
DataFile dataFile = createTCODataFiles( tcovs );
|
|
|
|
// Copy the TCO type as a key and the TCO data file to the clipboard
|
|
copyStringPair( QString( "tco_%1" ).arg( m_tco->getTrack()->type() ),
|
|
dataFile.toString() );
|
|
}
|
|
|
|
void TrackContentObjectView::cut( QVector<TrackContentObjectView *> tcovs )
|
|
{
|
|
// Copy the selected TCOs
|
|
copy( tcovs );
|
|
|
|
// Now that the TCOs are copied we can delete them, since we are cutting
|
|
remove( tcovs );
|
|
}
|
|
|
|
void TrackContentObjectView::paste()
|
|
{
|
|
// For getMimeData()
|
|
using namespace Clipboard;
|
|
|
|
// If possible, paste the selection on the TimePos of the selected Track and remove it
|
|
TimePos tcoPos = TimePos( m_tco->startPosition() );
|
|
|
|
TrackContentWidget *tcw = getTrackView()->getTrackContentWidget();
|
|
|
|
if( tcw->pasteSelection( tcoPos, getMimeData() ) )
|
|
{
|
|
// If we succeed on the paste we delete the TCO we pasted on
|
|
remove();
|
|
}
|
|
}
|
|
|
|
void TrackContentObjectView::toggleMute( QVector<TrackContentObjectView *> tcovs )
|
|
{
|
|
for( auto tcov: tcovs )
|
|
{
|
|
// No need to check for nullptr because we check while building the tcovs QVector
|
|
tcov->getTrackContentObject()->toggleMute();
|
|
}
|
|
}
|
|
|
|
bool TrackContentObjectView::canMergeSelection(QVector<TrackContentObjectView*> tcovs)
|
|
{
|
|
// Can't merge a single TCO
|
|
if (tcovs.size() < 2) { return false; }
|
|
|
|
// We check if the owner of the first TCO is an Instrument Track
|
|
bool isInstrumentTrack = dynamic_cast<InstrumentTrackView*>(tcovs.at(0)->getTrackView());
|
|
|
|
// Then we create a set with all the TCOs owners
|
|
std::set<TrackView*> ownerTracks;
|
|
for (auto tcov: tcovs) { ownerTracks.insert(tcov->getTrackView()); }
|
|
|
|
// Can merge if there's only one owner track and it's an Instrument Track
|
|
return isInstrumentTrack && ownerTracks.size() == 1;
|
|
}
|
|
|
|
void TrackContentObjectView::mergeTCOs(QVector<TrackContentObjectView*> tcovs)
|
|
{
|
|
// Get the track that we are merging TCOs in
|
|
InstrumentTrack* track =
|
|
dynamic_cast<InstrumentTrack*>(tcovs.at(0)->getTrackView()->getTrack());
|
|
|
|
if (!track)
|
|
{
|
|
qWarning("Warning: Couldn't retrieve InstrumentTrack in mergeTCOs()");
|
|
return;
|
|
}
|
|
|
|
// For Undo/Redo
|
|
track->addJournalCheckPoint();
|
|
track->saveJournallingState(false);
|
|
|
|
// Find the earliest position of all the selected TCOVs
|
|
const auto earliestTCOV = std::min_element(tcovs.constBegin(), tcovs.constEnd(),
|
|
[](TrackContentObjectView* a, TrackContentObjectView* b)
|
|
{
|
|
return a->getTrackContentObject()->startPosition() <
|
|
b->getTrackContentObject()->startPosition();
|
|
}
|
|
);
|
|
|
|
const TimePos earliestPos = (*earliestTCOV)->getTrackContentObject()->startPosition();
|
|
|
|
// Create a pattern where all notes will be added
|
|
Pattern* newPattern = dynamic_cast<Pattern*>(track->createTCO(earliestPos));
|
|
if (!newPattern)
|
|
{
|
|
qWarning("Warning: Failed to convert TCO to Pattern on mergeTCOs");
|
|
return;
|
|
}
|
|
|
|
newPattern->saveJournallingState(false);
|
|
|
|
// Add the notes and remove the TCOs that are being merged
|
|
for (auto tcov: tcovs)
|
|
{
|
|
// Convert TCOV to PatternView
|
|
PatternView* pView = dynamic_cast<PatternView*>(tcov);
|
|
|
|
if (!pView)
|
|
{
|
|
qWarning("Warning: Non-pattern TCO on InstrumentTrack");
|
|
continue;
|
|
}
|
|
|
|
NoteVector currentTCONotes = pView->getPattern()->notes();
|
|
TimePos pViewPos = pView->getPattern()->startPosition();
|
|
|
|
for (Note* note: currentTCONotes)
|
|
{
|
|
Note* newNote = newPattern->addNote(*note, false);
|
|
TimePos originalNotePos = newNote->pos();
|
|
newNote->setPos(originalNotePos + (pViewPos - earliestPos));
|
|
}
|
|
|
|
// We disable the journalling system before removing, so the
|
|
// removal doesn't get added to the undo/redo history
|
|
tcov->getTrackContentObject()->saveJournallingState(false);
|
|
// No need to check for nullptr because we check while building the tcovs QVector
|
|
tcov->remove();
|
|
}
|
|
|
|
// Update length since we might have moved notes beyond the end of the pattern length
|
|
newPattern->updateLength();
|
|
// Rearrange notes because we might have moved them
|
|
newPattern->rearrangeAllNotes();
|
|
// Restore journalling states now that the operation is finished
|
|
newPattern->restoreJournallingState();
|
|
track->restoreJournallingState();
|
|
// Update song
|
|
Engine::getSong()->setModified();
|
|
getGUI()->songEditor()->update();
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief How many pixels a bar takes for this trackContentObjectView.
|
|
*
|
|
* \return the number of pixels per bar.
|
|
*/
|
|
float TrackContentObjectView::pixelsPerBar()
|
|
{
|
|
return m_trackView->trackContainerView()->pixelsPerBar();
|
|
}
|
|
|
|
|
|
/*! \brief Save the offsets between all selected tracks and a clicked track */
|
|
void TrackContentObjectView::setInitialOffsets()
|
|
{
|
|
QVector<selectableObject *> so = m_trackView->trackContainerView()->selectedObjects();
|
|
QVector<TimePos> offsets;
|
|
for( QVector<selectableObject *>::iterator it = so.begin();
|
|
it != so.end(); ++it )
|
|
{
|
|
TrackContentObjectView * tcov =
|
|
dynamic_cast<TrackContentObjectView *>( *it );
|
|
if( tcov == nullptr )
|
|
{
|
|
continue;
|
|
}
|
|
offsets.push_back( tcov->m_tco->startPosition() - m_initialTCOPos );
|
|
}
|
|
|
|
m_initialOffsets = offsets;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Detect whether the mouse moved more than n pixels on screen.
|
|
*
|
|
* \param _me The QMouseEvent.
|
|
* \param distance The threshold distance that the mouse has moved to return true.
|
|
*/
|
|
bool TrackContentObjectView::mouseMovedDistance( QMouseEvent * me, int distance )
|
|
{
|
|
QPoint dPos = mapToGlobal( me->pos() ) - m_initialMouseGlobalPos;
|
|
const int pixelsMoved = dPos.manhattanLength();
|
|
return ( pixelsMoved > distance || pixelsMoved < -distance );
|
|
}
|
|
|
|
|
|
|
|
|
|
bool TrackContentObjectView::unquantizedModHeld( QMouseEvent * me )
|
|
{
|
|
return me->modifiers() & Qt::ControlModifier || me->modifiers() & Qt::AltModifier;
|
|
}
|
|
|
|
|
|
|
|
|
|
/*! \brief Calculate the new position of a dragged TCO from a mouse event
|
|
*
|
|
*
|
|
* \param me The QMouseEvent
|
|
*/
|
|
TimePos TrackContentObjectView::draggedTCOPos( QMouseEvent * me )
|
|
{
|
|
//Pixels per bar
|
|
const float ppb = m_trackView->trackContainerView()->pixelsPerBar();
|
|
// The pixel distance that the mouse has moved
|
|
const int mouseOff = mapToGlobal(me->pos()).x() - m_initialMouseGlobalPos.x();
|
|
TimePos newPos = m_initialTCOPos + mouseOff * TimePos::ticksPerBar() / ppb;
|
|
TimePos offset = newPos - m_initialTCOPos;
|
|
// If the user is holding alt, or pressed ctrl after beginning the drag, don't quantize
|
|
if ( me->button() != Qt::NoButton || unquantizedModHeld(me) )
|
|
{ // We want to preserve this adjusted offset, even if the user switches to snapping
|
|
setInitialPos( m_initialMousePos );
|
|
}
|
|
else if ( me->modifiers() & Qt::ShiftModifier )
|
|
{ // If shift is held, quantize position (Default in 1.2.0 and earlier)
|
|
// or end position, whichever is closest to the actual position
|
|
TimePos startQ = newPos.quantize( getGUI()->songEditor()->m_editor->getSnapSize() );
|
|
// Find start position that gives snapped clip end position
|
|
TimePos endQ = ( newPos + m_tco->length() );
|
|
endQ = endQ.quantize( getGUI()->songEditor()->m_editor->getSnapSize() );
|
|
endQ = endQ - m_tco->length();
|
|
// Select the position closest to actual position
|
|
if ( abs(newPos - startQ) < abs(newPos - endQ) ) newPos = startQ;
|
|
else newPos = endQ;
|
|
}
|
|
else
|
|
{ // Otherwise, quantize moved distance (preserves user offsets)
|
|
newPos = m_initialTCOPos + offset.quantize( getGUI()->songEditor()->m_editor->getSnapSize() );
|
|
}
|
|
return newPos;
|
|
}
|
|
|
|
|
|
int TrackContentObjectView::knifeMarkerPos( QMouseEvent * me )
|
|
{
|
|
//Position relative to start of clip
|
|
const int markerPos = me->pos().x();
|
|
|
|
//In unquantized mode, we don't have to mess with the position at all
|
|
if ( unquantizedModHeld(me) ) { return markerPos; }
|
|
else
|
|
{ //Otherwise we...
|
|
//1: Convert the position to a TimePos
|
|
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 );
|
|
//3: Convert back to a pixel position
|
|
return midiPos * ppb / TimePos::ticksPerBar();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
TimePos TrackContentObjectView::quantizeSplitPos( TimePos midiPos, bool shiftMode )
|
|
{
|
|
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_tco->length() - midiPos;
|
|
const TimePos rightPos = m_tco->length() - rightOff.quantize( snapSize );
|
|
//...whichever gives a position closer to the cursor
|
|
if ( abs(leftPos - midiPos) < abs(rightPos - midiPos) ) { return leftPos; }
|
|
else { return rightPos; }
|
|
}
|
|
else
|
|
{
|
|
return TimePos(midiPos + m_initialTCOPos).quantize( snapSize ) - m_initialTCOPos;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return the color that the TCO's background should be
|
|
QColor TrackContentObjectView::getColorForDisplay( QColor defaultColor )
|
|
{
|
|
// Get the pure TCO color
|
|
auto tcoColor = m_tco->hasColor()
|
|
? m_tco->usesCustomClipColor()
|
|
? m_tco->color()
|
|
: m_tco->getTrack()->color()
|
|
: defaultColor;
|
|
|
|
// Set variables
|
|
QColor c, mutedCustomColor;
|
|
bool muted = m_tco->getTrack()->isMuted() || m_tco->isMuted();
|
|
mutedCustomColor = tcoColor;
|
|
mutedCustomColor.setHsv( mutedCustomColor.hsvHue(), mutedCustomColor.hsvSaturation() / 4, mutedCustomColor.value() );
|
|
|
|
// Change the pure color by state: selected, muted, colored, normal
|
|
if( isSelected() )
|
|
{
|
|
c = m_tco->hasColor()
|
|
? ( muted
|
|
? mutedCustomColor.darker( 350 )
|
|
: tcoColor.darker( 150 ) )
|
|
: selectedColor();
|
|
}
|
|
else
|
|
{
|
|
if( muted )
|
|
{
|
|
c = m_tco->hasColor()
|
|
? mutedCustomColor.darker( 250 )
|
|
: mutedBackgroundColor();
|
|
}
|
|
else
|
|
{
|
|
c = tcoColor;
|
|
}
|
|
}
|
|
|
|
// Return color to caller
|
|
return c;
|
|
}
|