diff --git a/res/skins/LateNight/controls/button_hotcue.xml b/res/skins/LateNight/controls/button_hotcue.xml index 0e499d5715c..952b85b37e5 100644 --- a/res/skins/LateNight/controls/button_hotcue.xml +++ b/res/skins/LateNight/controls/button_hotcue.xml @@ -17,6 +17,7 @@ 3 + 0 skins:LateNight//buttons/btn__square.svg diff --git a/res/skins/LateNight/skin.xml b/res/skins/LateNight/skin.xml index 2e4a5445055..edbc6ab2846 100644 --- a/res/skins/LateNight/skin.xml +++ b/res/skins/LateNight/skin.xml @@ -191,6 +191,10 @@ 127 105 105 + + 1 #999 #999 diff --git a/src/engine/controls/cuecontrol.cpp b/src/engine/controls/cuecontrol.cpp index b007455c861..80c2c08121f 100644 --- a/src/engine/controls/cuecontrol.cpp +++ b/src/engine/controls/cuecontrol.cpp @@ -394,6 +394,11 @@ void CueControl::connectControls() { this, &CueControl::hotcueClear, Qt::DirectConnection); + connect(pControl, + &HotcueControl::hotcueSwap, + this, + &CueControl::hotcueSwap, + Qt::DirectConnection); } } @@ -1218,6 +1223,26 @@ void CueControl::hotcueClear(HotcueControl* pControl, double value) { setHotcueFocusIndex(Cue::kNoHotCue); } +void CueControl::hotcueSwap(HotcueControl* pControl, double v) { + // 1-based GUI/human index to 0-based internal index + int newCuenum = static_cast(v) - 1; + if (newCuenum < mixxx::kFirstHotCueIndex || newCuenum >= m_iNumHotCues) { + return; + } + + auto lock = lockMutex(&m_trackMutex); + if (!m_pLoadedTrack) { + return; + } + + CuePointer pCue = pControl->getCue(); + if (!pCue) { + return; + } + + m_pLoadedTrack->swapHotcues(pCue->getHotCue(), newCuenum); +} + void CueControl::hotcuePositionChanged( HotcueControl* pControl, double value) { auto lock = lockMutex(&m_trackMutex); @@ -2585,6 +2610,13 @@ HotcueControl::HotcueControl(const QString& group, int hotcueIndex) &HotcueControl::slotHotcueClear, Qt::DirectConnection); + m_hotcueSwap = std::make_unique(keyForControl(QStringLiteral("swap"))); + connect(m_hotcueSwap.get(), + &ControlObject::valueChanged, + this, + &HotcueControl::slotHotcueSwap, + Qt::DirectConnection); + m_previewingType.setValue(mixxx::CueType::Invalid); m_previewingPosition.setValue(mixxx::audio::kInvalidFramePos); } @@ -2643,6 +2675,10 @@ void HotcueControl::slotHotcueClear(double v) { emit hotcueClear(this, v); } +void HotcueControl::slotHotcueSwap(double v) { + emit hotcueSwap(this, v); +} + void HotcueControl::slotHotcuePositionChanged(double newPosition) { emit hotcuePositionChanged(this, newPosition); } diff --git a/src/engine/controls/cuecontrol.h b/src/engine/controls/cuecontrol.h index 3f45de0a4f8..64bdbb51ef5 100644 --- a/src/engine/controls/cuecontrol.h +++ b/src/engine/controls/cuecontrol.h @@ -136,6 +136,7 @@ class HotcueControl : public QObject { void slotHotcueActivateLoop(double v); void slotHotcueActivatePreview(double v); void slotHotcueClear(double v); + void slotHotcueSwap(double v); void slotHotcueEndPositionChanged(double newPosition); void slotHotcuePositionChanged(double newPosition); void slotHotcueColorChangeRequest(double newColor); @@ -150,6 +151,7 @@ class HotcueControl : public QObject { void hotcueActivate(HotcueControl* pHotcue, double v, HotcueSetMode mode); void hotcueActivatePreview(HotcueControl* pHotcue, double v); void hotcueClear(HotcueControl* pHotcue, double v); + void hotcueSwap(HotcueControl* pHotcue, double v); void hotcuePositionChanged(HotcueControl* pHotcue, double newPosition); void hotcueEndPositionChanged(HotcueControl* pHotcue, double newEndPosition); void hotcuePlay(double v); @@ -181,6 +183,7 @@ class HotcueControl : public QObject { std::unique_ptr m_hotcueActivateLoop; std::unique_ptr m_hotcueActivatePreview; std::unique_ptr m_hotcueClear; + std::unique_ptr m_hotcueSwap; ControlValueAtomic m_previewingType; ControlValueAtomic m_previewingPosition; @@ -228,6 +231,7 @@ class CueControl : public EngineControl { void hotcueActivatePreview(HotcueControl* pControl, double v); void updateCurrentlyPreviewingIndex(int hotcueIndex); void hotcueClear(HotcueControl* pControl, double v); + void hotcueSwap(HotcueControl* pHotcue, double v); void hotcuePositionChanged(HotcueControl* pControl, double newPosition); void hotcueEndPositionChanged(HotcueControl* pControl, double newEndPosition); diff --git a/src/skin/legacy/tooltips.cpp b/src/skin/legacy/tooltips.cpp index c6b5c219c31..572e8d717ad 100644 --- a/src/skin/legacy/tooltips.cpp +++ b/src/skin/legacy/tooltips.cpp @@ -699,7 +699,12 @@ void Tooltips::addStandardTooltips() { << QString("%1 + %2: %3") .arg(rightClick, shift, - tr("Delete selected hotcue.")); + tr("Delete selected hotcue.")) + << tr("Drag this button onto another Hotcue button to move it " + "there (change its index). If the other hotcue is set, " + "the two are swapped.") + << tr("Dragging with Shift key pressed will not start previewing " + "the hotcue"); // Status displays and toggle buttons add("toggle_recording") diff --git a/src/track/cue.cpp b/src/track/cue.cpp index 28fe9cca19c..6b678b6746d 100644 --- a/src/track/cue.cpp +++ b/src/track/cue.cpp @@ -221,6 +221,17 @@ mixxx::audio::FrameDiff_t Cue::getLengthFrames() const { return m_endPosition - m_startPosition; } +void Cue::setHotCue(int n) { + VERIFY_OR_DEBUG_ASSERT(n >= mixxx::kFirstHotCueIndex) { + return; + } + const auto lock = lockMutex(&m_mutex); + if (m_iHotCue == n) { + return; + } + m_iHotCue = n; +} + int Cue::getHotCue() const { const auto lock = lockMutex(&m_mutex); return m_iHotCue; diff --git a/src/track/cue.h b/src/track/cue.h index c0474e76ae9..92eed026443 100644 --- a/src/track/cue.h +++ b/src/track/cue.h @@ -74,6 +74,7 @@ class Cue : public QObject { mixxx::audio::FrameDiff_t getLengthFrames() const; + void setHotCue(int n); int getHotCue() const; QString getLabel() const; @@ -104,7 +105,7 @@ class Cue : public QObject { mixxx::CueType m_type; mixxx::audio::FramePos m_startPosition; mixxx::audio::FramePos m_endPosition; - const int m_iHotCue; + int m_iHotCue; QString m_label; mixxx::RgbColor m_color; diff --git a/src/track/track.cpp b/src/track/track.cpp index 52986fb6701..6eae0749c5a 100644 --- a/src/track/track.cpp +++ b/src/track/track.cpp @@ -1062,6 +1062,21 @@ CuePointer Track::findCueById(DbId id) const { return CuePointer(); } +CuePointer Track::findHotcueByIndex(int idx) const { + auto locked = lockMutex(&m_qMutex); + auto cueIt = std::find_if( + m_cuePoints.begin(), + m_cuePoints.end(), + [idx](const CuePointer& pCue) { + return pCue && pCue->getHotCue() == idx; + }); + if (cueIt != m_cuePoints.end()) { + return *cueIt; + } else { + return {}; + } +} + void Track::removeCue(const CuePointer& pCue) { if (!pCue) { return; @@ -1103,6 +1118,30 @@ void Track::removeCuesOfType(mixxx::CueType type) { } } +void Track::swapHotcues(int a, int b) { + VERIFY_OR_DEBUG_ASSERT(a != b) { + qWarning() << "Track::swapHotcues rejected," << a << "==" << b; + return; + } + VERIFY_OR_DEBUG_ASSERT(a != Cue::kNoHotCue || b != Cue::kNoHotCue) { + qWarning() << "Track::swapHotcues rejected, both a and b are kNoHotCue"; + return; + } + auto locked = lockMutex(&m_qMutex); + CuePointer pCueA = findHotcueByIndex(a); + CuePointer pCueB = findHotcueByIndex(b); + if (!pCueA && !pCueB) { + return; + } + if (pCueA) { + pCueA->setHotCue(b); + } + if (pCueB) { + pCueB->setHotCue(a); + } + emit cuesUpdated(); +} + void Track::setCuePoints(const QList& cuePoints) { // While this method could be called from any thread, // associated Cue objects should always live on the diff --git a/src/track/track.h b/src/track/track.h index 0d181d3ab00..af398438ff7 100644 --- a/src/track/track.h +++ b/src/track/track.h @@ -322,6 +322,7 @@ class Track : public QObject { } CuePointer findCueByType(mixxx::CueType type) const; // NOTE: Cannot be used for hotcues. CuePointer findCueById(DbId id) const; + CuePointer findHotcueByIndex(int idx) const; void removeCue(const CuePointer& pCue); void removeCuesOfType(mixxx::CueType); QList getCuePoints() const { @@ -329,7 +330,7 @@ class Track : public QObject { // lock thread-unsafe copy constructors of QList return m_cuePoints; } - + void swapHotcues(int a, int b); void setCuePoints(const QList& cuePoints); #ifdef __STEM__ diff --git a/src/util/db/dbid.h b/src/util/db/dbid.h index a001459df21..6aa57810f6f 100644 --- a/src/util/db/dbid.h +++ b/src/util/db/dbid.h @@ -88,6 +88,18 @@ class DbId { return debug << dbId.m_value; } + friend QDataStream& operator<<(QDataStream& out, const DbId& dbId) { + // explicit cast as recommended by Qt docs + return out << static_cast(dbId.m_value); + } + + friend QDataStream& operator>>(QDataStream& in, DbId& dbId) { + quint32 v; + in >> v; + dbId.m_value = v; + return in; + } + friend qhash_seed_t qHash( const DbId& dbId, qhash_seed_t seed = 0) { diff --git a/src/widget/whotcuebutton.cpp b/src/widget/whotcuebutton.cpp index 9e55ec23354..da168cdcfc2 100644 --- a/src/widget/whotcuebutton.cpp +++ b/src/widget/whotcuebutton.cpp @@ -1,18 +1,25 @@ #include "widget/whotcuebutton.h" +#include +#include +#include +#include +#include #include #include "mixer/playerinfo.h" #include "moc_whotcuebutton.cpp" #include "skin/legacy/skincontext.h" #include "track/track.h" +#include "util/dnd.h" #include "util/valuetransformer.h" #include "widget/controlwidgetconnection.h" #include "widget/wbasewidget.h" namespace { constexpr int kDefaultDimBrightThreshold = 127; -} // namespace +const QString kDragDataType = QStringLiteral("hotcueDragInfo"); +} // anonymous namespace WHotcueButton::WHotcueButton(const QString& group, QWidget* pParent) : WPushButton(pParent), @@ -24,6 +31,7 @@ WHotcueButton::WHotcueButton(const QString& group, QWidget* pParent) m_bCueColorDimmed(false), m_bCueColorIsLight(false), m_bCueColorIsDark(false) { + setAcceptDrops(true); } void WHotcueButton::setup(const QDomNode& node, const SkinContext& context) { @@ -49,6 +57,15 @@ void WHotcueButton::setup(const QDomNode& node, const SkinContext& context) { m_hoverCueColor = context.selectBool(node, QStringLiteral("Hover"), false); + // For dnd/swapping hotcues we use the rendered widget pixmap as dnd cursor. + // Unfortnately the margin that constraints the bg color is not considered, + // so we shrink the rect by custom margins. + okay = false; + int dndMargin = context.selectInt(node, QStringLiteral("DndRectMargin"), &okay); + if (okay && dndMargin > 0) { + m_dndRectMargins = QMargins(dndMargin, dndMargin, dndMargin, dndMargin); + } + m_pCueMenuPopup = make_parented(context.getConfig(), this); ColorPaletteSettings colorPaletteSettings(context.getConfig()); auto colorPalette = colorPaletteSettings.getHotcueColorPalette(); @@ -92,8 +109,8 @@ void WHotcueButton::setup(const QDomNode& node, const SkinContext& context) { } } -void WHotcueButton::mousePressEvent(QMouseEvent* e) { - const bool rightClick = e->button() == Qt::RightButton; +void WHotcueButton::mousePressEvent(QMouseEvent* pEvent) { + const bool rightClick = pEvent->button() == Qt::RightButton; if (rightClick) { if (isPressed()) { // Discard right clicks when already left clicked. @@ -119,7 +136,7 @@ void WHotcueButton::mousePressEvent(QMouseEvent* e) { if (!pHotCue) { return; } - if (e->modifiers().testFlag(Qt::ShiftModifier)) { + if (pEvent->modifiers().testFlag(Qt::ShiftModifier)) { pTrack->removeCue(pHotCue); return; } @@ -131,16 +148,104 @@ void WHotcueButton::mousePressEvent(QMouseEvent* e) { } // Pass all other press events to the base class. - WPushButton::mousePressEvent(e); + // Except when Shift is pressed which is used to swap hotcues without + // starting the preview. + if (!pEvent->modifiers().testFlag(Qt::ShiftModifier)) { + WPushButton::mousePressEvent(pEvent); + } } -void WHotcueButton::mouseReleaseEvent(QMouseEvent* e) { - const bool rightClick = e->button() == Qt::RightButton; +void WHotcueButton::mouseReleaseEvent(QMouseEvent* pEvent) { + const bool rightClick = pEvent->button() == Qt::RightButton; if (rightClick) { // Don't handle stray release events return; } - WPushButton::mouseReleaseEvent(e); + WPushButton::mouseReleaseEvent(pEvent); +} + +void WHotcueButton::mouseMoveEvent(QMouseEvent* pEvent) { + TrackPointer pTrack = PlayerInfo::instance().getTrackInfo(m_group); + if (!pTrack) { + return; + } + + // Maybe set up a QDrag for swapping hotcues. + // Only allow moving set hotcues to empty or set slots. + // Note that Track::swapHotcues() allows both directions. + if (m_hotcue == Cue::kNoHotCue) { + return; + } + + if (DragAndDropHelper::mouseMoveInitiatesDrag(pEvent)) { + const TrackId id = pTrack->getId(); + VERIFY_OR_DEBUG_ASSERT(id.isValid()) { + return; + } + QDrag* pDrag = new QDrag(this); + HotcueDragInfo dragData(id, m_hotcue); + auto mimeData = std::make_unique(); + mimeData->setData(kDragDataType, dragData.toByteArray()); + pDrag->setMimeData(mimeData.release()); + + // Use the currently rendered button as dnd cursor + // (incl. hover and pressed style). + // Note: for some reason, both grab() and render() render with sharp corners, + // ie. qss 'border-radius' is not applied to the drag image. + const QPixmap currLook = grab(rect().marginsRemoved(m_dndRectMargins)); + pDrag->setDragCursor(currLook, Qt::MoveAction); + + m_dragging = true; + pDrag->exec(); + m_dragging = false; + + // Release this button afterwards. + // This prevents both the preview and the pressed state from getting stuck. + QEvent leaveEv(QEvent::Leave); + QApplication::sendEvent(this, &leaveEv); + } +} + +void WHotcueButton::dragEnterEvent(QDragEnterEvent* pEvent) { + if (pEvent->source() == this) { + pEvent->ignore(); + return; + } + TrackPointer pTrack = PlayerInfo::instance().getTrackInfo(m_group); + if (!pTrack) { + return; + } + QByteArray mimeDataBytes = pEvent->mimeData()->data(kDragDataType); + if (mimeDataBytes.isEmpty()) { + return; + } + HotcueDragInfo dragData = HotcueDragInfo::fromByteArray(mimeDataBytes); + if (dragData.isValid() && + dragData.trackId == pTrack->getId() && + dragData.hotcue != m_hotcue) { + pEvent->acceptProposedAction(); + } +} + +void WHotcueButton::dropEvent(QDropEvent* pEvent) { + if (pEvent->source() == this) { + pEvent->ignore(); + return; + } + TrackPointer pTrack = PlayerInfo::instance().getTrackInfo(m_group); + if (!pTrack) { + return; + } + QByteArray mimeDataBytes = pEvent->mimeData()->data(kDragDataType); + if (mimeDataBytes.isEmpty()) { + return; + } + HotcueDragInfo dragData = HotcueDragInfo::fromByteArray(mimeDataBytes); + if (dragData.isValid() && + dragData.trackId == pTrack->getId() && + dragData.hotcue != m_hotcue) { + pTrack->swapHotcues(dragData.hotcue, m_hotcue); + } } ConfigKey WHotcueButton::createConfigKey(const QString& name) { diff --git a/src/widget/whotcuebutton.h b/src/widget/whotcuebutton.h index 48dd3395c74..6867ef68126 100644 --- a/src/widget/whotcuebutton.h +++ b/src/widget/whotcuebutton.h @@ -2,12 +2,42 @@ #include +#include "track/trackid.h" #include "util/parented_ptr.h" #include "widget/wcuemenupopup.h" #include "widget/wpushbutton.h" class WHotcueButton : public WPushButton { Q_OBJECT + + struct HotcueDragInfo { + HotcueDragInfo(TrackId id, int cue) + : trackId(id), + hotcue(cue) {}; + + static HotcueDragInfo fromByteArray(const QByteArray& bytes) { + QDataStream stream(bytes); + TrackId trackId; + int hotcue; + stream >> trackId >> hotcue; + return HotcueDragInfo(trackId, hotcue); + }; + + QByteArray toByteArray() { + QByteArray bytes; + QDataStream dataStream(&bytes, QIODevice::WriteOnly); + dataStream << trackId << hotcue; + return bytes; + }; + + bool isValid() { + return trackId.isValid() && hotcue != Cue::kNoHotCue; + } + + TrackId trackId = TrackId(); + int hotcue = Cue::kNoHotCue; + }; + public: WHotcueButton(const QString& group, QWidget* pParent); @@ -25,8 +55,12 @@ class WHotcueButton : public WPushButton { Q_PROPERTY(QString type MEMBER m_type); protected: - void mousePressEvent(QMouseEvent* e) override; - void mouseReleaseEvent(QMouseEvent* e) override; + void mousePressEvent(QMouseEvent* pEvent) override; + void mouseReleaseEvent(QMouseEvent* pEvent) override; + void mouseMoveEvent(QMouseEvent* pEvent) override; + void dragEnterEvent(QDragEnterEvent* pEvent) override; + void dropEvent(QDropEvent* pEvent) override; + void restyleAndRepaint() override; private slots: @@ -48,4 +82,5 @@ class WHotcueButton : public WPushButton { bool m_bCueColorIsLight; bool m_bCueColorIsDark; QString m_type; + QMargins m_dndRectMargins; }; diff --git a/src/widget/wpushbutton.cpp b/src/widget/wpushbutton.cpp index 426acd2fefb..2f5303be27a 100644 --- a/src/widget/wpushbutton.cpp +++ b/src/widget/wpushbutton.cpp @@ -239,6 +239,7 @@ void WPushButton::setup(const QDomNode& node, const SkinContext& context) { void WPushButton::setStates(int iStates) { m_bHovered = false; m_bPressed = false; + m_dragging = false; m_iNoStates = iStates; m_elideMode = Qt::ElideNone; m_activeTouchButton = Qt::NoButton; @@ -440,7 +441,9 @@ bool WPushButton::event(QEvent* e) { m_bHovered = true; restyleAndRepaint(); } else if (e->type() == QEvent::Leave) { - if (m_bPressed) { + // Leave might occur sporadically while dragging (swapping) a WHotcueButton. + // Don't release in that case. + if (m_bPressed && !m_dragging) { // A Leave event is send instead of a mouseReleaseEvent() // fake it to get not stuck in pressed state QMouseEvent mouseEvent = QMouseEvent( @@ -473,6 +476,8 @@ void WPushButton::focusOutEvent(QFocusEvent* e) { } void WPushButton::mouseReleaseEvent(QMouseEvent * e) { + // Note. when changing any of these actions, also take care of + // WHotcueButton::release() const bool leftClick = e->button() == Qt::LeftButton; const bool rightClick = e->button() == Qt::RightButton; @@ -498,7 +503,7 @@ void WPushButton::mouseReleaseEvent(QMouseEvent * e) { if (rightClick) { // This is the secondary clickButton function, - // due the leak of visual feedback we do not allow a toggle + // due the lack of visual feedback we do not allow a toggle // function m_bPressed = false; if (m_rightButtonMode == mixxx::control::ButtonMode::Push || m_iNoStates == 1) { diff --git a/src/widget/wpushbutton.h b/src/widget/wpushbutton.h index 94fbad7e6b4..9e59e507a79 100644 --- a/src/widget/wpushbutton.h +++ b/src/widget/wpushbutton.h @@ -98,6 +98,8 @@ class WPushButton : public WWidget { bool m_bPressed; // True, if the button is pointer is above button bool m_bHovered; + // Set true by WHotcueButton while it's being dragged + bool m_dragging; // Array of associated pixmaps int m_iNoStates;