feat: spotify-style toolbar layout
Three-column layout matching spotify-qt: - Left: album art thumbnail + track title/artist (stacked, 2 lines) - Center: prev/play/next controls above elapsed / progress / total - Right: volume button, shuffle, queue toggle, search toggle Controls and toggles use QToolButton directly instead of QAction so the layout is driven by HBoxLayout + VBoxLayout with equal stretch on left and right, keeping the player controls perfectly centred at all widths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
#include <QHBoxLayout>
|
#include <QHBoxLayout>
|
||||||
|
#include <QVBoxLayout>
|
||||||
|
#include <QToolButton>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
|
|
||||||
MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *parent)
|
||||||
@@ -19,87 +21,151 @@ MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
|||||||
m_nam = new QNetworkAccessManager(this);
|
m_nam = new QNetworkAccessManager(this);
|
||||||
connect(m_nam, &QNetworkAccessManager::finished, this, &MainToolBar::onAlbumArtReady);
|
connect(m_nam, &QNetworkAccessManager::finished, this, &MainToolBar::onAlbumArtReady);
|
||||||
|
|
||||||
// --- Album art thumbnail ---
|
// ----------------------------------------------------------------
|
||||||
m_artLabel = new QLabel(this);
|
// Root container — three equal-stretch columns
|
||||||
m_artLabel->setFixedSize(36, 36);
|
// ----------------------------------------------------------------
|
||||||
|
auto *root = new QWidget(this);
|
||||||
|
auto *rootLo = new QHBoxLayout(root);
|
||||||
|
rootLo->setContentsMargins(6, 2, 6, 2);
|
||||||
|
rootLo->setSpacing(0);
|
||||||
|
|
||||||
|
// ---- LEFT: album art + track info ----
|
||||||
|
auto *leftWidget = new QWidget(root);
|
||||||
|
auto *leftLo = new QHBoxLayout(leftWidget);
|
||||||
|
leftLo->setContentsMargins(0, 0, 0, 0);
|
||||||
|
leftLo->setSpacing(8);
|
||||||
|
|
||||||
|
m_artLabel = new QLabel(leftWidget);
|
||||||
|
m_artLabel->setFixedSize(44, 44);
|
||||||
m_artLabel->setScaledContents(true);
|
m_artLabel->setScaledContents(true);
|
||||||
m_artLabel->setStyleSheet("border: 1px solid #444; background: #1a1a1a;");
|
m_artLabel->setStyleSheet("border: 1px solid #444; background: #1a1a1a; border-radius: 3px;");
|
||||||
m_artLabel->setPixmap(QIcon(":/res/icons/view-media-album-cover.svg")
|
m_artLabel->setPixmap(QIcon(":/res/icons/view-media-album-cover.svg").pixmap(40, 40));
|
||||||
.pixmap(32, 32));
|
|
||||||
addWidget(m_artLabel);
|
|
||||||
addSeparator();
|
|
||||||
|
|
||||||
// --- Playback controls ---
|
m_trackLabel = new QLabel(tr("Not playing"), leftWidget);
|
||||||
m_previous = addAction(Icon::previous(), tr("Previous"));
|
m_trackLabel->setMinimumWidth(120);
|
||||||
connect(m_previous, &QAction::triggered, this, &MainToolBar::onPrevious);
|
m_trackLabel->setMaximumWidth(280);
|
||||||
|
|
||||||
m_playPause = addAction(Icon::play(), tr("Play"));
|
|
||||||
connect(m_playPause, &QAction::triggered, this, &MainToolBar::onPlayPause);
|
|
||||||
|
|
||||||
m_next = addAction(Icon::next(), tr("Next"));
|
|
||||||
connect(m_next, &QAction::triggered, this, &MainToolBar::onNext);
|
|
||||||
|
|
||||||
addSeparator();
|
|
||||||
|
|
||||||
// --- Track info label ---
|
|
||||||
m_trackLabel = new QLabel(tr("Not playing"), this);
|
|
||||||
m_trackLabel->setMinimumWidth(180);
|
|
||||||
m_trackLabel->setMaximumWidth(340);
|
|
||||||
m_trackLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
m_trackLabel->setAlignment(Qt::AlignVCenter | Qt::AlignLeft);
|
||||||
addWidget(m_trackLabel);
|
m_trackLabel->setWordWrap(false);
|
||||||
|
|
||||||
addSeparator();
|
leftLo->addWidget(m_artLabel);
|
||||||
|
leftLo->addWidget(m_trackLabel);
|
||||||
|
leftLo->addStretch(1);
|
||||||
|
|
||||||
// --- Progress slider ---
|
// ---- CENTER: controls + progress ----
|
||||||
m_progress = new ClickableSlider(Qt::Horizontal, this);
|
auto *centerWidget = new QWidget(root);
|
||||||
|
auto *centerLo = new QVBoxLayout(centerWidget);
|
||||||
|
centerLo->setContentsMargins(0, 0, 0, 0);
|
||||||
|
centerLo->setSpacing(2);
|
||||||
|
|
||||||
|
// Controls row: prev / play-pause / next
|
||||||
|
auto *ctrlRow = new QHBoxLayout;
|
||||||
|
ctrlRow->setSpacing(4);
|
||||||
|
ctrlRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
auto makeBtn = [&](const QIcon &icon, const QString &tip) -> QToolButton * {
|
||||||
|
auto *btn = new QToolButton(centerWidget);
|
||||||
|
btn->setIcon(icon);
|
||||||
|
btn->setToolTip(tip);
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
btn->setIconSize(QSize(22, 22));
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
|
||||||
|
m_prevBtn = makeBtn(Icon::previous(), tr("Previous"));
|
||||||
|
m_playBtn = makeBtn(Icon::play(), tr("Play"));
|
||||||
|
m_playBtn->setIconSize(QSize(28, 28));
|
||||||
|
m_nextBtn = makeBtn(Icon::next(), tr("Next"));
|
||||||
|
|
||||||
|
connect(m_prevBtn, &QToolButton::clicked, this, &MainToolBar::onPrevious);
|
||||||
|
connect(m_playBtn, &QToolButton::clicked, this, &MainToolBar::onPlayPause);
|
||||||
|
connect(m_nextBtn, &QToolButton::clicked, this, &MainToolBar::onNext);
|
||||||
|
|
||||||
|
ctrlRow->addStretch(1);
|
||||||
|
ctrlRow->addWidget(m_prevBtn);
|
||||||
|
ctrlRow->addWidget(m_playBtn);
|
||||||
|
ctrlRow->addWidget(m_nextBtn);
|
||||||
|
ctrlRow->addStretch(1);
|
||||||
|
|
||||||
|
// Progress row: elapsed / slider / total
|
||||||
|
auto *progRow = new QHBoxLayout;
|
||||||
|
progRow->setSpacing(6);
|
||||||
|
progRow->setContentsMargins(0, 0, 0, 0);
|
||||||
|
|
||||||
|
m_elapsedLabel = new QLabel(QStringLiteral("0:00"), centerWidget);
|
||||||
|
m_elapsedLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||||
|
m_elapsedLabel->setMinimumWidth(36);
|
||||||
|
|
||||||
|
m_progress = new ClickableSlider(Qt::Horizontal, centerWidget);
|
||||||
m_progress->setRange(0, 1000);
|
m_progress->setRange(0, 1000);
|
||||||
m_progress->setValue(0);
|
m_progress->setValue(0);
|
||||||
m_progress->setMinimumWidth(160);
|
m_progress->setMinimumWidth(200);
|
||||||
m_progress->setMaximumWidth(380);
|
m_progress->setMaximumWidth(500);
|
||||||
addWidget(m_progress);
|
|
||||||
|
m_totalLabel = new QLabel(QStringLiteral("0:00"), centerWidget);
|
||||||
|
m_totalLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
|
||||||
|
m_totalLabel->setMinimumWidth(36);
|
||||||
|
|
||||||
connect(m_progress, &QSlider::sliderPressed, this, [this] { m_seeking = true; });
|
connect(m_progress, &QSlider::sliderPressed, this, [this] { m_seeking = true; });
|
||||||
connect(m_progress, &QSlider::sliderReleased, this, &MainToolBar::onProgressReleased);
|
connect(m_progress, &QSlider::sliderReleased, this, &MainToolBar::onProgressReleased);
|
||||||
|
|
||||||
m_timeLabel = new QLabel("0:00 / 0:00", this);
|
progRow->addWidget(m_elapsedLabel);
|
||||||
addWidget(m_timeLabel);
|
progRow->addWidget(m_progress, 1);
|
||||||
|
progRow->addWidget(m_totalLabel);
|
||||||
|
|
||||||
addSeparator();
|
centerLo->addLayout(ctrlRow);
|
||||||
|
centerLo->addLayout(progRow);
|
||||||
|
|
||||||
// --- Volume ---
|
// ---- RIGHT: volume + shuffle + queue + search ----
|
||||||
m_volume = new VolumeButton(this);
|
auto *rightWidget = new QWidget(root);
|
||||||
|
auto *rightLo = new QHBoxLayout(rightWidget);
|
||||||
|
rightLo->setContentsMargins(0, 0, 0, 0);
|
||||||
|
rightLo->setSpacing(4);
|
||||||
|
|
||||||
|
m_volume = new VolumeButton(rightWidget);
|
||||||
m_volume->setValue(AppSettings::instance().volume());
|
m_volume->setValue(AppSettings::instance().volume());
|
||||||
addWidget(m_volume);
|
|
||||||
connect(m_volume, &VolumeButton::volumeChanged, this, &MainToolBar::onVolumeChanged);
|
connect(m_volume, &VolumeButton::volumeChanged, this, &MainToolBar::onVolumeChanged);
|
||||||
|
|
||||||
addSeparator();
|
auto makeToggle = [&](const QIcon &icon, const QString &tip) -> QToolButton * {
|
||||||
|
auto *btn = new QToolButton(rightWidget);
|
||||||
|
btn->setIcon(icon);
|
||||||
|
btn->setToolTip(tip);
|
||||||
|
btn->setCheckable(true);
|
||||||
|
btn->setAutoRaise(true);
|
||||||
|
btn->setIconSize(QSize(22, 22));
|
||||||
|
return btn;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Shuffle ---
|
m_shuffleBtn = makeToggle(Icon::get(QStringLiteral("media-playlist-shuffle")), tr("Shuffle"));
|
||||||
m_shuffle = addAction(Icon::get(QStringLiteral("media-playlist-shuffle")), tr("Shuffle"));
|
m_queueBtn = makeToggle(Icon::queue(), tr("Queue"));
|
||||||
m_shuffle->setCheckable(true);
|
m_searchBtn = makeToggle(Icon::search(), tr("Search"));
|
||||||
connect(m_shuffle, &QAction::toggled, this, &MainToolBar::onShuffleToggled);
|
|
||||||
|
|
||||||
addSeparator();
|
connect(m_shuffleBtn, &QToolButton::toggled, this, &MainToolBar::onShuffleToggled);
|
||||||
|
connect(m_queueBtn, &QToolButton::toggled, this, &MainToolBar::queueToggled);
|
||||||
|
connect(m_searchBtn, &QToolButton::toggled, this, &MainToolBar::searchToggled);
|
||||||
|
|
||||||
// --- Queue toggle ---
|
rightLo->addStretch(1);
|
||||||
m_queueBtn = addAction(Icon::queue(), tr("Queue"));
|
rightLo->addWidget(m_volume);
|
||||||
m_queueBtn->setCheckable(true);
|
rightLo->addWidget(m_shuffleBtn);
|
||||||
connect(m_queueBtn, &QAction::toggled, this, &MainToolBar::queueToggled);
|
rightLo->addWidget(m_queueBtn);
|
||||||
|
rightLo->addWidget(m_searchBtn);
|
||||||
|
|
||||||
// --- Search toggle ---
|
// ---- Assemble root: equal stretch on left + right keeps center centred ----
|
||||||
m_search = addAction(Icon::search(), tr("Search"));
|
rootLo->addWidget(leftWidget, 1);
|
||||||
m_search->setCheckable(true);
|
rootLo->addWidget(centerWidget, 0, Qt::AlignCenter);
|
||||||
connect(m_search, &QAction::toggled, this, &MainToolBar::searchToggled);
|
rootLo->addWidget(rightWidget, 1);
|
||||||
|
|
||||||
// --- Backend signals ---
|
root->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||||
|
addWidget(root);
|
||||||
|
|
||||||
|
// ---- Backend signals ----
|
||||||
connect(m_backend, &QobuzBackend::stateChanged, this, &MainToolBar::onBackendStateChanged);
|
connect(m_backend, &QobuzBackend::stateChanged, this, &MainToolBar::onBackendStateChanged);
|
||||||
connect(m_backend, &QobuzBackend::trackChanged, this, &MainToolBar::onTrackChanged);
|
connect(m_backend, &QobuzBackend::trackChanged, this, &MainToolBar::onTrackChanged);
|
||||||
connect(m_backend, &QobuzBackend::positionChanged, this, &MainToolBar::onPositionChanged);
|
connect(m_backend, &QobuzBackend::positionChanged, this, &MainToolBar::onPositionChanged);
|
||||||
connect(m_backend, &QobuzBackend::trackFinished, this, &MainToolBar::onTrackFinished);
|
connect(m_backend, &QobuzBackend::trackFinished, this, &MainToolBar::onTrackFinished);
|
||||||
|
|
||||||
// --- Queue signals ---
|
// ---- Queue signals ----
|
||||||
connect(m_queue, &PlayQueue::queueChanged, this, &MainToolBar::onQueueChanged);
|
connect(m_queue, &PlayQueue::queueChanged, this, &MainToolBar::onQueueChanged);
|
||||||
onQueueChanged(); // initialise button states
|
onQueueChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- public ----
|
// ---- public ----
|
||||||
@@ -107,14 +173,14 @@ MainToolBar::MainToolBar(QobuzBackend *backend, PlayQueue *queue, QWidget *paren
|
|||||||
void MainToolBar::setPlaying(bool playing)
|
void MainToolBar::setPlaying(bool playing)
|
||||||
{
|
{
|
||||||
m_playing = playing;
|
m_playing = playing;
|
||||||
m_playPause->setIcon(playing ? Icon::pause() : Icon::play());
|
m_playBtn->setIcon(playing ? Icon::pause() : Icon::play());
|
||||||
m_playPause->setText(playing ? tr("Pause") : tr("Play"));
|
m_playBtn->setToolTip(playing ? tr("Pause") : tr("Play"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainToolBar::setCurrentTrack(const QJsonObject &track)
|
void MainToolBar::setCurrentTrack(const QJsonObject &track)
|
||||||
{
|
{
|
||||||
const QString title = track["title"].toString();
|
const QString title = track["title"].toString();
|
||||||
const QString artist = track["performer"].toObject()["name"].toString().isEmpty()
|
const QString artist = track["performer"].toObject()["name"].toString().isEmpty()
|
||||||
? track["album"].toObject()["artist"].toObject()["name"].toString()
|
? track["album"].toObject()["artist"].toObject()["name"].toString()
|
||||||
: track["performer"].toObject()["name"].toString();
|
: track["performer"].toObject()["name"].toString();
|
||||||
|
|
||||||
@@ -123,10 +189,9 @@ void MainToolBar::setCurrentTrack(const QJsonObject &track)
|
|||||||
} else if (artist.isEmpty()) {
|
} else if (artist.isEmpty()) {
|
||||||
m_trackLabel->setText(title);
|
m_trackLabel->setText(title);
|
||||||
} else {
|
} else {
|
||||||
m_trackLabel->setText(QStringLiteral("%1 — %2").arg(artist, title));
|
m_trackLabel->setText(QStringLiteral("%1\n%2").arg(title, artist));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Album art
|
|
||||||
const QString artUrl = track["album"].toObject()["image"].toObject()["small"].toString();
|
const QString artUrl = track["album"].toObject()["image"].toObject()["small"].toString();
|
||||||
if (!artUrl.isEmpty() && artUrl != m_currentArtUrl) {
|
if (!artUrl.isEmpty() && artUrl != m_currentArtUrl) {
|
||||||
m_currentArtUrl = artUrl;
|
m_currentArtUrl = artUrl;
|
||||||
@@ -142,10 +207,8 @@ void MainToolBar::updateProgress(quint64 position, quint64 duration)
|
|||||||
m_progress->blockSignals(true);
|
m_progress->blockSignals(true);
|
||||||
m_progress->setValue(sliderPos);
|
m_progress->setValue(sliderPos);
|
||||||
m_progress->blockSignals(false);
|
m_progress->blockSignals(false);
|
||||||
m_timeLabel->setText(
|
m_elapsedLabel->setText(TrackListModel::formatDuration(static_cast<qint64>(position)));
|
||||||
QStringLiteral("%1 / %2")
|
m_totalLabel->setText(TrackListModel::formatDuration(static_cast<qint64>(duration)));
|
||||||
.arg(TrackListModel::formatDuration(static_cast<qint64>(position)),
|
|
||||||
TrackListModel::formatDuration(static_cast<qint64>(duration))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- private slots ----
|
// ---- private slots ----
|
||||||
@@ -207,20 +270,20 @@ void MainToolBar::onPositionChanged(quint64 position, quint64 duration)
|
|||||||
|
|
||||||
void MainToolBar::onTrackFinished()
|
void MainToolBar::onTrackFinished()
|
||||||
{
|
{
|
||||||
// Auto-advance queue
|
|
||||||
if (m_queue->canGoNext()) {
|
if (m_queue->canGoNext()) {
|
||||||
onNext();
|
onNext();
|
||||||
} else {
|
} else {
|
||||||
setPlaying(false);
|
setPlaying(false);
|
||||||
m_progress->setValue(0);
|
m_progress->setValue(0);
|
||||||
m_timeLabel->setText("0:00 / 0:00");
|
m_elapsedLabel->setText(QStringLiteral("0:00"));
|
||||||
|
m_totalLabel->setText(QStringLiteral("0:00"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainToolBar::onQueueChanged()
|
void MainToolBar::onQueueChanged()
|
||||||
{
|
{
|
||||||
m_previous->setEnabled(m_queue->canGoPrev());
|
m_prevBtn->setEnabled(m_queue->canGoPrev());
|
||||||
m_next->setEnabled(m_queue->canGoNext());
|
m_nextBtn->setEnabled(m_queue->canGoNext());
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainToolBar::onShuffleToggled(bool checked)
|
void MainToolBar::onShuffleToggled(bool checked)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include <QToolBar>
|
#include <QToolBar>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QAction>
|
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
@@ -50,18 +49,23 @@ private:
|
|||||||
QobuzBackend *m_backend = nullptr;
|
QobuzBackend *m_backend = nullptr;
|
||||||
PlayQueue *m_queue = nullptr;
|
PlayQueue *m_queue = nullptr;
|
||||||
|
|
||||||
QLabel *m_artLabel = nullptr;
|
// Left
|
||||||
QLabel *m_trackLabel = nullptr;
|
QLabel *m_artLabel = nullptr;
|
||||||
ClickableSlider *m_progress = nullptr;
|
QLabel *m_trackLabel = nullptr;
|
||||||
QLabel *m_timeLabel = nullptr;
|
|
||||||
VolumeButton *m_volume = nullptr;
|
|
||||||
|
|
||||||
QAction *m_previous = nullptr;
|
// Center
|
||||||
QAction *m_playPause = nullptr;
|
QToolButton *m_prevBtn = nullptr;
|
||||||
QAction *m_next = nullptr;
|
QToolButton *m_playBtn = nullptr;
|
||||||
QAction *m_shuffle = nullptr;
|
QToolButton *m_nextBtn = nullptr;
|
||||||
QAction *m_queueBtn = nullptr;
|
ClickableSlider *m_progress = nullptr;
|
||||||
QAction *m_search = nullptr;
|
QLabel *m_elapsedLabel = nullptr;
|
||||||
|
QLabel *m_totalLabel = nullptr;
|
||||||
|
|
||||||
|
// Right
|
||||||
|
VolumeButton *m_volume = nullptr;
|
||||||
|
QToolButton *m_shuffleBtn = nullptr;
|
||||||
|
QToolButton *m_queueBtn = nullptr;
|
||||||
|
QToolButton *m_searchBtn = nullptr;
|
||||||
|
|
||||||
QNetworkAccessManager *m_nam = nullptr;
|
QNetworkAccessManager *m_nam = nullptr;
|
||||||
QString m_currentArtUrl;
|
QString m_currentArtUrl;
|
||||||
|
|||||||
Reference in New Issue
Block a user