diff --git a/data/themes/classic/cursor_select_left.png b/data/themes/classic/cursor_select_left.png new file mode 100644 index 00000000000..eaa80e0bbe3 Binary files /dev/null and b/data/themes/classic/cursor_select_left.png differ diff --git a/data/themes/classic/cursor_select_right.png b/data/themes/classic/cursor_select_right.png new file mode 100644 index 00000000000..abd4aecfb56 Binary files /dev/null and b/data/themes/classic/cursor_select_right.png differ diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 505ab348340..9eeb41993be 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -663,15 +663,25 @@ lmms--gui--TimeLineWidget { background-color: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #8796a7, stop: 1.0 #3e454e ); - qproperty-inactiveLoopColor: rgba( 52, 63, 53, 64 ); + qproperty-inactiveLoopColor: rgba( 52, 63, 53, 64 ); qproperty-inactiveLoopBrush: rgba( 255, 255, 255, 32 ); qproperty-inactiveLoopInnerColor: rgba( 255, 255, 255, 32 ); + qproperty-inactiveLoopHandleColor: rgba( 192, 192, 192, 100 ); qproperty-activeLoopColor: rgba( 52, 63, 53, 255 ); qproperty-activeLoopBrush: qlineargradient( x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #378d59, stop: 1.0 #297e36 ); qproperty-activeLoopInnerColor: rgba( 74, 155, 100, 255 ); + qproperty-activeLoopHandleColor: rgba( 192, 192, 192, 200 ); + + /* Width of loop marker handles (when handle mode is active) */ + qproperty-loopHandleWidth: 8; + qproperty-barLineColor: rgb( 192, 192, 192 ); qproperty-barNumberColor: rgb( 192, 192, 192 ); + + /* Cursor hotspots for loop marker adjustment */ + qproperty-mouseHotspotSelLeft: 0px 16px; + qproperty-mouseHotspotSelRight: 32px 16px; } QTreeView { diff --git a/data/themes/default/cursor_select_left.png b/data/themes/default/cursor_select_left.png new file mode 100644 index 00000000000..eaa80e0bbe3 Binary files /dev/null and b/data/themes/default/cursor_select_left.png differ diff --git a/data/themes/default/cursor_select_right.png b/data/themes/default/cursor_select_right.png new file mode 100644 index 00000000000..abd4aecfb56 Binary files /dev/null and b/data/themes/default/cursor_select_right.png differ diff --git a/data/themes/default/style.css b/data/themes/default/style.css index e05d526533b..7963f51a417 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -704,23 +704,32 @@ lmms--gui--TimeLineWidget { /* Properties for the loop indicator rectangle in inactive state: - LoopColor: Color of the outermost border - LoopBrush: Brush to paint the main portion of the rectangle - - LoopInnerColor: Color used to paint the inlayed border */ + - LoopInnerColor: Color used to paint the inlayed border + - LoopHandleColor: Color used to paint loop marker handles */ qproperty-inactiveLoopColor: #3B424A; qproperty-inactiveLoopBrush: #3B424A; qproperty-inactiveLoopInnerColor: #3B424A; + qproperty-inactiveLoopHandleColor: rgba( 192, 192, 192, 100 ); /* Properties for the loop indicator rectangle in active state. See above for detailed description. */ qproperty-activeLoopColor: #21A14F; qproperty-activeLoopBrush: #21A14F; qproperty-activeLoopInnerColor: #21A14F; + qproperty-activeLoopHandleColor: rgba( 192, 192, 192, 200 ); /* Vertical padding for the loop indicator rectangle. A value of zero draws the rectangle at the full height of the widget. */ qproperty-loopRectangleVerticalPadding: 1; + /* Width of loop marker handles (when handle mode is active) */ + qproperty-loopHandleWidth: 8; qproperty-barLineColor: rgb( 192, 192, 192 ); qproperty-barNumberColor: rgb( 192, 192, 192 ); + + /* Cursor hotspots for loop marker adjustment */ + qproperty-mouseHotspotSelLeft: 0px 16px; + qproperty-mouseHotspotSelRight: 32px 16px; } lmms--gui--TrackContainerView QLabel diff --git a/include/SetupDialog.h b/include/SetupDialog.h index 882ca2bedce..ce81bb47778 100644 --- a/include/SetupDialog.h +++ b/include/SetupDialog.h @@ -86,6 +86,7 @@ private slots: void toggleMMPZ(bool enabled); void toggleDisableBackup(bool enabled); void toggleOpenLastProject(bool enabled); + void loopMarkerModeChanged(); void setLanguage(int lang); // Performance settings widget. @@ -147,6 +148,8 @@ private slots: bool m_MMPZ; bool m_disableBackup; bool m_openLastProject; + QString m_loopMarkerMode; + QComboBox* m_loopMarkerComboBox; QString m_lang; QStringList m_languages; diff --git a/include/TimeLineWidget.h b/include/TimeLineWidget.h index c87458e6c22..5c683cfd9f9 100644 --- a/include/TimeLineWidget.h +++ b/include/TimeLineWidget.h @@ -25,6 +25,10 @@ #ifndef LMMS_GUI_TIMELINE_WIDGET_H #define LMMS_GUI_TIMELINE_WIDGET_H +#include + +#include +#include #include #include "Song.h" @@ -57,10 +61,15 @@ class TimeLineWidget : public QWidget Q_PROPERTY( QColor inactiveLoopColor READ getInactiveLoopColor WRITE setInactiveLoopColor ) Q_PROPERTY( QBrush inactiveLoopBrush READ getInactiveLoopBrush WRITE setInactiveLoopBrush ) Q_PROPERTY( QColor inactiveLoopInnerColor READ getInactiveLoopInnerColor WRITE setInactiveLoopInnerColor ) + Q_PROPERTY(QColor inactiveLoopHandleColor MEMBER m_inactiveLoopHandleColor) Q_PROPERTY( QColor activeLoopColor READ getActiveLoopColor WRITE setActiveLoopColor ) Q_PROPERTY( QBrush activeLoopBrush READ getActiveLoopBrush WRITE setActiveLoopBrush ) Q_PROPERTY( QColor activeLoopInnerColor READ getActiveLoopInnerColor WRITE setActiveLoopInnerColor ) + Q_PROPERTY(QColor activeLoopHandleColor MEMBER m_activeLoopHandleColor) Q_PROPERTY( int loopRectangleVerticalPadding READ getLoopRectangleVerticalPadding WRITE setLoopRectangleVerticalPadding ) + Q_PROPERTY(int loopHandleWidth MEMBER m_loopHandleWidth) + Q_PROPERTY(QSize mouseHotspotSelLeft READ mouseHotspotSelLeft WRITE setMouseHotspotSelLeft) + Q_PROPERTY(QSize mouseHotspotSelRight READ mouseHotspotSelRight WRITE setMouseHotspotSelRight) enum class AutoScrollState { @@ -99,6 +108,28 @@ class TimeLineWidget : public QWidget inline int const & getLoopRectangleVerticalPadding() const { return m_loopRectangleVerticalPadding; } inline void setLoopRectangleVerticalPadding(int const & loopRectangleVerticalPadding) { m_loopRectangleVerticalPadding = loopRectangleVerticalPadding; } + auto mouseHotspotSelLeft() const -> QSize + { + const auto point = m_cursorSelectLeft.hotSpot(); + return QSize{point.x(), point.y()}; + } + + void setMouseHotspotSelLeft(const QSize& s) + { + m_cursorSelectLeft = QCursor{m_cursorSelectLeft.pixmap(), s.width(), s.height()}; + } + + auto mouseHotspotSelRight() const -> QSize + { + const auto point = m_cursorSelectRight.hotSpot(); + return QSize{point.x(), point.y()}; + } + + void setMouseHotspotSelRight(const QSize& s) + { + m_cursorSelectRight = QCursor{m_cursorSelectRight.pixmap(), s.width(), s.height()}; + } + inline Song::PlayPos & pos() { return( m_pos ); @@ -143,51 +174,64 @@ public slots: void mousePressEvent( QMouseEvent * _me ) override; void mouseMoveEvent( QMouseEvent * _me ) override; void mouseReleaseEvent( QMouseEvent * _me ) override; - + void contextMenuEvent(QContextMenuEvent* event) override; private: + enum class Action + { + NoAction, + MovePositionMarker, + MoveLoopBegin, + MoveLoopEnd, + MoveLoop, + SelectSongClip, + }; + + auto getClickedTime(int xPosition) const -> TimePos; + auto getLoopAction(QMouseEvent* event) const -> Action; + auto actionCursor(Action action) const -> QCursor; + QPixmap m_posMarkerPixmap = embed::getIconPixmap("playpos_marker"); - QColor m_inactiveLoopColor; - QBrush m_inactiveLoopBrush; - QColor m_inactiveLoopInnerColor; + QColor m_inactiveLoopColor = QColor{52, 63, 53, 64}; + QBrush m_inactiveLoopBrush = QColor{255, 255, 255, 32}; + QColor m_inactiveLoopInnerColor = QColor{255, 255, 255, 32}; + QColor m_inactiveLoopHandleColor = QColor{255, 255, 255, 32}; - QColor m_activeLoopColor; - QBrush m_activeLoopBrush; - QColor m_activeLoopInnerColor; + QColor m_activeLoopColor = QColor{52, 63, 53, 255}; + QBrush m_activeLoopBrush = QColor{55, 141, 89}; + QColor m_activeLoopInnerColor = QColor{74, 155, 100, 255}; + QColor m_activeLoopHandleColor = QColor{74, 155, 100, 255}; - int m_loopRectangleVerticalPadding; + int m_loopRectangleVerticalPadding = 1; + int m_loopHandleWidth = 5; - QColor m_barLineColor; - QColor m_barNumberColor; + QColor m_barLineColor = QColor{192, 192, 192}; + QColor m_barNumberColor = m_barLineColor.darker(120); - AutoScrollState m_autoScroll; + QCursor m_cursorSelectLeft = QCursor{embed::getIconPixmap("cursor_select_left"), 0, 16}; + QCursor m_cursorSelectRight = QCursor{embed::getIconPixmap("cursor_select_right"), 32, 16}; - bool m_changedPosition; + AutoScrollState m_autoScroll = AutoScrollState::Enabled; + // Width of the unused region on the widget's left (above track labels or piano) int m_xOffset; - int m_posMarkerX; float m_ppb; - float m_snapSize; + float m_snapSize = 1.f; Song::PlayPos & m_pos; Timeline* m_timeline; + // Leftmost position visible in parent editor const TimePos & m_begin; const Song::PlayMode m_mode; + // When in MoveLoop mode we need the initial positions. Storing only the latest + // position allows for unquantized drag but fails when toggling quantization. + std::array m_oldLoopPos; + TimePos m_dragStartPos; - TextFloat * m_hint; + TextFloat* m_hint = nullptr; int m_initalXSelect; - - enum class Action - { - NoAction, - MovePositionMarker, - MoveLoopBegin, - MoveLoopEnd, - SelectSongClip, - } m_action; - - int m_moveXOff; + Action m_action = Action::NoAction; }; } // namespace lmms::gui diff --git a/src/gui/editors/SongEditor.cpp b/src/gui/editors/SongEditor.cpp index 69a41a7648b..400c03aa90c 100644 --- a/src/gui/editors/SongEditor.cpp +++ b/src/gui/editors/SongEditor.cpp @@ -806,7 +806,7 @@ void SongEditor::updatePosition( const TimePos & t ) m_scrollBack = false; } - const int x = m_timeLine->markerX(t) + 8; + const int x = m_timeLine->markerX(t); if( x >= trackOpWidth + widgetWidth -1 ) { m_positionLine->show(); diff --git a/src/gui/editors/TimeLineWidget.cpp b/src/gui/editors/TimeLineWidget.cpp index f77361a913d..7657e2916cd 100644 --- a/src/gui/editors/TimeLineWidget.cpp +++ b/src/gui/editors/TimeLineWidget.cpp @@ -27,11 +27,14 @@ #include #include +#include +#include #include #include #include #include +#include "ConfigManager.h" #include "embed.h" #include "GuiApplication.h" #include "NStateButton.h" @@ -47,35 +50,17 @@ namespace TimeLineWidget::TimeLineWidget(const int xoff, const int yoff, const float ppb, Song::PlayPos& pos, Timeline& timeline, const TimePos& begin, Song::PlayMode mode, QWidget* parent) : - QWidget( parent ), - m_inactiveLoopColor( 52, 63, 53, 64 ), - m_inactiveLoopBrush( QColor( 255, 255, 255, 32 ) ), - m_inactiveLoopInnerColor( 255, 255, 255, 32 ), - m_activeLoopColor( 52, 63, 53, 255 ), - m_activeLoopBrush( QColor( 55, 141, 89 ) ), - m_activeLoopInnerColor( 74, 155, 100, 255 ), - m_loopRectangleVerticalPadding( 1 ), - m_barLineColor( 192, 192, 192 ), - m_barNumberColor( m_barLineColor.darker( 120 ) ), - m_autoScroll( AutoScrollState::Enabled ), - m_changedPosition( true ), - m_xOffset( xoff ), - m_posMarkerX( 0 ), - m_ppb( ppb ), - m_snapSize( 1.0 ), - m_pos( pos ), + QWidget{parent}, + m_xOffset{xoff}, + m_ppb{ppb}, + m_pos{pos}, m_timeline{&timeline}, - m_begin( begin ), - m_mode( mode ), - m_hint( nullptr ), - m_action( Action::NoAction ), - m_moveXOff( 0 ) + m_begin{begin}, + m_mode{mode} { setAttribute( Qt::WA_OpaquePaintEvent, true ); move( 0, yoff ); - m_xOffset -= m_posMarkerPixmap.width() / 2; - setMouseTracking(true); auto updateTimer = new QTimer(this); @@ -98,12 +83,8 @@ TimeLineWidget::~TimeLineWidget() void TimeLineWidget::setXOffset(const int x) { m_xOffset = x; - m_xOffset -= m_posMarkerPixmap.width() / 2; } - - - void TimeLineWidget::addToolButtons( QToolBar * _tool_bar ) { auto autoScroll = new NStateButton(_tool_bar); @@ -151,15 +132,8 @@ void TimeLineWidget::addToolButtons( QToolBar * _tool_bar ) void TimeLineWidget::updatePosition() { - const int new_x = markerX( m_pos ); - - if( new_x != m_posMarkerX ) - { - m_posMarkerX = new_x; - m_changedPosition = true; - emit positionChanged( m_pos ); - update(); - } + emit positionChanged(m_pos); + update(); } void TimeLineWidget::toggleAutoScroll( int _n ) @@ -175,19 +149,18 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) p.fillRect( 0, 0, width(), height(), p.background() ); // Clip so that we only draw everything starting from the offset - const int leftMargin = m_xOffset + m_posMarkerPixmap.width() / 2; - p.setClipRect(leftMargin, 0, width() - leftMargin, height() ); + p.setClipRect(m_xOffset, 0, width() - m_xOffset, height()); - // Draw the loop rectangle - int const & loopRectMargin = getLoopRectangleVerticalPadding(); + // Variables for the loop rectangle + int const loopRectMargin = getLoopRectangleVerticalPadding(); int const loopRectHeight = this->height() - 2 * loopRectMargin; - int const loopStart = markerX(m_timeline->loopBegin()) + 8; - int const loopEndR = markerX(m_timeline->loopEnd()) + 9; + int const loopStart = markerX(m_timeline->loopBegin()); + int const loopEndR = markerX(m_timeline->loopEnd()); int const loopRectWidth = loopEndR - loopStart; bool const loopPointsActive = m_timeline->loopEnabled(); - // Draw the main rectangle (inner fill only) + // Draw the main loop rectangle (inner fill only) QRect outerRectangle( loopStart, loopRectMargin, loopRectWidth - 1, loopRectHeight - 1 ); p.fillRect( outerRectangle, loopPointsActive ? getActiveLoopBrush() : getInactiveLoopBrush()); @@ -203,8 +176,7 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) QColor const & barNumberColor = getBarNumberColor(); bar_t barNumber = m_begin.getBar(); - int const x = m_xOffset + m_posMarkerPixmap.width() / 2 - - ((static_cast(m_begin * m_ppb) / TimePos::ticksPerBar()) % static_cast(m_ppb)); + int const x = m_xOffset - ((static_cast(m_begin * m_ppb) / TimePos::ticksPerBar()) % static_cast(m_ppb)); // Double the interval between bar numbers until they are far enough appart int barLabelInterval = 1; @@ -225,83 +197,139 @@ void TimeLineWidget::paintEvent( QPaintEvent * ) } } - // Draw the main rectangle (outer border) + // Draw the loop rectangle's outer outline p.setPen( loopPointsActive ? getActiveLoopColor() : getInactiveLoopColor() ); p.setBrush( Qt::NoBrush ); p.drawRect( outerRectangle ); - // Draw the inner border outline (no fill) + // Draw the loop rectangle's inner outline QRect innerRectangle = outerRectangle.adjusted( 1, 1, -1, -1 ); p.setPen( loopPointsActive ? getActiveLoopInnerColor() : getInactiveLoopInnerColor() ); p.setBrush( Qt::NoBrush ); p.drawRect( innerRectangle ); + + // Draw loop handles if necessary + const auto handleMode = ConfigManager::inst()->value("app", "loopmarkermode") == "handles"; + if (handleMode && underMouse() && QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier)) + { + const auto handleWidth = std::min(m_loopHandleWidth, loopRectWidth / 2 - 1); + const auto leftHandle = QRectF(loopStart - .5, loopRectMargin - .5, handleWidth, loopRectHeight); + const auto rightHandle = QRectF(loopEndR - handleWidth - .5, loopRectMargin - .5, handleWidth, loopRectHeight); + const auto color = loopPointsActive ? m_activeLoopHandleColor : m_inactiveLoopHandleColor; + + p.fillRect(leftHandle, color); + p.fillRect(rightHandle, color); + } // Only draw the position marker if the position line is in view - if (m_posMarkerX >= m_xOffset && m_posMarkerX < width() - m_posMarkerPixmap.width() / 2) + if (markerX(m_pos) >= m_xOffset && markerX(m_pos) < width() - m_posMarkerPixmap.width() / 2) { // Let the position marker extrude to the left p.setClipping(false); p.setOpacity(0.6); - p.drawPixmap(m_posMarkerX, height() - m_posMarkerPixmap.height(), m_posMarkerPixmap); + p.drawPixmap(markerX(m_pos) - (m_posMarkerPixmap.width() / 2), + height() - m_posMarkerPixmap.height(), m_posMarkerPixmap); } } +auto TimeLineWidget::getClickedTime(const int xPosition) const -> TimePos +{ + // How far into the timeline we clicked, measuring pixels from the leftmost part of the editor + const auto pixelDelta = std::max(xPosition - m_xOffset, 0); + return m_begin + static_cast(pixelDelta * TimePos::ticksPerBar() / m_ppb); +} +auto TimeLineWidget::getLoopAction(QMouseEvent* event) const -> TimeLineWidget::Action +{ + const auto mode = ConfigManager::inst()->value("app", "loopmarkermode"); + const auto xPos = event->x(); + const auto button = event->button(); + if (mode == "handles") + { + // Loop start and end pos, or closest edge of screen if loop extends off it + const auto leftMost = std::max(markerX(m_timeline->loopBegin()), m_xOffset); + const auto rightMost = std::min(markerX(m_timeline->loopEnd()), width()); + // Distance from click to handle, positive aimed towards center of loop + const auto deltaLeft = xPos - leftMost; + const auto deltaRight = rightMost - xPos; + + if (deltaLeft < 0 || deltaRight < 0) { return Action::NoAction; } // Clicked outside loop + else if (deltaLeft <= m_loopHandleWidth && deltaLeft < deltaRight) { return Action::MoveLoopBegin; } + else if (deltaRight <= m_loopHandleWidth) { return Action::MoveLoopEnd; } + else { return Action::MoveLoop; } + } + else if (mode == "closest") + { + const TimePos loopMid = (m_timeline->loopBegin() + m_timeline->loopEnd()) / 2; + return getClickedTime(xPos) < loopMid ? Action::MoveLoopBegin : Action::MoveLoopEnd; + } + else // Default to dual-button mode + { + if (button == Qt::LeftButton) { return Action::MoveLoopBegin; } + else if (button == Qt::RightButton) { return Action::MoveLoopEnd; } + return Action::NoAction; + } +} -void TimeLineWidget::mousePressEvent( QMouseEvent* event ) +auto TimeLineWidget::actionCursor(Action action) const -> QCursor { - if( event->x() < m_xOffset ) - { - return; + switch (action) { + case Action::MoveLoop: return Qt::SizeHorCursor; + case Action::MoveLoopBegin: return m_cursorSelectLeft; + case Action::MoveLoopEnd: return m_cursorSelectRight; + // Fall back to normal cursor if no action or action cursor not specified + default: return Qt::ArrowCursor; } - if( event->button() == Qt::LeftButton && !(event->modifiers() & Qt::ShiftModifier) ) +} + +void TimeLineWidget::mousePressEvent(QMouseEvent* event) +{ + if (event->x() < m_xOffset) { return; } + + const auto shift = event->modifiers() & Qt::ShiftModifier; + const auto ctrl = event->modifiers() & Qt::ControlModifier; + + if (shift) // loop marker manipulation { - m_action = Action::MovePositionMarker; - if (event->x() - m_xOffset < m_posMarkerPixmap.width()) - { - m_moveXOff = event->x() - m_xOffset; - } - else + m_action = getLoopAction(event); + setCursor(actionCursor(m_action)); + + if (m_action == Action::MoveLoop) { - m_moveXOff = m_posMarkerPixmap.width() / 2; + m_dragStartPos = getClickedTime(event->x()); + m_oldLoopPos = {m_timeline->loopBegin(), m_timeline->loopEnd()}; } } - else if( event->button() == Qt::LeftButton && (event->modifiers() & Qt::ShiftModifier) ) + else if (event->button() == Qt::LeftButton && ctrl) // selection { m_action = Action::SelectSongClip; m_initalXSelect = event->x(); } - else if( event->button() == Qt::RightButton ) + else if (event->button() == Qt::LeftButton && !ctrl) // move playhead { - m_moveXOff = m_posMarkerPixmap.width() / 2; - - const auto cursorXOffset = std::max(event->x() - m_xOffset - m_moveXOff, 0); - const TimePos timeAtCursor = m_begin + static_cast(cursorXOffset * TimePos::ticksPerBar() / m_ppb); - const TimePos loopMid = (m_timeline->loopBegin() + m_timeline->loopEnd()) / 2; - - m_action = timeAtCursor < loopMid ? Action::MoveLoopBegin : Action::MoveLoopEnd; + m_action = Action::MovePositionMarker; } - if( m_action == Action::MoveLoopBegin || m_action == Action::MoveLoopEnd ) + if (m_action == Action::MoveLoopBegin || m_action == 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 ); + m_hint = TextFloat::displayMessage(tr("Hint"), + tr("Press <%1> to disable magnetic loop points.").arg(UI_CTRL_KEY), + embed::getIconPixmap("hint"), 0); } - mouseMoveEvent( event ); -} - + setContextMenuPolicy(m_action == Action::NoAction ? Qt::DefaultContextMenu : Qt::PreventContextMenu); + mouseMoveEvent(event); +} void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) { parentWidget()->update(); // essential for widgets that this timeline had taken their mouse move event from. - const auto cursorXOffset = std::max(event->x() - m_xOffset - m_moveXOff, 0); - TimePos timeAtCursor = m_begin + static_cast(cursorXOffset * TimePos::ticksPerBar() / m_ppb); + auto timeAtCursor = getClickedTime(event->x()); + const auto control = event->modifiers() & Qt::ControlModifier; switch( m_action ) { @@ -324,7 +352,6 @@ void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) const auto otherPoint = m_action == Action::MoveLoopBegin ? m_timeline->loopEnd() : m_timeline->loopBegin(); - const bool control = event->modifiers() & Qt::ControlModifier; if (control) { // no ctrl-press-hint when having ctrl pressed @@ -349,17 +376,34 @@ void TimeLineWidget::mouseMoveEvent( QMouseEvent* event ) update(); break; } - case Action::SelectSongClip: + case Action::MoveLoop: + { + const TimePos dragDelta = timeAtCursor - m_dragStartPos; + auto loopPos = m_oldLoopPos; + for (auto& point : loopPos) + { + point += dragDelta; + if (!control) { point = point.quantize(m_snapSize); } + } + m_timeline->setLoopPoints(loopPos[0], loopPos[1]); + break; + } + case Action::SelectSongClip: emit regionSelectedFromPixels( m_initalXSelect , event->x() ); - break; + break; default: break; } -} - - + if (event->buttons() == Qt::NoButton) + { + setCursor(QGuiApplication::keyboardModifiers().testFlag(Qt::ShiftModifier) + ? actionCursor(getLoopAction(event)) + : Qt::ArrowCursor + ); + } +} void TimeLineWidget::mouseReleaseEvent( QMouseEvent* event ) { @@ -369,5 +413,45 @@ void TimeLineWidget::mouseReleaseEvent( QMouseEvent* event ) m_action = Action::NoAction; } +void TimeLineWidget::contextMenuEvent(QContextMenuEvent* event) +{ + if (event->x() < m_xOffset) { return; } + + auto menu = QMenu{}; + + menu.addAction(tr("Set loop begin here"), [this, event] { + auto begin = getClickedTime(event->x()); + const auto end = m_timeline->loopEnd(); + if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { begin = begin.quantize(m_snapSize); } + if (begin == end) { m_timeline->setLoopEnd(end + m_snapSize * TimePos::ticksPerBar()); } + m_timeline->setLoopBegin(begin); + update(); + }); + menu.addAction(tr("Set loop end here"), [this, event] { + const auto begin = m_timeline->loopBegin(); + auto end = getClickedTime(event->x()); + if (!QGuiApplication::keyboardModifiers().testFlag(Qt::ControlModifier)) { end = end.quantize(m_snapSize); } + if (begin == end) { m_timeline->setLoopBegin(begin - m_snapSize * TimePos::ticksPerBar()); } + m_timeline->setLoopEnd(end); + update(); + }); + + menu.addSeparator(); + + const auto loopMenu = menu.addMenu(tr("Loop edit mode (hold shift)")); + const auto loopMode = ConfigManager::inst()->value("app", "loopmarkermode", "dual"); + const auto addLoopModeAction = [loopMenu, &loopMode](QString text, QString mode) { + const auto action = loopMenu->addAction(text, [mode] { + ConfigManager::inst()->setValue("app", "loopmarkermode", mode); + }); + action->setCheckable(true); + if (loopMode == mode) { action->setChecked(true); } + }; + addLoopModeAction(tr("Dual-button"), "dual"); + addLoopModeAction(tr("Grab closest"), "closest"); + addLoopModeAction(tr("Handles"), "handles"); + + menu.exec(event->globalPos()); +} } // namespace lmms::gui diff --git a/src/gui/modals/SetupDialog.cpp b/src/gui/modals/SetupDialog.cpp index 209422563bc..fffa94c82ba 100644 --- a/src/gui/modals/SetupDialog.cpp +++ b/src/gui/modals/SetupDialog.cpp @@ -120,6 +120,7 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : "app", "disablebackup").toInt()), m_openLastProject(ConfigManager::inst()->value( "app", "openlastproject").toInt()), + m_loopMarkerMode{ConfigManager::inst()->value("app", "loopmarkermode", "dual")}, m_lang(ConfigManager::inst()->value( "app", "language")), m_saveInterval( ConfigManager::inst()->value( @@ -255,6 +256,19 @@ SetupDialog::SetupDialog(ConfigTab tab_to_open) : addCheckBox(tr("Show warning when deleting a mixer channel that is in use"), guiGroupBox, guiGroupLayout, m_mixerChannelDeletionWarning, SLOT(toggleMixerChannelDeletionWarning(bool)), false); + m_loopMarkerComboBox = new QComboBox{guiGroupBox}; + + m_loopMarkerComboBox->addItem(tr("Dual-button"), "dual"); + m_loopMarkerComboBox->addItem(tr("Grab closest"), "closest"); + m_loopMarkerComboBox->addItem(tr("Handles"), "handles"); + + m_loopMarkerComboBox->setCurrentIndex(m_loopMarkerComboBox->findData(m_loopMarkerMode)); + connect(m_loopMarkerComboBox, qOverload(&QComboBox::currentIndexChanged), + this, &SetupDialog::loopMarkerModeChanged); + + guiGroupLayout->addWidget(new QLabel{tr("Loop edit mode"), guiGroupBox}); + guiGroupLayout->addWidget(m_loopMarkerComboBox); + generalControlsLayout->addWidget(guiGroupBox); generalControlsLayout->addSpacing(10); @@ -922,6 +936,7 @@ void SetupDialog::accept() QString::number(!m_disableBackup)); ConfigManager::inst()->setValue("app", "openlastproject", QString::number(m_openLastProject)); + ConfigManager::inst()->setValue("app", "loopmarkermode", m_loopMarkerMode); ConfigManager::inst()->setValue("app", "language", m_lang); ConfigManager::inst()->setValue("ui", "saveinterval", QString::number(m_saveInterval)); @@ -1061,6 +1076,12 @@ void SetupDialog::toggleOpenLastProject(bool enabled) } +void SetupDialog::loopMarkerModeChanged() +{ + m_loopMarkerMode = m_loopMarkerComboBox->currentData().toString(); +} + + void SetupDialog::setLanguage(int lang) { m_lang = m_languages[lang];