mirror of
https://github.com/LMMS/lmms.git
synced 2026-03-04 22:26:07 -05:00
Summary: * `NULL` -> `nullptr` * `gui` -> Function `getGUI()` * `pluginFactory` -> Function `getPluginFactory()` * `assert` (redefinition) -> using `NDEBUG` instead, which standard `assert` respects. * `powf` (C stdlib symbol clash) -> removed and all expansions replaced with calls to `std::pow`. * `exp10` (nonstandard function symbol clash) -> removed and all expansions replaced with calls to `std::pow`. * `PATH_DEV_DSP` -> File-scope QString of identical name and value. * `VST_SNC_SHM_KEY_FILE` -> constexpr char* with identical name and value. * `MM_ALLOC` and `MM_FREE` -> Functions with identical name and implementation. * `INVAL`, `OUTVAL`, etc. for automation nodes -> Functions with identical names and implementations. * BandLimitedWave.h: All integer constant macros replaced with constexpr ints of same name and value. * `FAST_RAND_MAX` -> constexpr int of same name and value. * `QSTR_TO_STDSTR` -> Function with identical name and equivalent implementation. * `CCONST` -> constexpr function template with identical name and implementation. * `F_OPEN_UTF8` -> Function with identical name and equivalent implementation. * `LADSPA_PATH_SEPARATOR` -> constexpr char with identical name and value. * `UI_CTRL_KEY` -> constexpr char* with identical name and value. * `ALIGN_SIZE` -> Renamed to `LMMS_ALIGN_SIZE` and converted from a macro to a constexpr size_t. * `JACK_MIDI_BUFFER_MAX` -> constexpr size_t with identical name and value. * versioninfo.h: `PLATFORM`, `MACHINE` and `COMPILER_VERSION` -> prefixed with `LMMS_BUILDCONF_` and converted from macros to constexpr char* literals. * Header guard _OSCILLOSCOPE -> renamed to OSCILLOSCOPE_H * Header guard _TIME_DISPLAY_WIDGET -> renamed to TIME_DISPLAY_WIDGET_H * C-style typecasts in DrumSynth.cpp have been replaced with `static_cast`. * constexpr numerical constants are initialized with assignment notation instead of curly brace intializers. * In portsmf, `Alg_seq::operator[]` will throw an exception instead of returning null if the operator index is out of range. Additionally, in many places, global constants that were declared as `const T foo = bar;` were changed from const to constexpr, leaving them const and making them potentially evaluable at compile time. Some macros that only appeared in single source files and were unused in those files have been removed entirely.
1504 lines
43 KiB
C++
1504 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 "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;
|
|
}
|