aluf/src/CommonWidgets.cpp

665 lines
20 KiB
C++

// src/CommonWidgets.cpp
#include "CommonWidgets.h"
#include "Theme.h"
#include <QBoxLayout>
#include <QFileInfo>
#include <QHBoxLayout>
#include <QListWidget>
#include <QMouseEvent>
#include <QPainter>
#include <QPropertyAnimation>
#include <QPushButton>
#include <QResizeEvent>
#include <QScrollArea>
#include <QShowEvent>
#include <QVBoxLayout>
#include <algorithm>
#include <cmath>
// --- TouchSlider ---
TouchSlider::TouchSlider(QWidget *parent) : QWidget(parent) {
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
setFixedHeight(Theme::Dims::SliderHeight);
}
void TouchSlider::setRange(int min, int max) {
m_min = min;
m_max = max;
m_value = std::clamp(m_value, m_min, m_max);
update();
}
void TouchSlider::setValue(int val) {
val = std::clamp(val, m_min, m_max);
if (val == m_value) return;
m_value = val;
update();
emit valueChanged(m_value);
}
QSize TouchSlider::sizeHint() const {
return QSize(200, Theme::Dims::SliderHeight);
}
int TouchSlider::trackLeft() const { return Theme::Dims::TrackPad; }
int TouchSlider::trackRight() const { return width() - Theme::Dims::TrackPad; }
int TouchSlider::handleX() const {
if (m_max == m_min) return trackLeft();
float frac = float(m_value - m_min) / float(m_max - m_min);
return trackLeft() + frac * (trackRight() - trackLeft());
}
void TouchSlider::paintEvent(QPaintEvent *) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
int cy = height() / 2;
int tl = trackLeft();
int tr = trackRight();
int hx = handleX();
int hh = Theme::Dims::TrackH / 2;
p.setPen(Qt::NoPen);
p.setBrush(Theme::Colors::TrackBg);
p.drawRoundedRect(QRectF(tl, cy - hh, tr - tl, Theme::Dims::TrackH), hh, hh);
p.setBrush(Theme::Colors::Accent);
if (hx > tl)
p.drawRoundedRect(QRectF(tl, cy - hh, hx - tl, Theme::Dims::TrackH), hh, hh);
p.setBrush(Qt::white);
p.drawEllipse(QPointF(hx, cy), Theme::Dims::HandleRadius,
Theme::Dims::HandleRadius);
}
void TouchSlider::mousePressEvent(QMouseEvent *event) {
int hx = handleX();
int dx = event->pos().x() - hx;
int dy = event->pos().y() - height() / 2;
int hr = Theme::Dims::HitRadius;
if (dx * dx + dy * dy <= hr * hr) {
m_dragging = true;
emit sliderPressed();
}
}
void TouchSlider::mouseMoveEvent(QMouseEvent *event) {
if (!m_dragging) return;
int tl = trackLeft();
int tr = trackRight();
float frac = float(event->pos().x() - tl) / float(tr - tl);
frac = std::clamp(frac, 0.0f, 1.0f);
int newVal = m_min + frac * (m_max - m_min);
if (newVal != m_value) {
m_value = newVal;
update();
emit valueChanged(m_value);
}
}
void TouchSlider::mouseReleaseEvent(QMouseEvent *) {
if (m_dragging) {
m_dragging = false;
emit sliderReleased();
}
}
// --- ToggleSwitch ---
ToggleSwitch::ToggleSwitch(const QString &label, QWidget *parent)
: QWidget(parent), m_label(label) {
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
setFixedHeight(Theme::Dims::ToggleRow);
setCursor(Qt::PointingHandCursor);
}
void ToggleSwitch::setChecked(bool checked) {
if (m_checked == checked) return;
m_checked = checked;
if (m_anim) m_anim->stop();
m_anim = new QPropertyAnimation(this, "knobX", this);
connect(m_anim, &QObject::destroyed, this, [this]() { m_anim = nullptr; });
m_anim->setDuration(150);
m_anim->setStartValue(m_knobX);
m_anim->setEndValue(checked ? 1.0f : 0.0f);
m_anim->start(QAbstractAnimation::DeleteWhenStopped);
emit toggled(checked);
}
void ToggleSwitch::setKnobX(float x) {
m_knobX = x;
update();
}
QSize ToggleSwitch::sizeHint() const {
QFontMetrics fm(font());
int textW = m_label.isEmpty() ? 0 : fm.horizontalAdvance(m_label) + 8;
return QSize(Theme::Dims::ToggleW + textW, Theme::Dims::ToggleRow);
}
void ToggleSwitch::paintEvent(QPaintEvent *) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
constexpr int tw = Theme::Dims::ToggleW;
constexpr int th = Theme::Dims::ToggleH;
constexpr int kd = Theme::Dims::ToggleKnob;
constexpr int pad = 2;
int ty = (height() - th) / 2;
auto off = Theme::Colors::TrackOff;
auto on = Theme::Colors::Accent;
QColor trackColor = QColor::fromRgbF(
off.redF() + (on.redF() - off.redF()) * m_knobX,
off.greenF() + (on.greenF() - off.greenF()) * m_knobX,
off.blueF() + (on.blueF() - off.blueF()) * m_knobX);
p.setPen(Qt::NoPen);
p.setBrush(trackColor);
p.drawRoundedRect(QRectF(0, ty, tw, th), th / 2.0, th / 2.0);
float knobLeft = pad + m_knobX * (tw - kd - 2 * pad);
float knobCX = knobLeft + kd / 2.0;
float knobCY = ty + th / 2.0;
p.setBrush(Qt::white);
p.drawEllipse(QPointF(knobCX, knobCY), kd / 2.0, kd / 2.0);
if (!m_label.isEmpty()) {
p.setPen(Qt::white);
QFont f = font();
f.setPointSize(13);
p.setFont(f);
p.drawText(QRectF(tw + 8, 0, width() - tw - 8, height()),
Qt::AlignLeft | Qt::AlignVCenter, m_label);
}
}
void ToggleSwitch::mousePressEvent(QMouseEvent *) { setChecked(!m_checked); }
// --- PlaylistDelegate ---
void PlaylistDelegate::paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const {
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
if (option.state & QStyle::State_Selected)
painter->fillRect(option.rect, Theme::Colors::ListSelected);
else
painter->fillRect(option.rect, Theme::Colors::SurfaceDark);
QRect r = option.rect.adjusted(Theme::Dims::ListPad, Theme::Dims::ListPad,
-Theme::Dims::ListPad, -Theme::Dims::ListPad);
constexpr int iconSize = Theme::Dims::ListIcon;
QPixmap art = index.data(Qt::DecorationRole).value<QPixmap>();
QRect iconRect(r.left(), r.top(), iconSize, iconSize);
if (!art.isNull()) {
int x = iconRect.x() + (iconRect.width() - art.width()) / 2;
int y = iconRect.y() + (iconRect.height() - art.height()) / 2;
painter->drawPixmap(x, y, art);
} else {
painter->fillRect(iconRect, Theme::Colors::AlbumArtPlaceholder);
}
QRect textRect = r.adjusted(iconSize + 10, 0, 0, 0);
QString title = index.data(Qt::DisplayRole).toString();
QString artist = index.data(Qt::UserRole + 1).toString();
painter->setPen(Theme::Colors::TextPrimary);
QFont f = option.font;
f.setBold(true);
f.setPointSize(Theme::Dims::ListTitlePt);
painter->setFont(f);
QFontMetrics fmTitle(f);
QRect titleRect = textRect;
titleRect.setHeight(fmTitle.height());
QString elidedTitle =
fmTitle.elidedText(title, Qt::ElideRight, titleRect.width());
painter->drawText(titleRect, Qt::AlignLeft | Qt::AlignTop, elidedTitle);
painter->setPen(Theme::Colors::TextSecondary);
f.setBold(false);
f.setPointSize(Theme::Dims::ListArtistPt);
painter->setFont(f);
QFontMetrics fmArtist(f);
QRect artistRect = textRect;
artistRect.setTop(titleRect.bottom() + 2);
artistRect.setHeight(fmArtist.height());
QString elidedArtist =
fmArtist.elidedText(artist, Qt::ElideRight, artistRect.width());
painter->drawText(artistRect, Qt::AlignLeft | Qt::AlignTop, elidedArtist);
painter->setPen(Theme::Colors::ListSeparator);
painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
painter->restore();
}
QSize PlaylistDelegate::sizeHint(const QStyleOptionViewItem &,
const QModelIndex &) const {
return QSize(0, Theme::Dims::ListRow);
}
// --- XYPad ---
XYPad::XYPad(const QString &title, QWidget *parent)
: QWidget(parent), m_title(title) {
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
setMinimumHeight(Theme::Dims::XYMinH);
setCursor(Qt::CrossCursor);
}
void XYPad::setFormatter(std::function<QString(float, float)> formatter) {
m_formatter = formatter;
}
void XYPad::setValues(float x, float y) {
m_x = std::clamp(x, 0.0f, 1.0f);
m_y = std::clamp(y, 0.0f, 1.0f);
update();
}
void XYPad::paintEvent(QPaintEvent *) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.fillRect(rect(), Theme::Colors::XYBg);
p.setPen(QPen(Theme::Colors::XYBorder, 1));
p.drawRect(rect().adjusted(0, 0, -1, -1));
p.setPen(QPen(Theme::Colors::XYGrid, 1, Qt::DotLine));
p.drawLine(width() / 2, 0, width() / 2, height());
p.drawLine(0, height() / 2, width(), height() / 2);
int px = m_x * width();
int py = (1.0f - m_y) * height();
p.setPen(Qt::NoPen);
p.setBrush(Theme::Colors::XYAccentDim);
p.drawEllipse(QPoint(px, py), Theme::Dims::XYIndicator,
Theme::Dims::XYIndicator);
p.setBrush(Qt::white);
p.drawEllipse(QPoint(px, py), Theme::Dims::XYInner, Theme::Dims::XYInner);
p.setPen(QPen(Theme::Colors::XYAccentFaint, 1));
p.drawLine(px, 0, px, height());
p.drawLine(0, py, width(), py);
p.setPen(Qt::white);
QFont f = font();
f.setBold(true);
f.setPointSize(10);
p.setFont(f);
QString text = m_title;
if (m_formatter)
text += "\n" + m_formatter(m_x, m_y);
p.drawText(rect().adjusted(10, 10, -10, -10), Qt::AlignLeft | Qt::AlignTop,
text);
}
void XYPad::mousePressEvent(QMouseEvent *event) {
int px = m_x * width();
int py = (1.0f - m_y) * height();
QPoint diff = event->pos() - QPoint(px, py);
int hr = Theme::Dims::HitRadius;
if (diff.x() * diff.x() + diff.y() * diff.y() <= hr * hr) {
m_dragging = true;
m_lastPos = event->pos();
}
}
void XYPad::mouseMoveEvent(QMouseEvent *event) {
if (!m_dragging) return;
QPoint delta = event->pos() - m_lastPos;
m_lastPos = event->pos();
m_x = std::clamp(m_x + delta.x() * 0.7f / width(), 0.0f, 1.0f);
m_y = std::clamp(m_y - delta.y() * 0.7f / height(), 0.0f, 1.0f);
update();
emit valuesChanged(m_x, m_y);
}
void XYPad::mouseReleaseEvent(QMouseEvent *) { m_dragging = false; }
// --- ExpandedXYPad ---
ExpandedXYPad::ExpandedXYPad(QWidget *parent) : QWidget(parent) {
hide();
}
void ExpandedXYPad::open(const QString &title,
std::function<QString(float, float)> formatter,
float x, float y) {
m_title = title;
m_formatter = formatter;
m_x = x;
m_y = y;
m_dragging = false;
raise();
show();
update();
}
QRect ExpandedXYPad::padRect() const {
int side = std::min(width(), height()) * 85 / 100;
int cx = (width() - side) / 2;
int cy = (height() - side) / 2;
return QRect(cx, cy, side, side);
}
void ExpandedXYPad::paintEvent(QPaintEvent *) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.fillRect(rect(), m_dragging ? Theme::Colors::ExpandedXYBgDragging
: Theme::Colors::ExpandedXYBg);
QRect pr = padRect();
p.fillRect(pr, Theme::Colors::XYBg);
p.setPen(QPen(Theme::Colors::XYBorder, 1));
p.drawRect(pr.adjusted(0, 0, -1, -1));
p.setPen(QPen(Theme::Colors::XYGrid, 1, Qt::DotLine));
p.drawLine(pr.center().x(), pr.top(), pr.center().x(), pr.bottom());
p.drawLine(pr.left(), pr.center().y(), pr.right(), pr.center().y());
int px = pr.left() + m_x * pr.width();
int py = pr.top() + (1.0f - m_y) * pr.height();
p.setPen(Qt::NoPen);
p.setBrush(Theme::Colors::XYAccentDim);
p.drawEllipse(QPoint(px, py), Theme::Dims::XYIndicator,
Theme::Dims::XYIndicator);
p.setBrush(Qt::white);
p.drawEllipse(QPoint(px, py), Theme::Dims::XYInner, Theme::Dims::XYInner);
p.setPen(QPen(Theme::Colors::XYAccentFaint, 1));
p.drawLine(px, pr.top(), px, pr.bottom());
p.drawLine(pr.left(), py, pr.right(), py);
p.setPen(Qt::white);
QFont f = font();
f.setBold(true);
f.setPointSize(12);
p.setFont(f);
QString text = m_title;
if (m_formatter)
text += "\n" + m_formatter(m_x, m_y);
p.drawText(pr.adjusted(10, 10, -10, -10), Qt::AlignLeft | Qt::AlignTop, text);
}
void ExpandedXYPad::mousePressEvent(QMouseEvent *event) {
QRect pr = padRect();
int hr = Theme::Dims::HitRadius;
int px = pr.left() + m_x * pr.width();
int py = pr.top() + (1.0f - m_y) * pr.height();
QPoint diff = event->pos() - QPoint(px, py);
if (diff.x() * diff.x() + diff.y() * diff.y() <= hr * hr) {
m_dragging = true;
m_lastPos = event->pos();
update();
return;
}
QRect cushion = pr.adjusted(-hr, -hr, hr, hr);
if (!cushion.contains(event->pos())) {
hide();
emit closed();
}
}
void ExpandedXYPad::mouseMoveEvent(QMouseEvent *event) {
if (!m_dragging) return;
QRect pr = padRect();
QPoint delta = event->pos() - m_lastPos;
m_lastPos = event->pos();
m_x = std::clamp(m_x + delta.x() * 0.7f / pr.width(), 0.0f, 1.0f);
m_y = std::clamp(m_y - delta.y() * 0.7f / pr.height(), 0.0f, 1.0f);
update();
emit valuesChanged(m_x, m_y);
}
void ExpandedXYPad::mouseReleaseEvent(QMouseEvent *) {
m_dragging = false;
update();
}
// --- OverlayWidget ---
OverlayWidget::OverlayWidget(QWidget *content, QWidget *parent)
: QWidget(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
layout->setContentsMargins(20, 20, 20, 20);
QScrollArea *scroll = new QScrollArea(this);
scroll->setWidget(content);
scroll->setWidgetResizable(true);
scroll->setFrameShape(QFrame::NoFrame);
scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scroll->setMaximumWidth(500);
scroll->setStyleSheet(
"QScrollArea { background: transparent; border: none; }"
"QScrollArea > QWidget > QWidget { background: transparent; }");
scroll->viewport()->setStyleSheet("background: transparent;");
m_content = scroll;
layout->addWidget(scroll);
hide();
}
void OverlayWidget::showEvent(QShowEvent *event) {
QWidget::showEvent(event);
if constexpr (Theme::FrostedGlass) {
if (auto *w = window()) {
QPixmap shot = w->grab();
int sw = shot.width() / 10;
int sh = shot.height() / 10;
m_blurredBg = shot.scaled(sw, sh, Qt::IgnoreAspectRatio,
Qt::SmoothTransformation)
.scaled(shot.size(), Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
}
}
void OverlayWidget::mousePressEvent(QMouseEvent *event) {
if (!m_content->geometry().contains(event->pos()))
hide();
}
void OverlayWidget::paintEvent(QPaintEvent *) {
QPainter p(this);
if constexpr (Theme::FrostedGlass) {
if (!m_blurredBg.isNull())
p.drawPixmap(0, 0, m_blurredBg);
}
p.fillRect(rect(), Theme::Colors::OverlayDim);
}
// --- WelcomeWidget ---
WelcomeWidget::WelcomeWidget(QWidget *parent) : QWidget(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignCenter);
layout->setSpacing(20);
layout->setContentsMargins(40, 40, 40, 40);
QLabel *title = new QLabel("Yr Crystals", this);
title->setStyleSheet("color: white; font-size: 32px; font-weight: bold;");
title->setAlignment(Qt::AlignCenter);
layout->addWidget(title);
QString minSizeRule = Theme::Dims::BtnMinSize > 0
? QString(" min-height: %1px;").arg(Theme::Dims::BtnMinSize)
: QString();
QString btnStyle = QString(
"QPushButton { background-color: %1; color: %2; border: 1px solid "
"%3; border-radius: 8px; padding: 15px; font-size: 18px;%4 } "
"QPushButton:pressed { background-color: %5; }")
.arg(Theme::hex(Theme::Colors::SurfaceLight),
Theme::hex(Theme::Colors::TextPrimary),
Theme::hex(Theme::Colors::BorderMid),
minSizeRule,
Theme::hex(Theme::Colors::BorderMid));
QString listStyle = QString(
"QListWidget { background: transparent; border: none; color: %1; "
"font-size: 16px; }"
"QListWidget::item { padding: %2px; border-bottom: 1px solid %3; }"
"QListWidget::item:hover { background: %4; }"
"QListWidget::item:selected { background: %5; }")
.arg(Theme::hex(Theme::Colors::TextMuted))
.arg(Theme::Dims::ListItemPad)
.arg(Theme::hex(Theme::Colors::SurfaceLight),
Theme::hex(Theme::Colors::SurfaceMid),
Theme::hex(Theme::Colors::SurfaceLight));
QHBoxLayout *btnLayout = new QHBoxLayout();
btnLayout->setSpacing(20);
QPushButton *btnFile = new QPushButton("Open File", this);
btnFile->setStyleSheet(btnStyle);
btnFile->setCursor(Qt::PointingHandCursor);
connect(btnFile, &QPushButton::clicked, this,
&WelcomeWidget::openFileClicked);
btnLayout->addWidget(btnFile);
QPushButton *btnFolder = new QPushButton("Open Folder", this);
btnFolder->setStyleSheet(btnStyle);
btnFolder->setCursor(Qt::PointingHandCursor);
connect(btnFolder, &QPushButton::clicked, this,
&WelcomeWidget::openFolderClicked);
btnLayout->addWidget(btnFolder);
layout->addLayout(btnLayout);
QString labelStyle = QString("color: %1; font-size: 16px; margin-top: 20px;")
.arg(Theme::hex(Theme::Colors::TextSecondary));
m_listsContainer = new QWidget(this);
QWidget *recentCol = new QWidget(m_listsContainer);
QVBoxLayout *recentLayout = new QVBoxLayout(recentCol);
recentLayout->setContentsMargins(0, 0, 0, 0);
QLabel *recentLabel = new QLabel("Recent", recentCol);
recentLabel->setStyleSheet(labelStyle);
recentLayout->addWidget(recentLabel);
m_recentList = new QListWidget(recentCol);
m_recentList->setStyleSheet(listStyle);
m_recentList->setFocusPolicy(Qt::NoFocus);
m_recentList->setCursor(Qt::PointingHandCursor);
m_recentList->setSelectionMode(QAbstractItemView::SingleSelection);
connect(m_recentList, &QListWidget::itemClicked, this,
&WelcomeWidget::onRecentClicked);
recentLayout->addWidget(m_recentList);
QWidget *freqCol = new QWidget(m_listsContainer);
QVBoxLayout *freqLayout = new QVBoxLayout(freqCol);
freqLayout->setContentsMargins(0, 0, 0, 0);
QLabel *freqLabel = new QLabel("Frequents", freqCol);
freqLabel->setStyleSheet(labelStyle);
freqLayout->addWidget(freqLabel);
m_frequentList = new QListWidget(freqCol);
m_frequentList->setStyleSheet(listStyle);
m_frequentList->setFocusPolicy(Qt::NoFocus);
m_frequentList->setCursor(Qt::PointingHandCursor);
m_frequentList->setSelectionMode(QAbstractItemView::SingleSelection);
connect(m_frequentList, &QListWidget::itemClicked, this,
&WelcomeWidget::onRecentClicked);
freqLayout->addWidget(m_frequentList);
m_listsLayout = new QVBoxLayout(m_listsContainer);
m_listsLayout->setContentsMargins(0, 0, 0, 0);
m_listsLayout->addWidget(recentCol);
m_listsLayout->addWidget(freqCol);
m_isHorizontal = false;
layout->addWidget(m_listsContainer, 1);
refreshRecents();
}
void WelcomeWidget::refreshRecents() {
m_recentList->clear();
QStringList files = Utils::getRecentFiles();
QStringList folders = Utils::getRecentFolders();
for (const auto &path : folders) {
QListWidgetItem *item =
new QListWidgetItem("📁 " + QFileInfo(path).fileName());
item->setData(Qt::UserRole, path);
item->setToolTip(path);
m_recentList->addItem(item);
}
for (const auto &path : files) {
QListWidgetItem *item =
new QListWidgetItem("🎵 " + QFileInfo(path).fileName());
item->setData(Qt::UserRole, path);
item->setToolTip(path);
m_recentList->addItem(item);
}
m_frequentList->clear();
auto freqs = Utils::getFrequentPaths(10);
for (const auto &pair : freqs) {
QString name = QFileInfo(pair.first).fileName();
QListWidgetItem *item =
new QListWidgetItem(name + " (" + QString::number(pair.second) + ")");
item->setData(Qt::UserRole, pair.first);
item->setToolTip(pair.first);
m_frequentList->addItem(item);
}
}
void WelcomeWidget::resizeEvent(QResizeEvent *event) {
QWidget::resizeEvent(event);
updateListsLayout();
}
void WelcomeWidget::updateListsLayout() {
bool wantHorizontal = (width() > height());
if (wantHorizontal == m_isHorizontal)
return;
m_isHorizontal = wantHorizontal;
QList<QWidget *> children;
while (m_listsLayout->count() > 0) {
QLayoutItem *item = m_listsLayout->takeAt(0);
if (item->widget())
children.append(item->widget());
delete item;
}
delete m_listsLayout;
if (m_isHorizontal)
m_listsLayout = new QHBoxLayout(m_listsContainer);
else
m_listsLayout = new QVBoxLayout(m_listsContainer);
m_listsLayout->setContentsMargins(0, 0, 0, 0);
for (auto *w : children)
m_listsLayout->addWidget(w);
}
void WelcomeWidget::onRecentClicked(QListWidgetItem *item) {
if (item) {
emit pathSelected(item->data(Qt::UserRole).toString());
}
}