diff --git a/include/TimeLineWidget.h b/include/TimeLineWidget.h index c7ac0124c9e..38ebd2857cd 100644 --- a/include/TimeLineWidget.h +++ b/include/TimeLineWidget.h @@ -190,9 +190,14 @@ public slots: void mousePressEvent( QMouseEvent * _me ) override; void mouseMoveEvent( QMouseEvent * _me ) override; void mouseReleaseEvent( QMouseEvent * _me ) override; + void contextMenuEvent( QContextMenuEvent * _cme ) override; private: + void chooseMouseAction(QMouseEvent* event); + TimePos getPositionFromX(const int x) const; + void setLoopPoint(bool end, int x, bool unquantized=false); + static QPixmap * s_posMarkerPixmap; QColor m_inactiveLoopColor; @@ -217,6 +222,7 @@ public slots: int m_xOffset; int m_posMarkerX; float m_ppb; + float m_snapSize; Song::PlayPos & m_pos; const TimePos & m_begin; const Song::PlayModes m_mode; @@ -232,14 +238,16 @@ public slots: enum actions { NoAction, + Thresholded, MovePositionMarker, MoveLoopBegin, MoveLoopEnd, + MoveLoopClosest, + DragLoop, SelectSongTCO, + ShowContextMenu } m_action; - int m_moveXOff; - signals: void positionChanged( const TimePos & _t ); diff --git a/src/gui/TimeLineWidget.cpp b/src/gui/TimeLineWidget.cpp index e7e7ca113e0..95934de97bc 100644 --- a/src/gui/TimeLineWidget.cpp +++ b/src/gui/TimeLineWidget.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include "TimeLineWidget.h" @@ -62,13 +63,13 @@ TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, m_xOffset( xoff ), m_posMarkerX( 0 ), m_ppb( ppb ), + m_snapSize( 1.0 ), m_pos( pos ), m_begin( begin ), m_mode( mode ), m_savedPos( -1 ), m_hint( NULL ), - m_action( NoAction ), - m_moveXOff( 0 ) + m_action( NoAction ) { m_loopPos[0] = 0; m_loopPos[1] = DefaultTicksPerBar; @@ -93,6 +94,10 @@ TimeLineWidget::TimeLineWidget( const int xoff, const int yoff, const float ppb, updateTimer->start( 1000 / 60 ); // 60 fps connect( Engine::getSong(), SIGNAL( timeSignatureChanged( int,int ) ), this, SLOT( update() ) ); + + // Required to prevent modified right click from triggering the context menu + // Instead, manually trigger the context menu on unmodified right clicks + setContextMenuPolicy(Qt::PreventContextMenu); } @@ -304,60 +309,52 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) -void TimeLineWidget::mousePressEvent( QMouseEvent* event ) +void TimeLineWidget::contextMenuEvent(QContextMenuEvent*) { - if( event->x() < m_xOffset ) - { - return; - } - if( event->button() == Qt::LeftButton && !(event->modifiers() & Qt::ShiftModifier) ) - { - m_action = MovePositionMarker; - if( event->x() - m_xOffset < s_posMarkerPixmap->width() ) - { - m_moveXOff = event->x() - m_xOffset; - } - else - { - m_moveXOff = s_posMarkerPixmap->width() / 2; - } - } - else if( event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier) ) - { - m_action = SelectSongTCO; - m_initalXSelect = event->x(); - } - else if( event->button() == Qt::RightButton ) - { - m_moveXOff = s_posMarkerPixmap->width() / 2; - const TimePos t = m_begin + static_cast( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * TimePos::ticksPerBar() / m_ppb ); - const TimePos loopMid = ( m_loopPos[0] + m_loopPos[1] ) / 2; + QMenu contextMenu(tr("Timeline"), this); + + // TODO: Shortcut hints should be read from a config + + QAction setStartPoint(tr("Loop start (shift + left click)"), this); + connect(&setStartPoint, &QAction::triggered, this, [this](){ + setLoopPoint(0, m_initalXSelect); + }); + QAction setEndPoint(tr("Loop end (shift + right click)"), this); + connect(&setEndPoint, &QAction::triggered, this, [this](){ + setLoopPoint(1, m_initalXSelect); + }); + QAction selectLoopPoints(tr("Select between loop points"), this); + connect(&selectLoopPoints, &QAction::triggered, this, [this](){ + emit regionSelectedFromPixels( + m_xOffset + m_loopPos[0] * m_ppb / TimePos::ticksPerBar(), + m_xOffset + m_loopPos[1] * m_ppb / TimePos::ticksPerBar() + ); + emit selectionFinished(); + }); + contextMenu.addAction(&setStartPoint); + contextMenu.addAction(&setEndPoint); + contextMenu.addAction(&selectLoopPoints); + contextMenu.exec(QCursor::pos()); +} - if( t < loopMid ) - { - m_action = MoveLoopBegin; - } - else if( t > loopMid ) - { - m_action = MoveLoopEnd; - } - if( m_loopPos[0] > m_loopPos[1] ) - { - qSwap( m_loopPos[0], m_loopPos[1] ); - } - m_loopPos[( m_action == MoveLoopBegin ) ? 0 : 1] = t; - } - if( m_action == MoveLoopBegin || m_action == MoveLoopEnd ) - { - delete m_hint; - m_hint = TextFloat::displayMessage( tr( "Hint" ), - tr( "Press <%1> to disable magnetic loop points." ).arg(UI_CTRL_KEY), - embed::getIconPixmap( "hint" ), 0 ); - } - mouseMoveEvent( event ); +void TimeLineWidget::mousePressEvent( QMouseEvent* event ) +{ + auto button = event->button(); + auto mods = event->modifiers(); + + if (event->x() < m_xOffset || m_action != NoAction) { return; } + // Handles moving playhead + else if (button == Qt::LeftButton && mods == Qt::NoModifier) { m_action = MovePositionMarker; } + // Wait for drag or release + else { m_action = Thresholded; } + + // Set initial position for actions that need it + m_initalXSelect = event->x(); + + mouseMoveEvent(event); } @@ -366,14 +363,35 @@ void TimeLineWidget::mousePressEvent( QMouseEvent* event ) void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) { parentWidget()->update(); // essential for widgets that this timeline had taken their mouse move event from. - const TimePos t = m_begin + static_cast( qMax( event->x() - m_xOffset - m_moveXOff, 0 ) * TimePos::ticksPerBar() / m_ppb ); + const TimePos t = getPositionFromX(event->x()); + // Fine adjust when both ctrl and shift are held, hide ctrl+shift hint + bool unquantized = event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier); + if (unquantized) + { + delete m_hint; + m_hint = nullptr; + } - switch( m_action ) + // Change action when mouse exceeds drag threshold + static const int dragThreshold = 5; + if (m_action == Thresholded && abs(event->x() - m_initalXSelect) > dragThreshold) + { + chooseMouseAction(event); + } + + // Translate MoveLoopClosest into left or right based on distance + if (m_action == MoveLoopClosest) + { + const TimePos loopMid = (m_loopPos[0] + m_loopPos[1]) / 2; + m_action = t < loopMid ? MoveLoopBegin : MoveLoopEnd; + } + + switch (m_action) { case MovePositionMarker: m_pos.setTicks(t.getTicks()); Engine::getSong()->setToTime(t, m_mode); - if (!( Engine::getSong()->isPlaying())) + if (!(Engine::getSong()->isPlaying())) { //Song::Mode_None is used when nothing is being played. Engine::getSong()->setToTime(t, Song::Mode_None); @@ -387,38 +405,20 @@ void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) case MoveLoopBegin: case MoveLoopEnd: { - const int i = m_action - MoveLoopBegin; // i == 0 || i == 1 - if( event->modifiers() & Qt::ControlModifier ) - { - // no ctrl-press-hint when having ctrl pressed - delete m_hint; - m_hint = NULL; - m_loopPos[i] = t; - } - else - { - 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 ) - { - m_loopPos[0] -= TimePos::ticksPerBar(); - } - else - { - m_loopPos[1] += TimePos::ticksPerBar(); - } - } + const int i = m_action == MoveLoopBegin ? 0 : 1; + setLoopPoint(i, event->x(), unquantized); + break; + } + case DragLoop: + { + if (unquantized) { m_loopPos[1] = t; } + else { m_loopPos[1] = t.quantize(m_snapSize); } update(); break; } - case SelectSongTCO: - emit regionSelectedFromPixels( m_initalXSelect , event->x() ); - break; + case SelectSongTCO: + emit regionSelectedFromPixels(m_initalXSelect , event->x()); + break; default: break; @@ -432,6 +432,126 @@ void TimeLineWidget::mouseReleaseEvent( QMouseEvent* event ) { delete m_hint; m_hint = NULL; - if ( m_action == SelectSongTCO ) { emit selectionFinished(); } + + // Change action if mouse has not moved + if (m_action == Thresholded) { chooseMouseAction(event); } + + mouseMoveEvent(event); + + switch (m_action) + { + case SelectSongTCO: + emit selectionFinished(); + break; + + case ShowContextMenu: + contextMenuEvent(nullptr); + break; + + default: + break; + } + + // Required when m_action == DragLoop, not harmful otherwise + if (m_loopPos[0] > m_loopPos[1]) { qSwap(m_loopPos[0], m_loopPos[1]); } + m_action = NoAction; +} + + + + +void TimeLineWidget::chooseMouseAction(QMouseEvent* event) +{ + auto buttons = event->button() | event->buttons(); // include released button + auto mods = event->modifiers(); + auto type = event->type(); + + // TODO: Read these from a config + auto leftCtrlAction = SelectSongTCO; + auto rightCtrlAction = NoAction; + auto leftShiftAction = MoveLoopBegin; + auto rightShiftAction = MoveLoopEnd; + + // Unmodified LMB is reserved for playhead, unmodified RMB for context menu + // Shift or Ctrl modified press behavior is bound by the user + // Shift + Ctrl modifier is reserved for fine adjustment of Shift actions + // TODO: Let MMB be bound + m_action = NoAction; + + // If mouse has moved past threshold + if (type == QEvent::MouseMove) + { + if (buttons & Qt::LeftButton) + { + if (mods & Qt::ShiftModifier) { m_action = leftShiftAction; } + else if (mods & Qt::ControlModifier) { m_action = leftCtrlAction; } + } + else if (buttons & Qt::RightButton) + { + if (mods & Qt::ShiftModifier) { m_action = rightShiftAction; } + else if (mods & Qt::ControlModifier) { m_action = rightCtrlAction; } + } + } + // If mouse has not moved + else if (type == QEvent::MouseButtonRelease) + { + if (buttons & Qt::LeftButton) + { + if (mods & Qt::ShiftModifier) { m_action = leftShiftAction; } + } + else if (buttons & Qt::RightButton) + { + if (mods & Qt::ShiftModifier) { m_action = rightShiftAction; } + else if (mods & Qt::ControlModifier) { m_action = rightCtrlAction; } + else { m_action = ShowContextMenu; } + } + } + + // Notify the user if they can disable quantization + bool unquantizable = m_action == MoveLoopBegin || m_action == MoveLoopEnd + || m_action == MoveLoopClosest || m_action == DragLoop; + bool unquantized = mods == (Qt::ControlModifier | Qt::ShiftModifier); + if (unquantizable && !unquantized && type == QEvent::MouseMove) + { + delete m_hint; + m_hint = TextFloat::displayMessage(tr("Hint"), + tr("Hold <%1> and to disable quantization.").arg(UI_CTRL_KEY), + embed::getIconPixmap("hint"), 0); + } +} + + + + +TimePos TimeLineWidget::getPositionFromX(const int x) const +{ + return m_begin + std::max(x - m_xOffset - s_posMarkerPixmap->width() / 2, 0) * TimePos::ticksPerBar() / m_ppb; +} + + + + +void TimeLineWidget::setLoopPoint(bool end, int x, bool unquantized) +{ + // Move the specified loop point + int i = end ? 1 : 0; + TimePos pos = getPositionFromX(x); + m_loopPos[i] = unquantized ? pos : pos.quantize(m_snapSize); + + // Length of quantization step + TimePos oneSnap = TimePos::ticksPerBar() * m_snapSize; + if (m_loopPos[1] == 0) + { + // End may not be at bar 0 + m_loopPos[1] = oneSnap; + } + if (m_loopPos[0] >= m_loopPos[1]) + { + // End moved before start - move start one step back + if (end) { m_loopPos[0] = std::max(0, m_loopPos[1] - oneSnap); } + // Start moved past end - move end one step forward + else { m_loopPos[1] = m_loopPos[0] + oneSnap; } + } + update(); }