/* * TrackContentObjectView.cpp - implementation of TrackContentObjectView class * * Copyright (c) 2004-2014 Tobias Doerffel * * 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 #include #include #include #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() ), 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( 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 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( 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(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 & tcoViews) const { Track * t = m_trackView->getTrack(); TrackContainer * tc = t->trackContainer(); DataFile dataFile( DataFile::DragNDropData ); QDomElement tcoParent = dataFile.createElement( "tcos" ); typedef QVector 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() : getClickedTCOs(); setInitialPos( me->pos() ); setInitialOffsets(); if( !fixedTCOs() && me->button() == Qt::LeftButton ) { SampleTCO * sTco = dynamic_cast( 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( 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 tcoViews; if( m_action == CopySelection ) { // Collect all selected TCOs QVector so = m_trackView->trackContainerView()->selectedObjects(); for( auto it = so.begin(); it != so.end(); ++it ) { TrackContentObjectView * tcov = dynamic_cast( *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 so = m_trackView->trackContainerView()->selectedObjects(); QVector tcos; // List of selected clips int leftmost = 0; // Leftmost clip's offset from grabbed clip // Populate tcos, find leftmost for( QVector::iterator it = so.begin(); it != so.end(); ++it ) { TrackContentObjectView * tcov = dynamic_cast( *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::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( 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( 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(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( min, initialLength + offset) ); } } else { SampleTCO * sTco = dynamic_cast( 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( x * TimePos::ticksPerBar() / ppb ) ); if( unquantizedModHeld(me) ) { // We want to preserve this adjusted offset, // even if the user switches to snapping later setInitialPos( m_initialMousePos ); //Don't resize to less than 1 tick t = qMin( m_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( 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( 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( 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 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 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::getClickedTCOs() { // Get a list of selected selectableObjects QVector sos = getGUI()->songEditor()->m_editor->selectedObjects(); // Convert to a list of selected TCOVs QVector selection; selection.reserve( sos.size() ); for( auto so: sos ) { TrackContentObjectView *tcov = dynamic_cast ( 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( 1, this ); } void TrackContentObjectView::remove( QVector 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 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 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 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 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(tcovs.at(0)->getTrackView()); // Then we create a set with all the TCOs owners std::set 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 tcovs) { // Get the track that we are merging TCOs in InstrumentTrack* track = dynamic_cast(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(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(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 so = m_trackView->trackContainerView()->selectedObjects(); QVector offsets; for( QVector::iterator it = so.begin(); it != so.end(); ++it ) { TrackContentObjectView * tcov = dynamic_cast( *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; }