From 7835b128e6455522d8bca582910a402002a5ccbb Mon Sep 17 00:00:00 2001 From: Spekular Date: Sat, 27 Jul 2019 11:14:49 +0200 Subject: [PATCH] Enhanced snapping in song editor (#4973) * New default behavior: Preserves offsets when moving clips, resizes in fixed increments. * Adds shift + drag: Snaps move start position (like current behavior) or end position (new), based on which is closest to the real position. When moving a selection, the grabbed clip snaps into position and the rest move relative to it. * Adds alt + drag: Allows fine adjustment of a clip's position or size, as an alternative to ctrl + drag. * Adds a Q dropdown in the song editor to allow finer or coarser snapping (8 bars to 1/16th bar) * Adds a proportional snap toggle. When enabled, snapping size/Q adjusts based on zoom, and a label appears showing the current snap size. This is disabled by default. --- data/themes/classic/proportional_snap.png | Bin 0 -> 263 bytes data/themes/default/proportional_snap.png | Bin 0 -> 263 bytes include/MidiTime.h | 3 +- include/SongEditor.h | 12 +- include/Track.h | 18 +- src/core/Track.cpp | 234 +++++++++++++++------- src/core/midi/MidiTime.cpp | 19 +- src/gui/TimeLineWidget.cpp | 6 +- src/gui/editors/SongEditor.cpp | 125 +++++++++++- src/tracks/SampleTrack.cpp | 5 +- 10 files changed, 329 insertions(+), 93 deletions(-) create mode 100644 data/themes/classic/proportional_snap.png create mode 100644 data/themes/default/proportional_snap.png diff --git a/data/themes/classic/proportional_snap.png b/data/themes/classic/proportional_snap.png new file mode 100644 index 0000000000000000000000000000000000000000..66a0bb0493411d97d9a3e3b22cd779afeb0d89f2 GIT binary patch literal 263 zcmV+i0r>ujP)w8hB+L*W8jWMdbMaYZ01ujP)w8hB+L*W8jWMdbMaYZ01 m_zoomLevels; @@ -141,7 +147,6 @@ private slots: EditMode m_ctrlMode; // mode they were in before they hit ctrl friend class SongEditorWindow; - } ; @@ -170,6 +175,8 @@ protected slots: void lostFocus(); void adjustUiAfterProjectLoad(); + void updateSnapLabel(); + signals: void playTriggered(); void resized(); @@ -181,6 +188,7 @@ protected slots: QAction* m_addBBTrackAction; QAction* m_addSampleTrackAction; QAction* m_addAutomationTrackAction; + QAction* m_setProportionalSnapAction; ActionGroup * m_editModeGroup; QAction* m_drawModeAction; @@ -188,6 +196,8 @@ protected slots: QAction* m_crtlAction; ComboBox * m_zoomingComboBox; + ComboBox * m_snappingComboBox; + QLabel* m_snapSizeLabel; }; #endif diff --git a/include/Track.h b/include/Track.h index 1267d2ef742..b00c5024896 100644 --- a/include/Track.h +++ b/include/Track.h @@ -236,7 +236,7 @@ class TrackContentObjectView : public selectableObject, public ModelView // access needsUpdate member variable bool needsUpdate(); void setNeedsUpdate( bool b ); - + public slots: virtual bool close(); void cut(); @@ -297,6 +297,9 @@ protected slots: Actions m_action; QPoint m_initialMousePos; QPoint m_initialMouseGlobalPos; + MidiTime m_initialTCOPos; + MidiTime m_initialTCOEnd; + QVector m_initialOffsets; TextFloat * m_hint; @@ -311,14 +314,17 @@ protected slots: bool m_gradient; bool m_needsUpdate; - inline void setInitialMousePos( QPoint pos ) + inline void setInitialPos( QPoint pos ) { m_initialMousePos = pos; m_initialMouseGlobalPos = mapToGlobal( pos ); + m_initialTCOPos = m_tco->startPosition(); + m_initialTCOEnd = m_initialTCOPos + m_tco->length(); } + void setInitialOffsets(); bool mouseMovedDistance( QMouseEvent * me, int distance ); - + MidiTime draggedTCOPos( QMouseEvent * me ); } ; @@ -564,13 +570,13 @@ class LMMS_EXPORT Track : public Model, public JournallingObject using Model::dataChanged; - inline int getHeight() + inline int getHeight() { return m_height >= MINIMAL_TRACK_HEIGHT - ? m_height + ? m_height : DEFAULT_TRACK_HEIGHT; } - inline void setHeight( int height ) + inline void setHeight( int height ) { m_height = height; } diff --git a/src/core/Track.cpp b/src/core/Track.cpp index 64c17c9e8ec..fe66ab4a5e5 100644 --- a/src/core/Track.cpp +++ b/src/core/Track.cpp @@ -267,6 +267,9 @@ TrackContentObjectView::TrackContentObjectView( TrackContentObject * tco, m_action( NoAction ), m_initialMousePos( QPoint( 0, 0 ) ), m_initialMouseGlobalPos( QPoint( 0, 0 ) ), + m_initialTCOPos( MidiTime(0) ), + m_initialTCOEnd( MidiTime(0) ), + m_initialOffsets( QVector() ), m_hint( NULL ), m_mutedColor( 0, 0, 0 ), m_mutedBackgroundColor( 0, 0, 0 ), @@ -524,7 +527,7 @@ void TrackContentObjectView::updatePosition() void TrackContentObjectView::dragEnterEvent( QDragEnterEvent * dee ) { TrackContentWidget * tcw = getTrackView()->getTrackContentWidget(); - MidiTime tcoPos = MidiTime( m_tco->startPosition().getTact(), 0 ); + MidiTime tcoPos = MidiTime( m_tco->startPosition() ); if( tcw->canPasteSelection( tcoPos, dee ) == false ) { dee->ignore(); @@ -563,7 +566,7 @@ void TrackContentObjectView::dropEvent( QDropEvent * de ) if( m_trackView->trackContainerView()->allowRubberband() == true ) { TrackContentWidget * tcw = getTrackView()->getTrackContentWidget(); - MidiTime tcoPos = MidiTime( m_tco->startPosition().getTact(), 0 ); + MidiTime tcoPos = MidiTime( m_tco->startPosition() ); if( tcw->pasteSelection( tcoPos, de ) == true ) { de->accept(); @@ -711,7 +714,8 @@ void TrackContentObjectView::paintTextLabel(QString const & text, QPainter & pai */ void TrackContentObjectView::mousePressEvent( QMouseEvent * me ) { - setInitialMousePos( me->pos() ); + setInitialPos( me->pos() ); + setInitialOffsets(); if( !fixedTCOs() && me->button() == Qt::LeftButton ) { if( me->modifiers() & Qt::ControlModifier ) @@ -725,7 +729,9 @@ void TrackContentObjectView::mousePressEvent( QMouseEvent * me ) m_action = ToggleSelected; } } - else if( !me->modifiers() ) + else if( !me->modifiers() + || (me->modifiers() & Qt::AltModifier) + || (me->modifiers() & Qt::ShiftModifier) ) { if( isSelected() ) { @@ -739,7 +745,8 @@ void TrackContentObjectView::mousePressEvent( QMouseEvent * me ) // move or resize m_tco->setJournalling( false ); - setInitialMousePos( me->pos() ); + setInitialPos( me->pos() ); + setInitialOffsets(); SampleTCO * sTco = dynamic_cast( m_tco ); if( me->x() < RESIZE_GRIP_WIDTH && sTco @@ -889,76 +896,86 @@ void TrackContentObjectView::mouseMoveEvent( QMouseEvent * me ) const float ppt = m_trackView->trackContainerView()->pixelsPerTact(); if( m_action == Move ) { - const int x = mapToParent( me->pos() ).x() - m_initialMousePos.x(); - MidiTime t = qMax( 0, (int) - m_trackView->trackContainerView()->currentPosition()+ - static_cast( x * MidiTime::ticksPerTact() / - ppt ) ); - if( ! ( me->modifiers() & Qt::ControlModifier ) - && me->button() == Qt::NoButton ) - { - t = t.toNearestTact(); - } - m_tco->movePosition( t ); + MidiTime newPos = draggedTCOPos( me ); + + // Don't go left of bar zero + newPos = max( 0, newPos.getTicks() ); + m_tco->movePosition( newPos ); m_trackView->getTrackContentWidget()->changePosition(); s_textFloat->setText( QString( "%1:%2" ). - arg( m_tco->startPosition().getTact() + 1 ). - arg( m_tco->startPosition().getTicks() % + arg( newPos.getTact() + 1 ). + arg( newPos.getTicks() % MidiTime::ticksPerTact() ) ); s_textFloat->moveGlobal( this, QPoint( width() + 2, height() + 2 ) ); } else if( m_action == MoveSelection ) { - const int dx = me->x() - m_initialMousePos.x(); - const bool snap = !(me->modifiers() & Qt::ControlModifier) && - me->button() == Qt::NoButton; + // 1: Find the position we want to move the grabbed TCO to + MidiTime newPos = draggedTCOPos( me ); + + // 2: Handle moving the other selected TCOs the same distance QVector so = m_trackView->trackContainerView()->selectedObjects(); - QVector tcos; - int smallestPos = 0; - MidiTime dtick = MidiTime( static_cast( dx * - MidiTime::ticksPerTact() / ppt ) ); - if( snap ) - { - dtick = dtick.toNearestTact(); - } - // find out smallest position of all selected objects for not - // moving an object before zero + 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 == NULL ) - { - continue; - } - TrackContentObject * tco = tcov->m_tco; - tcos.push_back( tco ); - smallestPos = qMin( smallestPos, - (int)tco->startPosition() + dtick ); - } - dtick -= smallestPos; - if( snap ) - { - dtick = dtick.toAbsoluteTact(); // round toward 0 + if( tcov == NULL ) { continue; } + tcos.push_back( tcov->m_tco ); + int index = std::distance( so.begin(), it ); + leftmost = 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 ) { - ( *it )->movePosition( ( *it )->startPosition() + dtick ); + int index = std::distance( tcos.begin(), it ); + ( *it )->movePosition( newPos + m_initialOffsets[index] ); } } else if( m_action == Resize || m_action == ResizeLeft ) { + // If the user is holding alt, or pressed ctrl after beginning the drag, don't quantize + const bool unquantized = (me->modifiers() & Qt::ControlModifier) || (me->modifiers() & Qt::AltModifier); + const float snapSize = gui->songEditor()->m_editor->getSnapSize(); + // Length in ticks of one snap increment + const MidiTime snapLength = MidiTime( (int)(snapSize * MidiTime::ticksPerTact()) ); + if( m_action == Resize ) { - MidiTime t = qMax( MidiTime::ticksPerTact() / 16, static_cast( me->x() * MidiTime::ticksPerTact() / ppt ) ); - if( ! ( me->modifiers() & Qt::ControlModifier ) && me->button() == Qt::NoButton ) - { - t = qMax( MidiTime::ticksPerTact(), t.toNearestTact() ); + // The clip's new length + MidiTime l = static_cast( me->x() * MidiTime::ticksPerTact() / ppt ); + + if ( unquantized ) + { // 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 + MidiTime end = MidiTime( m_initialTCOPos + l ).quantize( snapSize ); + // The end position has to be after the clip's start + MidiTime 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 + MidiTime initialLength = m_initialTCOEnd - m_initialTCOPos; + MidiTime offset = MidiTime( l - initialLength ).quantize( snapSize ); + // Don't resize to less than 1 tick + MidiTime min = MidiTime( initialLength % snapLength ); + if (min < 1) min += snapLength; + m_tco->changeLength( qMax( min, initialLength + offset) ); } - m_tco->changeLength( t ); } else { @@ -969,15 +986,34 @@ void TrackContentObjectView::mouseMoveEvent( QMouseEvent * me ) MidiTime t = qMax( 0, (int) m_trackView->trackContainerView()->currentPosition()+ - static_cast( x * MidiTime::ticksPerTact() / - ppt ) ); - if( ! ( me->modifiers() & Qt::ControlModifier ) - && me->button() == Qt::NoButton ) - { - t = t.toNearestTact(); + static_cast( x * MidiTime::ticksPerTact() / ppt ) ); + + if( unquantized ) + { // 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 + MidiTime 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 + MidiTime initialLength = m_initialTCOEnd - m_initialTCOPos; + MidiTime minLength = MidiTime( initialLength % snapLength ); + if (minLength < 1) minLength += snapLength; + MidiTime offset = MidiTime(t - m_initialTCOPos).quantize( snapSize ); + t = qMin( m_initialTCOEnd - minLength, m_initialTCOPos + offset ); } + MidiTime oldPos = m_tco->startPosition(); - if( m_tco->length() + ( oldPos - t ) >= MidiTime::ticksPerTact() ) + if( m_tco->length() + ( oldPos - t ) >= 1 ) { m_tco->movePosition( t ); m_trackView->getTrackContentWidget()->changePosition(); @@ -1091,7 +1127,6 @@ void TrackContentObjectView::contextMenuEvent( QContextMenuEvent * cme ) - /*! \brief How many pixels a tact (bar) takes for this trackContentObjectView. * * \return the number of pixels per tact (bar). @@ -1102,6 +1137,27 @@ float TrackContentObjectView::pixelsPerTact() } +/*! \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 == NULL ) + { + 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. @@ -1118,6 +1174,49 @@ bool TrackContentObjectView::mouseMovedDistance( QMouseEvent * me, int distance +/*! \brief Calculate the new position of a dragged TCO from a mouse event + * + * + * \param me The QMouseEvent + */ +MidiTime TrackContentObjectView::draggedTCOPos( QMouseEvent * me ) +{ + //Pixels per tact + const float ppt = m_trackView->trackContainerView()->pixelsPerTact(); + // The pixel distance that the mouse has moved + const int mouseOff = mapToGlobal(me->pos()).x() - m_initialMouseGlobalPos.x(); + MidiTime newPos = m_initialTCOPos + mouseOff * MidiTime::ticksPerTact() / ppt; + MidiTime 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 + || (me->modifiers() & Qt::ControlModifier) + || (me->modifiers() & Qt::AltModifier) ) + { + // 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 + MidiTime startQ = newPos.quantize( gui->songEditor()->m_editor->getSnapSize() ); + // Find start position that gives snapped clip end position + MidiTime endQ = ( newPos + m_tco->length() ); + endQ = endQ.quantize( gui->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( gui->songEditor()->m_editor->getSnapSize() ); + } + return newPos; +} + + + // =========================================================================== // trackContentWidget @@ -1496,7 +1595,6 @@ bool TrackContentWidget::pasteSelection( MidiTime tcoPos, QDropEvent * de ) int initialTrackIndex = tiAttr.value().toInt(); QDomAttr tcoPosAttr = metadata.attributeNode( "grabbedTCOPos" ); MidiTime grabbedTCOPos = tcoPosAttr.value().toInt(); - MidiTime grabbedTCOTact = MidiTime( grabbedTCOPos.getTact(), 0 ); // Snap the mouse position to the beginning of the dropped tact, in ticks const TrackContainer::TrackList tracks = getTrack()->trackContainer()->tracks(); @@ -1517,6 +1615,10 @@ bool TrackContentWidget::pasteSelection( MidiTime tcoPos, QDropEvent * de ) // TODO -- Need to draw the hovericon either way, or ghost the TCOs // onto their final position. + // All patterns should be offset the same amount as the grabbed pattern + // The offset is quantized (rather than the positions) to preserve fine adjustments + int offset = MidiTime(tcoPos - grabbedTCOPos).quantize(gui->songEditor()->m_editor->getSnapSize()); + for( int i = 0; isongEditor()->m_editor->getSnapSize(); + if (offset == 0) { pos += shift; } TrackContentObject * tco = t->createTCO( pos ); tco->restoreState( tcoElement ); @@ -1562,7 +1662,7 @@ bool TrackContentWidget::pasteSelection( MidiTime tcoPos, QDropEvent * de ) */ void TrackContentWidget::dropEvent( QDropEvent * de ) { - MidiTime tcoPos = MidiTime( getPosition( de->pos().x() ).getTact(), 0 ); + MidiTime tcoPos = MidiTime( getPosition( de->pos().x() ) ); if( pasteSelection( tcoPos, de ) == true ) { de->accept(); diff --git a/src/core/midi/MidiTime.cpp b/src/core/midi/MidiTime.cpp index b4607aee9c3..82ed642ba78 100644 --- a/src/core/midi/MidiTime.cpp +++ b/src/core/midi/MidiTime.cpp @@ -63,13 +63,18 @@ MidiTime::MidiTime( const tick_t ticks ) : { } -MidiTime MidiTime::toNearestTact() const -{ - if( m_ticks % s_ticksPerTact >= s_ticksPerTact/2 ) - { - return ( getTact() + 1 ) * s_ticksPerTact; - } - return getTact() * s_ticksPerTact; +MidiTime MidiTime::quantize(float bars) const +{ + //The intervals we should snap to, our new position should be a factor of this + int interval = s_ticksPerTact * bars; + //The lower position we could snap to + int lowPos = m_ticks / interval; + //Offset from the lower position + int offset = m_ticks % interval; + //1 if we should snap up, 0 if we shouldn't + int snapUp = offset / (interval / 2); + + return (lowPos + snapUp) * interval; } diff --git a/src/gui/TimeLineWidget.cpp b/src/gui/TimeLineWidget.cpp index 7c7f48c4ef3..87e513e7242 100644 --- a/src/gui/TimeLineWidget.cpp +++ b/src/gui/TimeLineWidget.cpp @@ -2,7 +2,7 @@ * TimeLineWidget.cpp - class timeLine, representing a time-line with position marker * * 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 @@ -384,14 +384,14 @@ void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) } else { - m_loopPos[i] = t.toNearestTact(); + m_loopPos[i] = t.quantize(1.0); } // Catch begin == end if( m_loopPos[0] == m_loopPos[1] ) { // Note, swap 1 and 0 below and the behavior "skips" the other // marking instead of pushing it. - if( m_action == MoveLoopBegin ) + if( m_action == MoveLoopBegin ) m_loopPos[0] -= MidiTime::ticksPerTact(); else m_loopPos[1] += MidiTime::ticksPerTact(); diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index b397434b102..6e23fcdbef7 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -76,11 +76,14 @@ SongEditor::SongEditor( Song * song ) : TrackContainerView( song ), m_song( song ), m_zoomingModel(new ComboBoxModel()), + m_snappingModel(new ComboBoxModel()), + m_proportionalSnap( false ), m_scrollBack( false ), m_smoothScroll( ConfigManager::inst()->value( "ui", "smoothscroll" ).toInt() ), m_mode(DrawMode) { m_zoomingModel->setParent(this); + m_snappingModel->setParent(this); // create time-line m_widgetWidthTotal = ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt()==1 ? @@ -230,7 +233,7 @@ SongEditor::SongEditor( Song * song ) : connect( m_song, SIGNAL( lengthChanged( int ) ), this, SLOT( updateScrollBar( int ) ) ); - // Set up zooming model + //Set up zooming model for( float const & zoomLevel : m_zoomLevels ) { m_zoomingModel->addItem( QString( "%1\%" ).arg( zoomLevel * 100 ) ); @@ -240,6 +243,24 @@ SongEditor::SongEditor( Song * song ) : connect( m_zoomingModel, SIGNAL( dataChanged() ), this, SLOT( zoomingChanged() ) ); + //Set up snapping model, 2^i + for ( int i = 3; i >= -4; i-- ) + { + if ( i > 0 ) + { + m_snappingModel->addItem( QString( "%1 Bars").arg( 1 << i ) ); + } + else if ( i == 0 ) + { + m_snappingModel->addItem( "1 Bar" ); + } + else + { + m_snappingModel->addItem( QString( "1/%1 Bar" ).arg( 1 << (-i) ) ); + } + } + m_snappingModel->setInitValue( m_snappingModel->findText( "1 Bar" ) ); + setFocusPolicy( Qt::StrongFocus ); setFocus(); } @@ -264,6 +285,48 @@ void SongEditor::loadSettings( const QDomElement& element ) +float SongEditor::getSnapSize() const +{ + // 1 Bar is the third value in the snapping dropdown + int val = -m_snappingModel->value() + 3; + // If proportional snap is on, we snap to finer values when zoomed in + if (m_proportionalSnap) + { + val = val - m_zoomingModel->value() + 3; + } + val = max(val, -6); // -6 gives 1/64th bar snapping. Lower values cause crashing. + + if ( val >= 0 ){ + return 1 << val; + } + else { + return 1.0 / ( 1 << -val ); + } +} + +QString SongEditor::getSnapSizeString() const +{ + int val = -m_snappingModel->value() + 3; + val = val - m_zoomingModel->value() + 3; + val = max(val, -6); // -6 gives 1/64th bar snapping. Lower values cause crashing. + + if ( val >= 0 ){ + int bars = 1 << val; + if ( bars == 1 ) { return QString("1 Bar"); } + else + { + return QString( "%1 Bars" ).arg(bars); + } + } + else { + int div = ( 1 << -val ); + return QString( "1/%1 Bar" ).arg(div); + } +} + + + + void SongEditor::setHighQuality( bool hq ) { Engine::mixer()->changeQuality( Mixer::qualitySettings( @@ -298,6 +361,11 @@ void SongEditor::setEditModeSelect() setEditMode(SelectMode); } +void SongEditor::toggleProportionalSnap() +{ + m_proportionalSnap = !m_proportionalSnap; +} + @@ -653,10 +721,19 @@ ComboBoxModel *SongEditor::zoomingModel() const +ComboBoxModel *SongEditor::snappingModel() const +{ + return m_snappingModel; +} + + + + SongEditorWindow::SongEditorWindow(Song* song) : Editor(Engine::mixer()->audioDev()->supportsCapture(), false), m_editor(new SongEditor(song)), - m_crtlAction( NULL ) + m_crtlAction( NULL ), + m_snapSizeLabel( new QLabel( m_toolBar ) ) { setWindowTitle( tr( "Song-Editor" ) ); setWindowIcon( embed::getIconPixmap( "songeditor" ) ); @@ -718,23 +795,63 @@ SongEditorWindow::SongEditorWindow(Song* song) : QLabel * zoom_lbl = new QLabel( m_toolBar ); zoom_lbl->setPixmap( embed::getIconPixmap( "zoom" ) ); - // setup zooming-stuff + //Set up zooming-stuff m_zoomingComboBox = new ComboBox( m_toolBar ); m_zoomingComboBox->setFixedSize( 80, 22 ); m_zoomingComboBox->move( 580, 4 ); m_zoomingComboBox->setModel(m_editor->m_zoomingModel); m_zoomingComboBox->setToolTip(tr("Horizontal zooming")); + connect(m_editor->zoomingModel(), SIGNAL(dataChanged()), this, SLOT(updateSnapLabel())); zoomToolBar->addWidget( zoom_lbl ); zoomToolBar->addWidget( m_zoomingComboBox ); + DropToolBar *snapToolBar = addDropToolBarToTop(tr("Snap controls")); + QLabel * snap_lbl = new QLabel( m_toolBar ); + snap_lbl->setPixmap( embed::getIconPixmap( "quantize" ) ); + + //Set up quantization/snapping selector + m_snappingComboBox = new ComboBox( m_toolBar ); + m_snappingComboBox->setFixedSize( 80, 22 ); + m_snappingComboBox->setModel(m_editor->m_snappingModel); + m_snappingComboBox->setToolTip(tr("Clip snapping size")); + connect(m_editor->snappingModel(), SIGNAL(dataChanged()), this, SLOT(updateSnapLabel())); + + m_setProportionalSnapAction = new QAction(embed::getIconPixmap("proportional_snap"), + tr("Toggle proportional snap on/off"), this); + m_setProportionalSnapAction->setCheckable(true); + m_setProportionalSnapAction->setChecked(false); + connect(m_setProportionalSnapAction, SIGNAL(triggered()), m_editor, SLOT(toggleProportionalSnap())); + connect(m_setProportionalSnapAction, SIGNAL(triggered()), this, SLOT(updateSnapLabel()) ); + + snapToolBar->addWidget( snap_lbl ); + snapToolBar->addWidget( m_snappingComboBox ); + snapToolBar->addSeparator(); + snapToolBar->addAction( m_setProportionalSnapAction ); + + snapToolBar->addSeparator(); + snapToolBar->addWidget( m_snapSizeLabel ); + connect(song, SIGNAL(projectLoaded()), this, SLOT(adjustUiAfterProjectLoad())); connect(this, SIGNAL(resized()), m_editor, SLOT(updatePositionLine())); } QSize SongEditorWindow::sizeHint() const { - return {600, 300}; + return {720, 300}; +} + +void SongEditorWindow::updateSnapLabel(){ + if (m_setProportionalSnapAction->isChecked()) + { + m_snapSizeLabel->setText(QString("Snap: ") + m_editor->getSnapSizeString()); + m_snappingComboBox->setToolTip(tr("Base snapping size")); + } + else + { + m_snappingComboBox->setToolTip(tr("Clip snapping size")); + m_snapSizeLabel->clear(); + } } diff --git a/src/tracks/SampleTrack.cpp b/src/tracks/SampleTrack.cpp index ea3c5360249..4b51ef6ec65 100644 --- a/src/tracks/SampleTrack.cpp +++ b/src/tracks/SampleTrack.cpp @@ -529,7 +529,7 @@ void SampleTCOView::paintEvent( QPaintEvent * pe ) float nom = Engine::getSong()->getTimeSigModel().getNumerator(); float den = Engine::getSong()->getTimeSigModel().getDenominator(); float ticksPerTact = DefaultTicksPerTact * nom / den; - + float offset = m_tco->startTimeOffset() / ticksPerTact * pixelsPerTact(); QRect r = QRect( TCO_BORDER_WIDTH + offset, spacing, qMax( static_cast( m_tco->sampleLength() * ppt / ticksPerTact ), 1 ), rect().bottom() - 2 * spacing ); @@ -931,7 +931,7 @@ void SampleTrackView::dropEvent(QDropEvent *de) ? MidiTime(0) : MidiTime(((xPos - trackHeadWidth) / trackContainerView()->pixelsPerTact() * MidiTime::ticksPerTact()) + trackContainerView()->currentPosition() - ).toNearestTact(); + ).quantize(1.0); SampleTCO * sTco = static_cast(getTrack()->createTCO(tcoPos)); if (sTco) { sTco->setSampleFile(value); } @@ -1192,4 +1192,3 @@ void SampleTrackWindow::loadSettings(const QDomElement& element) m_stv->m_tlb->setChecked(true); } } -