feat: custom queue item delegate with two-line layout
Each queue item now shows: - Track title (medium weight) top-left - Duration (small, dimmed) top-right - Artist name (small, dimmed) bottom-left Replaces the flat "Artist — Track" single-line text. Play-next items get a slightly brighter title tint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,106 @@
|
||||
#include <QHBoxLayout>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QPainter>
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QApplication>
|
||||
|
||||
static constexpr int UpcomingIndexRole = Qt::UserRole + 1;
|
||||
static constexpr int IsPlayNextRole = Qt::UserRole + 2;
|
||||
static constexpr int TrackJsonRole = Qt::UserRole + 3;
|
||||
static constexpr int ArtistRole = Qt::UserRole + 4;
|
||||
static constexpr int DurationRole = Qt::UserRole + 5;
|
||||
|
||||
// ---- Custom delegate -------------------------------------------------------
|
||||
|
||||
class QueueDelegate : public QStyledItemDelegate
|
||||
{
|
||||
public:
|
||||
explicit QueueDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}
|
||||
|
||||
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override
|
||||
{
|
||||
return QSize(0, 52);
|
||||
}
|
||||
|
||||
void paint(QPainter *p, const QStyleOptionViewItem &option,
|
||||
const QModelIndex &index) const override
|
||||
{
|
||||
p->save();
|
||||
|
||||
// Background (selection / hover / alternate)
|
||||
QStyle *style = option.widget ? option.widget->style() : QApplication::style();
|
||||
style->drawPrimitive(QStyle::PE_PanelItemViewItem, &option, p, option.widget);
|
||||
|
||||
const bool isPlayNext = index.data(IsPlayNextRole).toBool();
|
||||
const QString title = index.data(Qt::DisplayRole).toString();
|
||||
const QString artist = index.data(ArtistRole).toString();
|
||||
const int dur = index.data(DurationRole).toInt();
|
||||
|
||||
const QRect r = option.rect.adjusted(10, 0, -10, 0);
|
||||
|
||||
// Duration string, right-aligned
|
||||
QString durStr;
|
||||
if (dur > 0) {
|
||||
const int m = dur / 60, s = dur % 60;
|
||||
durStr = QStringLiteral("%1:%2").arg(m).arg(s, 2, 10, QLatin1Char('0'));
|
||||
}
|
||||
|
||||
// Colours
|
||||
const QPalette &pal = option.palette;
|
||||
const bool selected = option.state & QStyle::State_Selected;
|
||||
QColor titleColor = selected ? pal.highlightedText().color() : pal.text().color();
|
||||
QColor artistColor = titleColor;
|
||||
artistColor.setAlpha(150);
|
||||
QColor durColor = artistColor;
|
||||
|
||||
if (isPlayNext && !selected) {
|
||||
titleColor = titleColor.lighter(130);
|
||||
}
|
||||
|
||||
// Fonts
|
||||
QFont titleFont = option.font;
|
||||
titleFont.setWeight(QFont::Medium);
|
||||
|
||||
QFont subFont = option.font;
|
||||
subFont.setPointSizeF(option.font.pointSizeF() * 0.85);
|
||||
|
||||
// Layout: title row vertically centred in top half, artist in bottom half
|
||||
const int halfH = r.height() / 2;
|
||||
const QRect topR = QRect(r.left(), r.top(), r.width(), halfH);
|
||||
const QRect botR = QRect(r.left(), r.top() + halfH, r.width(), halfH);
|
||||
|
||||
// Duration — draw right-aligned in top row
|
||||
if (!durStr.isEmpty()) {
|
||||
p->setFont(subFont);
|
||||
p->setPen(durColor);
|
||||
p->drawText(topR, Qt::AlignRight | Qt::AlignVCenter, durStr);
|
||||
}
|
||||
|
||||
// Reserve space for duration in title row
|
||||
const int durW = durStr.isEmpty() ? 0
|
||||
: QFontMetrics(subFont).horizontalAdvance(durStr) + 6;
|
||||
const QRect titleR = topR.adjusted(0, 0, -durW, 0);
|
||||
|
||||
// Title
|
||||
p->setFont(titleFont);
|
||||
p->setPen(titleColor);
|
||||
p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width()));
|
||||
|
||||
// Artist
|
||||
if (!artist.isEmpty()) {
|
||||
p->setFont(subFont);
|
||||
p->setPen(artistColor);
|
||||
p->drawText(botR, Qt::AlignLeft | Qt::AlignVCenter,
|
||||
p->fontMetrics().elidedText(artist, Qt::ElideRight, botR.width()));
|
||||
}
|
||||
|
||||
p->restore();
|
||||
}
|
||||
};
|
||||
|
||||
// ---- QueuePanel ------------------------------------------------------------
|
||||
|
||||
QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent)
|
||||
: QDockWidget(tr("Queue"), parent)
|
||||
@@ -21,7 +117,6 @@ QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent)
|
||||
layout->setContentsMargins(4, 4, 4, 4);
|
||||
layout->setSpacing(4);
|
||||
|
||||
// Header row: count label + Clear button
|
||||
auto *headerRow = new QHBoxLayout;
|
||||
m_countLabel = new QLabel(tr("Up next: 0 tracks"), container);
|
||||
m_clearBtn = new QPushButton(tr("Clear"), container);
|
||||
@@ -35,21 +130,17 @@ QueuePanel::QueuePanel(PlayQueue *queue, QWidget *parent)
|
||||
m_list->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_list->setDragDropMode(QAbstractItemView::InternalMove);
|
||||
m_list->setDefaultDropAction(Qt::MoveAction);
|
||||
m_list->setItemDelegate(new QueueDelegate(m_list));
|
||||
layout->addWidget(m_list, 1);
|
||||
|
||||
setWidget(container);
|
||||
setMinimumWidth(200);
|
||||
|
||||
connect(m_queue, &PlayQueue::queueChanged, this, &QueuePanel::refresh);
|
||||
connect(m_clearBtn, &QPushButton::clicked, this, [this] {
|
||||
m_queue->clearUpcoming();
|
||||
});
|
||||
connect(m_list, &QListWidget::itemDoubleClicked,
|
||||
this, &QueuePanel::onItemDoubleClicked);
|
||||
connect(m_list, &QListWidget::customContextMenuRequested,
|
||||
this, &QueuePanel::onContextMenu);
|
||||
connect(m_list->model(), &QAbstractItemModel::rowsMoved,
|
||||
this, &QueuePanel::onRowsMoved);
|
||||
connect(m_clearBtn, &QPushButton::clicked, this, [this] { m_queue->clearUpcoming(); });
|
||||
connect(m_list, &QListWidget::itemDoubleClicked, this, &QueuePanel::onItemDoubleClicked);
|
||||
connect(m_list, &QListWidget::customContextMenuRequested, this, &QueuePanel::onContextMenu);
|
||||
connect(m_list->model(), &QAbstractItemModel::rowsMoved, this, &QueuePanel::onRowsMoved);
|
||||
|
||||
refresh();
|
||||
}
|
||||
@@ -73,22 +164,14 @@ void QueuePanel::refresh()
|
||||
const QString artist = t["performer"].toObject()["name"].toString().isEmpty()
|
||||
? t["album"].toObject()["artist"].toObject()["name"].toString()
|
||||
: t["performer"].toObject()["name"].toString();
|
||||
const int duration = t["duration"].toInt();
|
||||
|
||||
const QString text = artist.isEmpty()
|
||||
? title
|
||||
: QStringLiteral("%1 — %2").arg(artist, title);
|
||||
|
||||
auto *item = new QListWidgetItem(text, m_list);
|
||||
auto *item = new QListWidgetItem(title, m_list);
|
||||
item->setData(UpcomingIndexRole, i);
|
||||
item->setData(IsPlayNextRole, i < playNextCount);
|
||||
item->setData(TrackJsonRole, QVariant::fromValue(t));
|
||||
|
||||
// "Play Next" tracks shown slightly differently
|
||||
if (i < playNextCount) {
|
||||
QFont f = item->font();
|
||||
f.setItalic(true);
|
||||
item->setFont(f);
|
||||
}
|
||||
item->setData(ArtistRole, artist);
|
||||
item->setData(DurationRole, duration);
|
||||
}
|
||||
|
||||
m_refreshing = false;
|
||||
@@ -109,10 +192,8 @@ void QueuePanel::onRowsMoved()
|
||||
|
||||
QVector<QJsonObject> newOrder;
|
||||
newOrder.reserve(m_list->count());
|
||||
for (int i = 0; i < m_list->count(); ++i) {
|
||||
const QVariant v = m_list->item(i)->data(TrackJsonRole);
|
||||
newOrder.append(v.value<QJsonObject>());
|
||||
}
|
||||
for (int i = 0; i < m_list->count(); ++i)
|
||||
newOrder.append(m_list->item(i)->data(TrackJsonRole).value<QJsonObject>());
|
||||
|
||||
m_refreshing = true;
|
||||
m_queue->setUpcomingOrder(newOrder);
|
||||
@@ -127,15 +208,11 @@ void QueuePanel::onContextMenu(const QPoint &pos)
|
||||
const int idx = item->data(UpcomingIndexRole).toInt();
|
||||
|
||||
QMenu menu(this);
|
||||
auto *removeAct = menu.addAction(tr("Remove from queue"));
|
||||
auto *toTopAct = menu.addAction(tr("Move to top (play next)"));
|
||||
auto *removeAct = menu.addAction(tr("Remove from queue"));
|
||||
auto *toTopAct = menu.addAction(tr("Move to top (play next)"));
|
||||
|
||||
connect(removeAct, &QAction::triggered, this, [this, idx] {
|
||||
m_queue->removeUpcoming(idx);
|
||||
});
|
||||
connect(toTopAct, &QAction::triggered, this, [this, idx] {
|
||||
m_queue->moveUpcomingToTop(idx);
|
||||
});
|
||||
connect(removeAct, &QAction::triggered, this, [this, idx] { m_queue->removeUpcoming(idx); });
|
||||
connect(toTopAct, &QAction::triggered, this, [this, idx] { m_queue->moveUpcomingToTop(idx); });
|
||||
|
||||
menu.exec(m_list->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user