// src/CommonWidgets.cpp #include "CommonWidgets.h" #include "Theme.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // --- 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(); 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 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 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 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()); } }