Files
archived-Reclass/src/startpage.h

361 lines
13 KiB
C++

#pragma once
#include "themes/thememanager.h"
#include <QDialog>
#include <QLineEdit>
#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QFileInfo>
#include <QDir>
#include <QSettings>
#include <QCoreApplication>
#include <QPainterPath>
namespace rcx {
// Single-widget start page: everything painted in paintEvent.
// Zero CSS, zero Fusion conflicts, zero child-widget styling issues.
class StartPageWidget : public QDialog {
Q_OBJECT
public:
explicit StartPageWidget(QWidget* parent = nullptr) : QDialog(parent) {
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
setMouseTracking(true);
setAttribute(Qt::WA_OpaquePaintEvent);
m_search = new QLineEdit(this);
m_search->setPlaceholderText("Search recent...");
m_search->setFixedHeight(30);
m_search->setMaximumWidth(330);
m_search->addAction(QIcon(":/vsicons/search.svg"), QLineEdit::TrailingPosition);
connect(m_search, &QLineEdit::textChanged, this, [this]{ buildGroups(); update(); });
loadEntries();
buildGroups();
applyTheme(ThemeManager::instance().current());
}
void applyTheme(const Theme& t) {
m_t = t;
m_search->setStyleSheet(
"QLineEdit { background: " + t.background.name() + "; color: " + t.text.name()
+ "; border: 1px solid " + t.border.name()
+ "; padding: 2px 8px; font-size: 13px; }"
"QLineEdit:focus { border: 1px solid " + t.borderFocused.name() + "; }");
update();
}
signals:
void openProject();
void newClass();
void importSource();
void importXml();
void importPdb();
void continueClicked();
void fileSelected(const QString& path);
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
const int LX = 48, TM = 36, RM = 32, GAP = 40, RW = 340;
const int rpX = width() - RW - RM;
const int lW = qMax(100, rpX - GAP - LX);
p.fillRect(rect(), m_t.background);
// ── Title ──
int y = TM;
QFont titleF = font(); titleF.setPixelSize(30); titleF.setWeight(QFont::Light);
p.setFont(titleF); p.setPen(m_t.text);
QFontMetrics titleFm(titleF);
p.drawText(LX, y + titleFm.ascent(), "Reclass");
y += titleFm.height() + 24;
// ── Headings (left + right at same y) ──
QFont headF = font(); headF.setPixelSize(20); headF.setWeight(QFont::DemiBold);
p.setFont(headF); QFontMetrics headFm(headF);
p.drawText(LX, y + headFm.ascent(), "Open recent");
int ry = y;
p.drawText(rpX, ry + headFm.ascent(), "Get started");
ry += headFm.height() + 14;
y += headFm.height() + 14;
// ── Search bar (only child widget) ──
m_search->setGeometry(LX, y, qMin(330, lW), 30);
y += 46;
m_listTop = y;
// ── Right panel ──
drawCards(p, rpX, ry, RW);
// ── File list ──
drawFileList(p, LX, lW);
// ── Border ──
p.setPen(QPen(m_t.border, 1));
p.setBrush(Qt::NoBrush);
p.drawRect(rect().adjusted(0, 0, -1, -1));
}
void mouseMoveEvent(QMouseEvent* e) override {
auto [z, i] = hitTest(e->pos());
if (z != m_hz || i != m_hi) {
m_hz = z; m_hi = i;
setCursor(z != HZ_None ? Qt::PointingHandCursor : Qt::ArrowCursor);
update();
}
}
void mousePressEvent(QMouseEvent* e) override {
if (e->button() != Qt::LeftButton) return;
auto [z, i] = hitTest(e->pos());
if (z == HZ_Entry) emit fileSelected(m_filtered[i].path);
if (z == HZ_Group) { m_groups[i].expanded = !m_groups[i].expanded; update(); }
if (z == HZ_Card && i == 0) emit newClass();
if (z == HZ_Card && i == 1) emit openProject();
if (z == HZ_Card && i == 2) emit importSource();
if (z == HZ_Card && i == 3) emit importXml();
if (z == HZ_Card && i == 4) emit importPdb();
if (z == HZ_Continue) emit continueClicked();
}
void wheelEvent(QWheelEvent* e) override {
m_scrollY = qBound(0, m_scrollY - e->angleDelta().y() / 2, m_maxScroll);
update();
}
void resizeEvent(QResizeEvent* e) override { QWidget::resizeEvent(e); update(); }
void leaveEvent(QEvent*) override { m_hz = HZ_None; m_hi = -1; setCursor(Qt::ArrowCursor); update(); }
void keyPressEvent(QKeyEvent* e) override { if (e->key() == Qt::Key_Escape) reject(); }
private:
enum HZ { HZ_None, HZ_Entry, HZ_Group, HZ_Card, HZ_Continue };
struct Hit { HZ zone; int idx; };
struct Entry {
QString path, fileName, dirPath;
QDateTime lastModified;
bool isExample;
};
struct Group {
QString name;
bool expanded = true;
QVector<int> entries;
};
Theme m_t;
QLineEdit* m_search;
QVector<Entry> m_all, m_filtered;
QVector<Group> m_groups;
int m_scrollY = 0, m_maxScroll = 0, m_listTop = 0, m_contentH = 0;
HZ m_hz = HZ_None;
int m_hi = -1;
// Hit rects populated during paint
QVector<QPair<int, QRectF>> m_grpRects, m_entRects;
QRectF m_cardR[5], m_contR;
void drawIcon(QPainter& p, const QString& path, int x, int y, int sz) {
QIcon(path).paint(&p, x, y, sz, sz);
}
// ── Data loading ──
void loadEntries() {
m_all.clear();
QSettings s("Reclass", "Reclass");
for (const auto& path : s.value("recentFiles").toStringList()) {
QFileInfo fi(path);
if (!fi.exists()) continue;
m_all.append({fi.absoluteFilePath(), fi.fileName(), fi.absolutePath(),
fi.lastModified(), false});
}
#ifdef __APPLE__
QDir exDir(QDir::cleanPath(QCoreApplication::applicationDirPath() + "/../Resources/examples"));
#else
QDir exDir(QCoreApplication::applicationDirPath() + "/examples");
#endif
for (const auto& fn : exDir.entryList({"*.rcx"}, QDir::Files, QDir::Name))
m_all.append({exDir.absoluteFilePath(fn), fn, exDir.absolutePath(),
QFileInfo(exDir.filePath(fn)).lastModified(), true});
}
void buildGroups() {
QString f = m_search->text().trimmed().toLower();
m_filtered.clear();
for (const auto& e : m_all)
if (f.isEmpty() || e.fileName.toLower().contains(f) || e.dirPath.toLower().contains(f))
m_filtered.append(e);
QDate today = QDate::currentDate();
QVector<int> bk[6];
for (int i = 0; i < m_filtered.size(); i++) {
auto& e = m_filtered[i];
if (e.isExample) { bk[5].append(i); continue; }
int d = e.lastModified.date().daysTo(today);
if (d == 0) bk[0].append(i);
else if (d == 1) bk[1].append(i);
else if (d < 7) bk[2].append(i);
else if (e.lastModified.date().month() == today.month()
&& e.lastModified.date().year() == today.year()) bk[3].append(i);
else bk[4].append(i);
}
static const char* names[] = {"Today","Yesterday","This week","This month","Older","Examples"};
m_groups.clear();
for (int i = 0; i < 6; i++)
if (!bk[i].isEmpty()) m_groups.append({names[i], true, bk[i]});
m_scrollY = 0;
}
// ── Drawing ──
void drawCards(QPainter& p, int x, int y, int w) {
struct C { const char* icon; const char* title; const char* desc; };
static const C cards[] = {
{":/vsicons/symbol-structure.svg", "New Class", "Start a new binary class definition"},
{":/vsicons/folder-opened.svg", "Open project", "Open an existing .rcx project"},
{":/vsicons/file-binary.svg", "Import from Source", "Import C/C++ header or source file"},
{":/vsicons/code.svg", "Import ReClass XML", "Import from ReClass .xml format"},
{":/vsicons/debug.svg", "Import PDB", "Import types from a .pdb symbol file"}
};
const int N = 5, CH = 84, R = 6, panelH = N * CH;
// Rounded panel background
QPainterPath clip;
clip.addRoundedRect(QRectF(x, y, w, panelH), R, R);
p.save();
p.setClipPath(clip);
p.fillRect(x, y, w, panelH, m_t.background);
for (int i = 0; i < N; i++) {
int cy = y + i * CH;
QRectF cr(x, cy, w, CH);
m_cardR[i] = cr;
bool hov = (m_hz == HZ_Card && m_hi == i);
if (hov) {
p.fillRect(cr, m_t.hover);
p.fillRect(QRectF(x, cy, 3, CH), m_t.indHoverSpan);
}
// Icon (32px, centered vertically)
int iconSz = 32;
drawIcon(p, cards[i].icon, x + 24, cy + (CH - iconSz) / 2, iconSz);
// Title + description block, centered vertically
int tx = x + 24 + iconSz + 16;
QFont tf = font(); tf.setPixelSize(15);
QFont df = font(); df.setPixelSize(12);
QFontMetrics tfm(tf), dfm(df);
int blockH = tfm.height() + 5 + dfm.height();
int by = cy + (CH - blockH) / 2;
p.setFont(tf); p.setPen(m_t.text);
p.drawText(tx, by + tfm.ascent(), cards[i].title);
p.setFont(df); p.setPen(m_t.textDim);
p.drawText(tx, by + tfm.height() + 5 + dfm.ascent(), cards[i].desc);
}
p.restore();
// "Continue →" centered under the panel
int cy = y + panelH + 8;
QFont lf = font(); lf.setPixelSize(13);
if (m_hz == HZ_Continue) lf.setUnderline(true);
p.setFont(lf); p.setPen(m_t.indHoverSpan);
QFontMetrics lfm(lf);
QString ct = QStringLiteral("Continue \u2192");
int cw = lfm.horizontalAdvance(ct);
m_contR = QRectF(x + (w - cw) / 2, cy, cw, lfm.height());
p.drawText(int(m_contR.x()), cy + lfm.ascent(), ct);
}
void drawFileList(QPainter& p, int x, int w) {
int listH = height() - 24 - m_listTop;
p.save();
p.setClipRect(x, m_listTop, w, listH);
int fy = m_listTop - m_scrollY;
m_grpRects.clear();
m_entRects.clear();
for (int gi = 0; gi < m_groups.size(); gi++) {
auto& g = m_groups[gi];
if (gi > 0) fy += 15;
// Group header
m_grpRects.append({gi, QRectF(x, fy, w, 28)});
p.setPen(Qt::NoPen); p.setBrush(m_t.text);
int triX = x + 8, triY = fy + 11;
QPolygonF tri;
if (g.expanded) tri << QPointF(triX,triY) << QPointF(triX+6,triY) << QPointF(triX+3,triY+6);
else tri << QPointF(triX,triY) << QPointF(triX+6,triY+3) << QPointF(triX,triY+6);
p.drawPolygon(tri);
QFont gf = font(); gf.setPixelSize(13);
p.setFont(gf); p.setPen(m_t.text);
p.drawText(triX + 14, fy + 14 + QFontMetrics(gf).ascent() / 2 - 1, g.name);
fy += 28;
if (!g.expanded) continue;
for (int ei : g.entries) {
auto& e = m_filtered[ei];
QRectF er(x, fy, w, 52);
m_entRects.append({ei, er});
if (m_hz == HZ_Entry && m_hi == ei) p.fillRect(er, m_t.hover);
drawIcon(p, e.isExample ? ":/vsicons/book.svg" : ":/vsicons/symbol-structure.svg",
x + 24, fy + 17, 18);
int tx = x + 52, avail = w - 64;
QFont nf = font(); nf.setPixelSize(14);
p.setFont(nf); p.setPen(m_t.text);
QFontMetrics nm(nf);
int ny = fy + 8;
p.drawText(tx, ny + nm.ascent(),
nm.elidedText(e.fileName, Qt::ElideMiddle, avail * 0.65));
if (!e.isExample) {
p.setPen(m_t.textDim);
QString dt = e.lastModified.toString("M/d/yyyy h:mm AP");
p.drawText(x + w - 12 - nm.horizontalAdvance(dt), ny + nm.ascent(), dt);
}
QFont pf = font(); pf.setPixelSize(12);
p.setFont(pf); p.setPen(m_t.textDim);
QFontMetrics pm(pf);
p.drawText(tx, ny + nm.height() + 4 + pm.ascent(),
pm.elidedText(e.dirPath, Qt::ElideMiddle, avail));
fy += 52;
}
}
m_contentH = fy + m_scrollY - m_listTop;
m_maxScroll = qMax(0, m_contentH - listH);
p.restore();
}
// ── Hit testing ──
Hit hitTest(QPoint pos) const {
for (int i = 0; i < 5; i++)
if (m_cardR[i].contains(pos)) return {HZ_Card, i};
if (m_contR.contains(pos)) return {HZ_Continue, 0};
if (pos.y() >= m_listTop && pos.y() < height() - 24) {
for (const auto& [gi, r] : m_grpRects)
if (r.contains(pos)) return {HZ_Group, gi};
for (const auto& [ei, r] : m_entRects)
if (r.contains(pos)) return {HZ_Entry, ei};
}
return {HZ_None, -1};
}
};
} // namespace rcx