clean structure

This commit is contained in:
2026-06-11 14:03:35 +02:00
parent 326424f6fa
commit 677f263648
29 changed files with 119 additions and 82 deletions
+40
View File
@@ -0,0 +1,40 @@
#include "CoverArtWidget.h"
#include <QPainter>
#include <QPixmap>
CoverArtWidget::CoverArtWidget(QWidget *parent) : QWidget(parent) {
setMinimumSize(130, 130);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
}
void CoverArtWidget::setTrack(const engine::Track *t) {
m_cover = (t && !t->coverArt.isNull()) ? t->coverArt : QImage{};
m_title = t ? t->displayTitle() : "No track";
m_artist = t ? t->artist : QString{};
update();
}
void CoverArtWidget::paintEvent(QPaintEvent *) {
QPainter p(this);
p.setRenderHint(QPainter::SmoothPixmapTransform);
int w = width(), h = height();
if (!m_cover.isNull()) {
QPixmap pm = QPixmap::fromImage(m_cover);
double sx = double(w)/pm.width(), sy = double(h)/pm.height();
double sc = std::max(sx, sy);
int dw = int(pm.width()*sc), dh = int(pm.height()*sc);
p.drawPixmap((w-dw)/2, (h-dh)/2, dw, dh, pm);
} else {
p.fillRect(rect(), palette().window());
p.setPen(palette().windowText().color());
QFont f = p.font(); f.setBold(true); f.setPointSizeF(9); p.setFont(f);
QString l1 = m_title.length()>20 ? m_title.left(18)+"" : m_title;
QString l2 = m_artist.length()>20 ? m_artist.left(18)+"" : m_artist;
QFontMetrics fm(p.font());
p.drawText((w-fm.horizontalAdvance(l1))/2, h/2, l1);
f.setBold(false); f.setPointSizeF(8); p.setFont(f);
QFontMetrics fm2(p.font());
p.drawText((w-fm2.horizontalAdvance(l2))/2, h/2+fm2.height()+2, l2);
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <QWidget>
#include <QImage>
#include "engine/Track.h"
class CoverArtWidget : public QWidget {
Q_OBJECT
public:
explicit CoverArtWidget(QWidget *parent = nullptr);
void setTrack(const engine::Track *t);
protected:
void paintEvent(QPaintEvent *) override;
private:
QImage m_cover;
QString m_title { "No track" };
QString m_artist;
};
+326
View File
@@ -0,0 +1,326 @@
#include "EqualizerWidget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QInputDialog>
#include <QFileDialog>
#include <QStyleFactory>
#include <QFile>
#include <QDir>
#include <QPainter>
#include <QPainterPath>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <algorithm>
#include <cmath>
static const inline QVector<QPair<QString,QString>> BUILTIN_PRESETS = {
{"Flat", "0,0,0,0,0,0,0,0,0,0"},
{"Bass Boost", "8,6,4,2,0,0,0,0,0,0"},
{"Treble Boost", "0,0,0,0,0,0,2,4,6,8"},
{"V-Shape", "6,4,2,-1,-3,-3,-1,2,4,6"},
{"Rock", "4,3,1,0,-1,0,2,3,3,2"},
{"Jazz", "3,2,1,2,0,-1,-1,0,1,2"},
{"Classical", "0,0,0,0,0,0,-2,-3,-3,-2"},
{"Electronic", "5,3,0,-1,-3,0,1,2,4,5"},
};
static const inline QString FREQ_LABELS[] = {"32Hz","64Hz","125Hz","250Hz","500Hz","1kHz","2kHz","4kHz","8kHz","16kHz"};
QString EqualizerWidget::presetFile() { return QDir::homePath() + "/.subwave/eq_presets.json"; }
QString EqualizerWidget::stateFile() { return QDir::homePath() + "/.subwave/eq_state.json"; }
EqualizerWidget::EqualizerWidget(AudioEngine *engine, QWidget *parent)
: QWidget(parent), m_engine(engine)
{
auto *vlay = new QVBoxLayout(this);
vlay->setSpacing(4);
vlay->setContentsMargins(4,4,4,4);
auto *topBar = new QHBoxLayout;
topBar->addWidget(new QLabel("Preset:"));
m_presetBox = new QComboBox;
m_presetBox->setStyle(QStyleFactory::create("Fusion"));
topBar->addWidget(m_presetBox);
auto *saveBtn = new QPushButton("Save...");
auto *deleteBtn = new QPushButton("Delete");
auto *resetBtn = new QPushButton("Reset");
topBar->addWidget(saveBtn);
topBar->addWidget(deleteBtn);
topBar->addWidget(resetBtn);
topBar->addStretch();
vlay->addLayout(topBar);
m_curve = new FreqCurveWidget(this);
m_curve->setMinimumHeight(80);
vlay->addWidget(m_curve);
auto *sliderRow = new QHBoxLayout;
sliderRow->setSpacing(2);
for (int i = 0; i < BANDS; i++) {
const int band = i;
auto *col = new QVBoxLayout;
col->setSpacing(1);
col->setAlignment(Qt::AlignHCenter);
m_dbLabels[i] = new QLabel("0 dB");
m_dbLabels[i]->setAlignment(Qt::AlignHCenter);
// Fixed width and monospace to prevent left/right shifting
QFont sf("Monospace", 8);
sf.setStyleHint(QFont::Monospace);
m_dbLabels[i]->setFont(sf);
m_dbLabels[i]->setFixedWidth(50); // ample room for "-12 dB"
m_sliders[i] = new QSlider(Qt::Vertical);
m_sliders[i]->setRange(-120, 120);
m_sliders[i]->setValue(0);
m_sliders[i]->setTickPosition(QSlider::TicksBothSides);
m_sliders[i]->setTickInterval(60);
auto *freqLbl = new QLabel(FREQ_LABELS[i]);
freqLbl->setAlignment(Qt::AlignHCenter);
QFont ff = freqLbl->font(); ff.setPointSizeF(7); freqLbl->setFont(ff);
col->addWidget(m_dbLabels[i]);
col->addWidget(m_sliders[i]);
col->addWidget(freqLbl);
sliderRow->addLayout(col);
connect(m_sliders[i], &QSlider::valueChanged, this, [this, band](int val){
float db = val / 10.f;
m_engine->setEqBand(band, db);
m_dbLabels[band]->setText(QString("%1%2 dB")
.arg(db >= 0 ? "+" : "").arg(db, 0, 'f', 0));
m_curve->update();
});
}
m_curve->setSliders(m_sliders);
vlay->addLayout(sliderRow);
loadPresets();
populatePresetBox();
loadState();
connect(m_presetBox, QOverload<int>::of(&QComboBox::activated),
this, [this](int idx) {
m_presetBox->hidePopup(); // force-close before anything else
m_presetBox->setEnabled(false);
QMetaObject::invokeMethod(this, [this, idx] {
applyPresetAt(idx);
m_presetBox->setEnabled(true);
}, Qt::QueuedConnection);
});
connect(saveBtn, &QPushButton::clicked, this, [this]{
bool ok = false;
QString name = QInputDialog::getText(this, "Save Preset", "Preset name:", QLineEdit::Normal, "", &ok);
if (!ok || name.trimmed().isEmpty()) return;
QString vals;
for (int i=0; i<BANDS; i++) {
if (i) vals += ',';
vals += QString::number(m_sliders[i]->value()/10.f,'f',1);
}
m_userPresets.removeIf([&](const QPair<QString,QString> &p){ return p.first == name; });
m_userPresets.append({name, vals});
savePresets();
populatePresetBox();
});
connect(deleteBtn, &QPushButton::clicked, this, [this]{
int idx = m_presetBox->currentIndex();
int builtinCount = BUILTIN_PRESETS.size();
if (idx < builtinCount) {
QMessageBox::information(this, "SubWave", "Built-in presets cannot be deleted.");
return;
}
int userIdx = idx - builtinCount;
if (userIdx < 0 || userIdx >= m_userPresets.size()) return;
m_userPresets.removeAt(userIdx);
savePresets();
populatePresetBox();
});
connect(resetBtn, &QPushButton::clicked, this, [this]{
m_presetBox->hidePopup();
m_presetBox->setCurrentIndex(0);
m_presetBox->setEnabled(false);
QMetaObject::invokeMethod(this, [this]{
applyPresetAt(0);
m_presetBox->setEnabled(true);
}, Qt::QueuedConnection);
});
}
void EqualizerWidget::saveState() {
QDir().mkpath(QFileInfo(stateFile()).absolutePath());
QFile f(stateFile());
if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return;
QJsonObject obj;
QString vals;
for (int i=0; i<BANDS; i++) {
if (i) vals += ',';
vals += QString::number(m_sliders[i]->value());
}
obj["bands"] = vals;
f.write(QJsonDocument(obj).toJson());
}
void EqualizerWidget::loadPresets() {
QFile f(presetFile());
if (!f.open(QIODevice::ReadOnly)) return;
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
m_userPresets.clear();
if (!doc.isArray()) return;
for (const QJsonValue &v : doc.array()) {
QJsonObject obj = v.toObject();
QString name = obj["name"].toString();
QString bands = obj["bands"].toString();
if (!name.isEmpty() && !bands.isEmpty())
m_userPresets.append({name, bands});
}
}
void EqualizerWidget::savePresets() {
QDir().mkpath(QFileInfo(presetFile()).absolutePath());
QFile f(presetFile());
if (!f.open(QIODevice::WriteOnly|QIODevice::Truncate)) return;
QJsonArray arr;
for (const auto &pr : m_userPresets) {
QJsonObject obj;
obj["name"] = pr.first;
obj["bands"] = pr.second;
arr.append(obj);
}
f.write(QJsonDocument(arr).toJson());
}
void EqualizerWidget::populatePresetBox() {
QSignalBlocker blk(m_presetBox);
m_presetBox->clear();
for (auto &pr : BUILTIN_PRESETS) m_presetBox->addItem(pr.first);
for (auto &pr : m_userPresets) m_presetBox->addItem(pr.first + "");
}
void EqualizerWidget::applyPresetAt(int idx) {
if (idx < 0) return;
QString vals;
if (idx < BUILTIN_PRESETS.size())
vals = BUILTIN_PRESETS[idx].second;
else {
int ui = idx - BUILTIN_PRESETS.size();
if (ui < 0 || ui >= m_userPresets.size()) return;
vals = m_userPresets[ui].second;
}
QStringList parts = vals.split(',');
for (int i=0; i<BANDS && i<parts.size(); i++) {
bool ok=false;
float db = parts[i].trimmed().toFloat(&ok);
if (ok) {
QSignalBlocker blk(m_sliders[i]);
m_sliders[i]->setValue(qRound(db*10));
m_engine->setEqBand(i, db);
m_dbLabels[i]->setText(QString("%1%2 dB")
.arg(db>=0?"+":"").arg(db,0,'f',0));
}
}
m_curve->update();
}
void EqualizerWidget::loadState() {
QFile f(stateFile());
if (!f.open(QIODevice::ReadOnly)) return;
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
if (!doc.isObject()) return;
QJsonObject obj = doc.object();
if (!obj.contains("bands")) return;
QString vals = obj["bands"].toString();
QStringList parts = vals.split(',');
for (int i=0; i<BANDS && i<parts.size(); i++) {
bool ok=false;
int raw = parts[i].trimmed().toInt(&ok);
if (ok) {
QSignalBlocker blk(m_sliders[i]);
m_sliders[i]->setValue(raw);
float db = raw/10.f;
m_engine->setEqBand(i, db);
m_dbLabels[i]->setText(QString("%1%2 dB")
.arg(db>=0?"+":"").arg(db,0,'f',0));
}
}
m_curve->update();
}
void EqualizerWidget::FreqCurveWidget::paintEvent(QPaintEvent *) {
if (!m_sliders) return;
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
int w = width(), h = height();
const int labelArea = 20;
int graphH = h - labelArea;
p.fillRect(rect(), palette().base());
float logMin = std::log10(20.f);
float logMax = std::log10(20000.f);
auto freqToX = [&](float f) -> float {
return (std::log10(f) - logMin) / (logMax - logMin) * w;
};
// 0 dB line
float midY = graphH / 2.f;
p.setPen(QColor(180, 180, 180));
p.drawLine(0, int(midY), w, int(midY));
// Draw frequency axis ticks and labels
const float tickFreqs[] = {20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000};
QFont tickFont = font();
tickFont.setPointSizeF(7);
p.setFont(tickFont);
p.setPen(palette().windowText().color());
for (float fq : tickFreqs) {
float x = freqToX(fq);
if (x < 0 || x > w) continue;
// Tick mark
p.drawLine(QPointF(x, graphH), QPointF(x, graphH + 3));
// Label
QString label;
if (fq >= 1000) label = QString::number(fq / 1000, 'f', 0) + "k";
else label = QString::number(fq, 'f', 0);
QRectF textRect(x - 20, graphH + 3, 40, labelArea - 3);
p.drawText(textRect, Qt::AlignHCenter | Qt::AlignTop, label);
}
// Draw EQ curve
QPainterPath path;
for (int i = 0; i < w; ++i) {
float f = std::pow(10.f, logMin + (logMax - logMin) * i / w);
double totalDb = 0.0;
for (int b = 0; b < BANDS; ++b) {
float db = m_sliders[b]->value() / 10.f;
if (std::abs(db) < 0.01f) continue;
float f0 = AudioEngine::EQ_FREQS[b], Q = 1.41421356f;
double ratio = f / f0;
double lrSq = (ratio - 1.0 / ratio) * (ratio - 1.0 / ratio);
double denom = 1.0 + lrSq / (Q * Q * (ratio + 1.0 / ratio) * (ratio + 1.0 / ratio));
totalDb += db * denom;
}
float py = midY - float(totalDb / 12.0 * graphH / 2 * 0.9);
py = std::clamp(py, 0.f, float(graphH));
if (i == 0) path.moveTo(i, py);
else path.lineTo(i, py);
}
p.setPen(QPen(palette().windowText().color(), 1.5));
p.drawPath(path);
// Draw control points (band handles)
p.setPen(Qt::NoPen);
p.setBrush(QColor(60, 120, 255));
for (int b = 0; b < BANDS; ++b) {
float f0 = AudioEngine::EQ_FREQS[b];
float db = m_sliders[b]->value() / 10.f;
float nx = freqToX(f0);
float ny = midY - (db / 12.f * graphH / 2 * 0.9f);
ny = std::clamp(ny, 0.f, float(graphH));
p.drawEllipse(QPointF(nx, ny), 3, 3);
}
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <QWidget>
#include <QComboBox>
#include <QSlider>
#include <QLabel>
#include <QPushButton>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include "AudioEngine.h"
class EqualizerWidget : public QWidget {
Q_OBJECT
static constexpr int BANDS = AudioEngine::EQ_BANDS;
public:
explicit EqualizerWidget(AudioEngine *engine, QWidget *parent = nullptr);
void saveState();
private:
void loadPresets();
void savePresets();
void populatePresetBox();
void applyPresetAt(int idx);
void loadState();
AudioEngine *m_engine;
QComboBox *m_presetBox;
QSlider *m_sliders[BANDS] {};
QLabel *m_dbLabels[BANDS] {};
QVector<QPair<QString,QString>> m_userPresets;
class FreqCurveWidget : public QWidget {
public:
explicit FreqCurveWidget(QWidget *p) : QWidget(p) {}
void setSliders(QSlider *sliders[BANDS]) { m_sliders = sliders; }
protected:
void paintEvent(QPaintEvent *) override;
private:
QSlider **m_sliders { nullptr };
} *m_curve;
static QString presetFile();
static QString stateFile();
};
+88
View File
@@ -0,0 +1,88 @@
#include "GradientDialog.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#include <QColorDialog>
#include <QLabel>
#include <QPainter>
#include <QPixmap>
GradientDialog::GradientDialog(const QVector<QColor> &stops,
QWidget *parent)
: QDialog(parent), m_stops(stops)
{
setWindowTitle("Bar Colour Gradient");
setMinimumSize(420, 180);
auto *mainLay = new QVBoxLayout(this);
// Preview
m_preview = new QLabel;
m_preview->setFixedHeight(40);
m_preview->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
mainLay->addWidget(m_preview);
// Colour stop buttons
auto *btnLay = new QHBoxLayout;
QStringList labels = { "Low", "Mid", "High", "Peak" };
for (int i = 0; i < m_stops.size(); ++i) {
auto *btn = new QPushButton(labels.value(i, QString::number(i)));
btn->setFixedSize(70, 30);
connect(btn, &QPushButton::clicked, this, [this, i]{ onStopClicked(i); });
btnLay->addWidget(btn);
m_buttons.append(btn);
}
mainLay->addLayout(btnLay);
// Action buttons
auto *dialogBtns = new QHBoxLayout;
auto *resetBtn = new QPushButton("Reset to Default");
connect(resetBtn, &QPushButton::clicked, this, &GradientDialog::onResetToDefault);
dialogBtns->addWidget(resetBtn);
dialogBtns->addStretch();
auto *ok = new QPushButton("Apply");
connect(ok, &QPushButton::clicked, this, &QDialog::accept);
dialogBtns->addWidget(ok);
mainLay->addLayout(dialogBtns);
updatePreview();
}
void GradientDialog::onStopClicked(int index)
{
QColor c = QColorDialog::getColor(m_stops[index], this,
QString("Choose %1 colour").arg(index));
if (c.isValid()) {
m_stops[index] = c;
updatePreview();
}
}
void GradientDialog::onResetToDefault()
{
m_stops = {
QColor("#078D70"), // Low
QColor("#26CEAA"), // Mid
QColor("#98E8C1"), // High
QColor("#FFFFFF") // Peak
};
updatePreview();
}
void GradientDialog::updatePreview()
{
QPixmap pix(m_preview->size());
if (pix.width() == 0) pix = QPixmap(400, 40); // fallback size
pix.fill(Qt::transparent);
QPainter p(&pix);
QLinearGradient grad(0, 0, pix.width(), 0);
for (int i = 0; i < m_stops.size(); ++i) {
qreal pos = (m_stops.size() == 1) ? 0.5
: qreal(i) / (m_stops.size() - 1);
grad.setColorAt(pos, m_stops[i]);
}
p.setBrush(grad);
p.drawRect(pix.rect());
p.end();
m_preview->setPixmap(pix);
}
+31
View File
@@ -0,0 +1,31 @@
#ifndef GRADIENTDIALOG_H
#define GRADIENTDIALOG_H
#include <QDialog>
#include <QColor>
#include <QVector>
class QPushButton;
class QLabel;
class GradientDialog : public QDialog
{
Q_OBJECT
public:
explicit GradientDialog(const QVector<QColor> &stops,
QWidget *parent = nullptr);
QVector<QColor> stops() const { return m_stops; }
private slots:
void onStopClicked(int index);
void onResetToDefault();
private:
QVector<QColor> m_stops;
QVector<QPushButton*> m_buttons;
QLabel *m_preview;
void updatePreview();
};
#endif
+419
View File
@@ -0,0 +1,419 @@
#include "MainWindow.h"
#include "AudioEngine.h"
#include "engine/MusicLibrary.h"
#include "PlaylistModel.h"
#include "TrackDelegate.h"
#include "CoverArtWidget.h"
#include "SpectrogramWidget.h"
#include "EqualizerWidget.h"
#include "Config.h"
#include "PlaylistCreatorDialog.h"
#include "PlaylistBrowserDialog.h"
#include "PlaylistManager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QMenuBar>
#include <QAction>
#include <QFileDialog>
#include <QMessageBox>
#include <QInputDialog>
#include <QCloseEvent>
#include <QKeySequence>
#include <QRandomGenerator>
#include <QDir>
#include <QApplication>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle("SubWave");
setMinimumSize(700, 760);
m_engine = new AudioEngine(this);
m_library = new engine::MusicLibrary(this);
m_model = new PlaylistModel(this);
m_library->setRootPath(Config::loadMusicRoot());
buildUI();
wireSignals();
m_uiTimer.setInterval(500);
connect(&m_uiTimer, &QTimer::timeout, this, &MainWindow::onUiTimer);
m_uiTimer.start();
reloadLibrary();
}
MainWindow::~MainWindow()
{
m_eqWidget->saveState();
m_engine->stop();
m_library->shutdown();
}
void MainWindow::closeEvent(QCloseEvent *e)
{
m_eqWidget->saveState();
m_engine->stop();
m_library->shutdown();
e->accept();
}
// ── UI Construction ─────────────────────────────────────────────────────────
void MainWindow::buildUI()
{
auto *central = new QWidget;
setCentralWidget(central);
auto *mainVlay = new QVBoxLayout(central);
mainVlay->setSpacing(4);
mainVlay->setContentsMargins(4, 4, 4, 4);
mainVlay->addWidget(buildNowPlayingPanel());
m_spectrogram = new SpectrogramWidget;
mainVlay->addWidget(m_spectrogram, 1);
mainVlay->addWidget(buildBottomTabs());
buildMenuBar();
}
QGroupBox *MainWindow::buildNowPlayingPanel()
{
auto *box = new QGroupBox("Now Playing");
auto *hlay = new QHBoxLayout(box);
m_coverWidget = new CoverArtWidget;
m_coverWidget->setFixedSize(130, 130);
hlay->addWidget(m_coverWidget);
auto *right = new QVBoxLayout;
right->setSpacing(4);
QFont boldF = m_lblTitle->font();
boldF.setBold(true);
boldF.setPointSizeF(11);
m_lblTitle->setFont(boldF);
right->addWidget(m_lblTitle);
right->addWidget(m_lblArtist);
right->addWidget(m_lblAlbum);
// Seek row
auto *seekRow = new QHBoxLayout;
m_seekBar->setRange(0, 1000);
m_seekBar->setValue(0);
seekRow->addWidget(m_seekBar);
seekRow->addWidget(m_lblTime);
right->addLayout(seekRow);
right->addLayout(buildTransportRow());
hlay->addLayout(right, 1);
return box;
}
QHBoxLayout *MainWindow::buildTransportRow()
{
auto *row = new QHBoxLayout;
auto *btns = new QHBoxLayout;
btns->setSpacing(3);
btns->addWidget(m_btnPrev);
btns->addWidget(m_btnPlay);
btns->addWidget(m_btnNext);
btns->addSpacing(8);
btns->addWidget(m_chkShuffle);
btns->addWidget(m_chkRepeat);
row->addLayout(btns);
row->addStretch();
row->addWidget(new QLabel("Volume:"));
m_volSlider->setRange(0, 100);
m_volSlider->setValue(80);
m_volSlider->setFixedWidth(90);
row->addWidget(m_volSlider);
return row;
}
QTabWidget *MainWindow::buildBottomTabs()
{
auto *tabs = new QTabWidget;
// Playlist tab
auto *plPanel = new QWidget;
auto *plVlay = new QVBoxLayout(plPanel);
plVlay->setContentsMargins(0, 2, 0, 0);
m_playlistView = new QListView;
m_playlistView->setModel(m_model);
m_playlistView->setItemDelegate(new TrackDelegate(this));
m_playlistView->setSelectionMode(QAbstractItemView::SingleSelection);
m_playlistView->setUniformItemSizes(true);
m_playlistView->setAlternatingRowColors(true);
m_playlistView->setMinimumHeight(220);
plVlay->addWidget(m_playlistView);
plVlay->addWidget(m_lblStatus);
tabs->addTab(plPanel, "Playlist");
// EQ tab
m_eqWidget = new EqualizerWidget(m_engine);
tabs->addTab(m_eqWidget, "Equalizer");
return tabs;
}
void MainWindow::buildMenuBar()
{
auto *mb = menuBar();
auto *file = mb->addMenu("&File");
auto *chooseAct = file->addAction("Choose Music Location…");
chooseAct->setShortcut(QKeySequence::Open);
connect(chooseAct, &QAction::triggered, this, [this]{
QString dir = QFileDialog::getExistingDirectory(
this, "Select Music Folder",
m_library->rootPath(),
QFileDialog::ShowDirsOnly);
if (dir.isEmpty()) return;
m_library->setRootPath(dir);
Config::saveMusicRoot(dir);
setWindowTitle("SubWave - " + QDir(dir).dirName());
reloadLibrary();
});
auto *refreshAct = file->addAction("Refresh Library");
refreshAct->setShortcut(Qt::Key_F5);
connect(refreshAct, &QAction::triggered, this, &MainWindow::reloadLibrary);
file->addSeparator();
auto *quitAct = file->addAction("Quit");
quitAct->setShortcut(QKeySequence::Quit);
connect(quitAct, &QAction::triggered, qApp, &QApplication::quit);
// Playlists menu
auto *playlists = mb->addMenu("&Playlists");
auto *newPlaylistAct = playlists->addAction("Create Playlist…");
connect(newPlaylistAct, &QAction::triggered, this, &MainWindow::onCreatePlaylist);
auto *loadPlaylistAct = playlists->addAction("Load Playlist…");
connect(loadPlaylistAct, &QAction::triggered, this, &MainWindow::onLoadPlaylist);
auto *help = mb->addMenu("&Help");
auto *about = help->addAction("About SubWave");
connect(about, &QAction::triggered, this, [this]{
QMessageBox::about(this, "About SubWave",
"SubWave - Qt6/FFmpeg music player\n\n"
"Supports MP3, FLAC, WAV, OGG, AAC, M4A, Opus, WMA.\n"
"Features a 10-band parametric EQ with user presets,\n"
"real-time spectrogram & waterfall display, shuffle,\n"
"repeat, and persistent library + EQ state.\n\n"
"Config: ~/.subwave/");
});
}
// ── Signal/Slot Wiring ──────────────────────────────────────────────────────
void MainWindow::wireSignals()
{
// Transport buttons
connect(m_btnPlay, &QPushButton::clicked, this, &MainWindow::togglePlayPause);
/*connect(m_btnStop, &QPushButton::clicked, this, [this]{
m_engine->stop();
m_btnPlay->setText("Play");
});*/
connect(m_btnPrev, &QPushButton::clicked, this, &MainWindow::playPrev);
connect(m_btnNext, &QPushButton::clicked, this, &MainWindow::playNext);
connect(m_volSlider, &QSlider::valueChanged, this, [this](int v){
m_engine->setVolume(v / 100.f);
});
// Seek
connect(m_seekBar, &QSlider::sliderPressed, this, [this]{ m_seekDragging = true; });
connect(m_seekBar, &QSlider::sliderReleased, this, [this]{
m_seekDragging = false;
m_engine->seekFraction(m_seekBar->value() / 1000.0);
});
// Doubleclick to play
connect(m_playlistView, &QListView::doubleClicked,
this, [this](const QModelIndex &idx){ playTrack(idx.row()); });
// Engine callbacks
connect(m_engine, &AudioEngine::trackEnded,
this, &MainWindow::playNext, Qt::QueuedConnection);
connect(m_engine, &AudioEngine::positionChanged,
this, [this](qint64 posFrames){
if (!m_seekDragging) {
qint64 total = m_engine->totalFrames();
if (total > 0)
m_seekBar->setValue(int(posFrames * 1000 / total));
}
}, Qt::QueuedConnection);
connect(m_engine, &AudioEngine::fftReady,
m_spectrogram, &SpectrogramWidget::receiveFft, Qt::QueuedConnection);
connect(m_engine, &AudioEngine::durationKnown,
this, [this](qint64 secs){
if (m_currentIndex >= 0) {
engine::Track *t = m_model->trackAt(m_currentIndex);
if (t && t->durationSecs == 0) {
t->durationSecs = secs;
m_model->refreshRow(m_currentIndex);
}
}
}, Qt::QueuedConnection);
// Library scanning
connect(m_library, &engine::MusicLibrary::scanStarted, this, [this]{
m_model->setTracks({});
m_lblStatus->setText("Scanning...");
});
connect(m_library, &engine::MusicLibrary::trackFound, this, [this](const engine::Track &t){
m_model->appendTrack(t);
m_lblStatus->setText(QString("%1 tracks found").arg(m_model->rowCount({})));
}, Qt::QueuedConnection);
connect(m_library, &engine::MusicLibrary::scanComplete, this, [this](int n){
m_model->setTracks(m_library->tracks()); // sorted version
m_lblStatus->setText(QString("%1 tracks.").arg(n));
}, Qt::QueuedConnection);
}
// ── Playback Control ────────────────────────────────────────────────────────
void MainWindow::reloadLibrary()
{
m_library->scan();
}
void MainWindow::playTrack(int index)
{
engine::Track *t = m_model->trackAt(index);
if (!t) return;
m_currentIndex = index;
m_model->setCurrentIndex(index);
m_engine->play(*t);
m_coverWidget->setTrack(t);
m_lblTitle->setText(t->displayTitle());
m_lblArtist->setText(t->artist);
m_lblAlbum->setText(t->album + (t->year > 0 ? " (" + QString::number(t->year) + ")" : ""));
m_seekBar->setValue(0);
m_btnPlay->setText("Pause");
m_spectrogram->reset();
m_playlistView->scrollTo(m_model->index(index));
}
void MainWindow::togglePlayPause()
{
if (m_engine->state() == AudioEngine::State::Stopped) {
if (m_currentIndex < 0 && m_model->rowCount({}) > 0)
playTrack(0);
else if (m_currentIndex >= 0)
playTrack(m_currentIndex);
} else {
m_engine->togglePause();
bool paused = (m_engine->state() == AudioEngine::State::Paused);
m_btnPlay->setText(paused ? "Play" : "Pause");
}
}
void MainWindow::playNext()
{
int n = m_model->rowCount({});
if (n == 0) return;
if (m_chkRepeat->isChecked() && m_currentIndex >= 0) {
playTrack(m_currentIndex);
return;
}
int next = m_chkShuffle->isChecked()
? QRandomGenerator::global()->bounded(n)
: ((m_currentIndex + 1) % n);
playTrack(next);
}
void MainWindow::playPrev()
{
if (m_currentIndex <= 0) return;
playTrack(m_currentIndex - 1);
}
// ── UI Timer ────────────────────────────────────────────────────────────────
void MainWindow::onUiTimer()
{
auto fmt = [](qint64 s) -> QString {
return QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
};
float sr = float(m_engine->sampleRate());
qint64 posF = m_engine->positionFrames();
qint64 totF = m_engine->totalFrames();
qint64 posSec = qint64(posF / sr);
engine::Track *currentTrack = (m_currentIndex >= 0) ? m_model->trackAt(m_currentIndex) : nullptr;
qint64 totSec = totF > 0 ? qint64(totF / sr)
: (currentTrack ? currentTrack->durationSecs : 0);
// Write back duration if we now know it
if (totF > 0 && m_currentIndex >= 0) {
engine::Track *t = m_model->trackAt(m_currentIndex);
if (t && t->durationSecs == 0) {
t->durationSecs = totSec;
m_model->refreshRow(m_currentIndex);
}
}
m_lblTime->setText(fmt(posSec) + " / " + fmt(totSec));
if (m_engine->state() == AudioEngine::State::Stopped)
m_btnPlay->setText("Play");
}
// ── Playlist Management ─────────────────────────────────────────────────────
void MainWindow::onCreatePlaylist()
{
PlaylistCreatorDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
// Playlist was created and saved
// Optionally auto-load it
auto playlist = dialog.getPlaylist();
loadPlaylistIntoModel(playlist.name);
}
}
void MainWindow::onLoadPlaylist()
{
PlaylistBrowserDialog dialog(this);
if (dialog.exec() == QDialog::Accepted) {
QString playlistName = dialog.getSelectedPlaylist();
if (!playlistName.isEmpty()) {
loadPlaylistIntoModel(playlistName);
}
}
}
void MainWindow::loadPlaylistIntoModel(const QString &playlistName)
{
auto playlist = PlaylistManager::loadPlaylist(playlistName);
if (playlist.tracks.isEmpty()) {
QMessageBox::warning(this, "Empty Playlist",
QString("Playlist \"%1\" is empty.").arg(playlistName));
return;
}
m_model->setTracks(playlist.tracks);
m_lblStatus->setText(
QString("Playlist: %1 (%2 tracks)")
.arg(playlistName)
.arg(playlist.tracks.size())
);
// Reset playback state
m_currentIndex = -1;
m_engine->stop();
m_btnPlay->setText("Play");
}
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include <QMainWindow>
#include <QSlider>
#include <QLabel>
#include <QPushButton>
#include <QCheckBox>
#include <QListView>
#include <QTimer>
#include <QTabWidget>
#include <QGroupBox>
#include <QHBoxLayout>
// Forward declarations
class AudioEngine;
namespace engine {
class MusicLibrary;
}
class PlaylistModel;
class CoverArtWidget;
class SpectrogramWidget;
class EqualizerWidget;
class PlaylistCreatorDialog;
class PlaylistBrowserDialog;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow() override;
protected:
void closeEvent(QCloseEvent *e) override;
private:
void buildUI();
QGroupBox *buildNowPlayingPanel();
QHBoxLayout *buildTransportRow();
QTabWidget *buildBottomTabs();
void buildMenuBar();
void wireSignals();
void reloadLibrary();
void playTrack(int index);
void togglePlayPause();
void playNext();
void playPrev();
void onUiTimer();
void onCreatePlaylist();
void onLoadPlaylist();
void loadPlaylistIntoModel(const QString &playlistName);
// Core
AudioEngine *m_engine;
engine::MusicLibrary *m_library;
PlaylistModel *m_model;
// Transport plain ASCII labels now
QLabel *m_lblTitle = new QLabel("No track selected");
QLabel *m_lblArtist = new QLabel(" ");
QLabel *m_lblAlbum = new QLabel(" ");
QLabel *m_lblTime = new QLabel("0:00 / 0:00");
QSlider *m_seekBar = new QSlider(Qt::Horizontal);
QPushButton *m_btnPrev = new QPushButton("|<");
QPushButton *m_btnPlay = new QPushButton("> Play");
QPushButton *m_btnNext = new QPushButton(">|");
QCheckBox *m_chkShuffle = new QCheckBox("Shuffle");
QCheckBox *m_chkRepeat = new QCheckBox("Repeat");
QSlider *m_volSlider = new QSlider(Qt::Horizontal);
// Playlist
QListView *m_playlistView;
QLabel *m_lblStatus = new QLabel("No library loaded.");
// Panels
CoverArtWidget *m_coverWidget;
SpectrogramWidget *m_spectrogram;
EqualizerWidget *m_eqWidget;
// State
int m_currentIndex { -1 };
bool m_seekDragging { false };
QTimer m_uiTimer;
};
+296
View File
@@ -0,0 +1,296 @@
#include "SpectrogramWidget.h"
#include "GradientDialog.h"
#include <QTimer>
#include <QSurfaceFormat>
#include <QFile>
#include <QMouseEvent>
#include <QColorDialog>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QDir>
#include <QStandardPaths>
#include <cmath>
#include <cstring>
SpectrogramWidget::SpectrogramWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
setAttribute(Qt::WA_OpaquePaintEvent);
QSurfaceFormat fmt;
fmt.setVersion(3, 3);
fmt.setProfile(QSurfaceFormat::CoreProfile);
fmt.setSwapInterval(1);
fmt.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
setFormat(fmt);
setMinimumSize(400, 200);
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_barHeights.resize(BAR_COUNT);
m_barPeaksVec.resize(BAR_COUNT);
loadBarColors();
auto *timer = new QTimer(this);
connect(timer, &QTimer::timeout,
this, QOverload<>::of(&SpectrogramWidget::update));
timer->start(16);
}
SpectrogramWidget::~SpectrogramWidget()
{
makeCurrent();
glDeleteTextures(1, &m_heatmapTex);
glDeleteVertexArrays(1, &m_vaoBars);
doneCurrent();
}
void SpectrogramWidget::receiveFft(const QVector<float> &mag)
{
QMutexLocker lk(&m_queueMtx);
while (m_fftQueue.size() >= 4)
m_fftQueue.dequeue();
m_fftQueue.enqueue(mag);
update();
}
void SpectrogramWidget::reset()
{
{
QMutexLocker lk(&m_queueMtx);
m_fftQueue.clear();
}
m_currentMag .fill(0.f);
m_barSmoothed.fill(0.f);
m_barPeak .fill(0.f);
m_peakTimer .fill(0);
update();
}
void SpectrogramWidget::initializeGL()
{
initializeOpenGLFunctions();
initShaders();
buildHeatmapTexture();
glGenVertexArrays(1, &m_vaoBars);
glDisable(GL_BLEND);
}
void SpectrogramWidget::initShaders()
{
auto load = [](const QString &path) -> QByteArray {
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning() << "Cannot open shader:" << path;
return {};
}
return f.readAll();
};
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Vertex, load(":/shaders/bars.vert")))
qWarning() << "Bars vert error:" << m_barsProg.log();
if (!m_barsProg.addShaderFromSourceCode(QOpenGLShader::Fragment, load(":/shaders/bars.frag")))
qWarning() << "Bars frag error:" << m_barsProg.log();
if (!m_barsProg.link())
qWarning() << "Bars link error:" << m_barsProg.log();
m_uBarHeights = m_barsProg.uniformLocation("barHeights");
m_uBarPeaks = m_barsProg.uniformLocation("barPeaks");
m_uBarCount = m_barsProg.uniformLocation("barCount");
m_uBarWidth = m_barsProg.uniformLocation("barWidth");
m_uGap = m_barsProg.uniformLocation("gap");
m_uBottomY = m_barsProg.uniformLocation("bottomY");
m_uTopClip = m_barsProg.uniformLocation("topClip");
m_uHeatmap = m_barsProg.uniformLocation("heatmap");
}
void SpectrogramWidget::buildHeatmapTexture()
{
updateHeatmapTexture(); // use current m_stops
}
void SpectrogramWidget::updateHeatmapTexture()
{
if (m_heatmapTex) {
glDeleteTextures(1, &m_heatmapTex);
m_heatmapTex = 0;
}
if (m_stops.size() < 2)
m_stops = { QColor(0,0,0), QColor(255,255,255) };
const int nSegs = m_stops.size() - 1;
auto lerp = [](int x, int y, float f) {
return std::clamp(int(x + (y - x) * f), 0, 255);
};
for (int i = 0; i < 256; i++) {
float t = float(i) / 255.f * nSegs;
int seg = std::min(int(t), nSegs - 1);
float f = t - seg;
QColor a = m_stops[seg], b = m_stops[seg+1];
m_heatmap[i] = qRgb(lerp(a.red(), b.red(), f),
lerp(a.green(), b.green(), f),
lerp(a.blue(), b.blue(), f));
}
glGenTextures(1, &m_heatmapTex);
glBindTexture(GL_TEXTURE_1D, m_heatmapTex);
std::vector<GLubyte> td(256 * 3);
for (int i = 0; i < 256; i++) {
td[3*i+0] = quint8(qRed (m_heatmap[i]));
td[3*i+1] = quint8(qGreen(m_heatmap[i]));
td[3*i+2] = quint8(qBlue (m_heatmap[i]));
}
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB, 256, 0,
GL_RGB, GL_UNSIGNED_BYTE, td.data());
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
}
void SpectrogramWidget::setStops(const QVector<QColor> &stops)
{
if (stops.size() < 2) return;
m_stops = stops;
makeCurrent();
updateHeatmapTexture();
doneCurrent();
update();
saveBarColors();
}
void SpectrogramWidget::loadBarColors()
{
QString configDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.subwave";
QDir().mkpath(configDir);
QString path = configDir + "/barcolors.json";
QFile f(path);
if (!f.open(QIODevice::ReadOnly))
return;
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
f.close();
if (!doc.isArray())
return;
QJsonArray arr = doc.array();
QVector<QColor> loaded;
for (const auto &v : arr) {
QJsonObject obj = v.toObject();
int r = obj["r"].toInt(0);
int g = obj["g"].toInt(0);
int b = obj["b"].toInt(0);
loaded.append(QColor(r, g, b));
}
if (loaded.size() >= 2)
m_stops = loaded;
}
void SpectrogramWidget::saveBarColors()
{
QString configDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/.subwave";
QDir().mkpath(configDir);
QString path = configDir + "/barcolors.json";
QFile f(path);
if (!f.open(QIODevice::WriteOnly))
return;
QJsonArray arr;
for (const QColor &c : m_stops) {
QJsonObject obj;
obj["r"] = c.red();
obj["g"] = c.green();
obj["b"] = c.blue();
arr.append(obj);
}
QJsonDocument doc(arr);
f.write(doc.toJson());
f.close();
}
void SpectrogramWidget::mousePressEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton) {
GradientDialog dlg(m_stops, this);
if (dlg.exec() == QDialog::Accepted) {
setStops(dlg.stops());
}
}
QOpenGLWidget::mousePressEvent(e);
}
void SpectrogramWidget::computeBars()
{
const int fftLen = m_currentMag.size();
const double logMin = std::log10(20.0), logMax = std::log10(22050.0);
QVector<float> bars(BAR_COUNT, 0.f);
for (int b = 0; b < BAR_COUNT; b++) {
double f1 = std::pow(10.0, logMin + (logMax-logMin)* b /BAR_COUNT);
double f2 = std::pow(10.0, logMin + (logMax-logMin)*(b+1)/BAR_COUNT);
int bin1 = std::clamp(int(f1/22050.0*fftLen), 0, fftLen-1);
int bin2 = std::clamp(int(f2/22050.0*fftLen), bin1, fftLen-1);
float sum = 0.f; int cnt = 0;
for (int k = bin1; k <= bin2; k++) { sum += m_currentMag[k]; cnt++; }
bars[b] = cnt > 0 ? sum/cnt : 0.f;
}
for (int b = 0; b < BAR_COUNT; b++) {
float db = bars[b] > 0 ? 20.f*std::log10(bars[b]) : DB_MIN;
bars[b] = std::clamp((db - DB_MIN) / (DB_MAX - DB_MIN), 0.f, 1.f);
}
for (int b = 0; b < BAR_COUNT; b++) {
m_barSmoothed[b] = m_barSmoothed[b]*0.65f + bars[b]*0.35f;
if (m_barSmoothed[b] >= m_barPeak[b]) {
m_barPeak[b] = m_barSmoothed[b];
m_peakTimer[b] = 25;
} else if (m_peakTimer[b] > 0) {
m_peakTimer[b]--;
} else {
m_barPeak[b] = std::max(0.f, m_barPeak[b] - 0.006f);
}
m_barHeights [b] = m_barSmoothed[b];
m_barPeaksVec[b] = m_barPeak[b];
}
}
void SpectrogramWidget::paintGL()
{
{
QMutexLocker lk(&m_queueMtx);
while (!m_fftQueue.isEmpty())
m_currentMag = m_fftQueue.dequeue();
}
computeBars();
QColor bg = palette().color(QPalette::Window);
glClearColor(bg.redF(), bg.greenF(), bg.blueF(), 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
if (m_barsProg.isLinked()) {
const float barWidth = 2.0f / BAR_COUNT;
const float gap = barWidth * 0.2f;
const float drawWidth = barWidth - gap;
m_barsProg.bind();
glUniform1fv(m_uBarHeights, BAR_COUNT, m_barHeights .constData());
glUniform1fv(m_uBarPeaks, BAR_COUNT, m_barPeaksVec.constData());
glUniform1i (m_uBarCount, BAR_COUNT);
glUniform1f (m_uBarWidth, drawWidth);
glUniform1f (m_uGap, gap);
glUniform1f (m_uBottomY, -1.0f);
glUniform1f (m_uTopClip, 1.0f);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_1D, m_heatmapTex);
glUniform1i(m_uHeatmap, 0);
glBindVertexArray(m_vaoBars);
glDrawArraysInstanced(GL_TRIANGLES, 0, 6, BAR_COUNT);
glBindVertexArray(0);
m_barsProg.release();
}
}
+68
View File
@@ -0,0 +1,68 @@
#pragma once
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QMutex>
#include <QQueue>
#include <array>
#include <QColor>
#include <QVector>
class SpectrogramWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core
{
Q_OBJECT
static constexpr int BAR_COUNT = 96;
static constexpr float DB_MIN = -80.f, DB_MAX = 0.f;
public:
explicit SpectrogramWidget(QWidget *parent = nullptr);
~SpectrogramWidget();
public slots:
void receiveFft(const QVector<float> &mag);
void reset();
protected:
void initializeGL() override;
void paintGL() override;
void mousePressEvent(QMouseEvent *e) override;
private:
void initShaders();
void buildHeatmapTexture();
void updateHeatmapTexture();
void setStops(const QVector<QColor> &stops);
void loadBarColors();
void saveBarColors();
void computeBars();
QMutex m_queueMtx;
QQueue<QVector<float>> m_fftQueue;
QVector<float> m_currentMag = QVector<float>(2048, 0.f);
QVector<float> m_barSmoothed = QVector<float>(BAR_COUNT, 0.f);
QVector<float> m_barPeak = QVector<float>(BAR_COUNT, 0.f);
QVector<int> m_peakTimer = QVector<int> (BAR_COUNT, 0);
QVector<float> m_barHeights;
QVector<float> m_barPeaksVec;
QVector<QColor> m_stops = { QColor("#078D70"),
QColor("#26CEAA"),
QColor("#98E8C1"),
QColor("#FFFFFF") };
std::array<QRgb, 256> m_heatmap{};
QOpenGLShaderProgram m_barsProg;
GLuint m_heatmapTex = 0;
GLuint m_vaoBars = 0;
int m_uBarHeights = -1;
int m_uBarPeaks = -1;
int m_uBarCount = -1;
int m_uBarWidth = -1;
int m_uGap = -1;
int m_uBottomY = -1;
int m_uTopClip = -1;
int m_uHeatmap = -1;
};
+56
View File
@@ -0,0 +1,56 @@
#include "TrackDelegate.h"
#include <QPainter>
TrackDelegate::TrackDelegate(QObject *parent)
: QStyledItemDelegate(parent) {}
QSize TrackDelegate::sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const {
return {0, 42}; // fixed row height
}
void TrackDelegate::paint(QPainter *p, const QStyleOptionViewItem &opt,
const QModelIndex &idx) const
{
p->save();
bool selected = opt.state & QStyle::State_Selected;
bool current = idx.data(Qt::UserRole + 1).toBool(); // is-playing flag
QColor bg = selected
? opt.palette.highlight().color()
: (idx.row() % 2 == 0
? opt.palette.base().color()
: opt.palette.alternateBase().color());
p->fillRect(opt.rect, bg);
if (current) {
QColor accent = opt.palette.highlight().color();
p->fillRect(opt.rect.left(), opt.rect.top(), 3, opt.rect.height(), accent);
}
QColor fg = selected ? opt.palette.highlightedText().color()
: opt.palette.text().color();
QColor dim = selected ? fg : fg.lighter(160);
int pad = current ? 10 : 7;
QRect r = opt.rect.adjusted(pad, 2, -6, -2);
int mid = r.top() + r.height() / 2;
QFont fTop = opt.font;
fTop.setBold(true);
fTop.setPointSizeF(10);
p->setFont(fTop);
p->setPen(fg);
QString title = idx.data(Qt::DisplayRole).toString();
p->drawText(r.left(), r.top(), r.width(), mid - r.top(),
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, title);
QFont fBot = opt.font;
fBot.setPointSizeF(8.5);
p->setFont(fBot);
p->setPen(dim);
QString sub = idx.data(Qt::UserRole).toString();
p->drawText(r.left(), mid, r.width(), r.bottom() - mid,
Qt::AlignLeft | Qt::AlignVCenter | Qt::TextSingleLine, sub);
p->restore();
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <QStyledItemDelegate>
class TrackDelegate : public QStyledItemDelegate {
public:
explicit TrackDelegate(QObject *parent = nullptr);
QSize sizeHint(const QStyleOptionViewItem &, const QModelIndex &) const override;
void paint(QPainter *p, const QStyleOptionViewItem &opt,
const QModelIndex &idx) const override;
};