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;