#include "user_guide_dialog.h" #include #include #include #include #include #include #include #include #include #include namespace { QString slugify(const QString& title) { QString slug; slug.reserve(title.size()); bool lastWasHyphen = false; for (const QChar& ch : title) { if (ch.isLetterOrNumber()) { slug += ch.toLower(); lastWasHyphen = false; } else if (!lastWasHyphen && !slug.isEmpty()) { slug += QLatin1Char('-'); lastWasHyphen = true; } } while (slug.endsWith(QLatin1Char('-'))) { slug.chop(1); } return slug; } } UserGuideDialog::UserGuideDialog(QWidget* parent) : QDialog(parent), m_sectionList(nullptr), m_browser(nullptr) { setWindowTitle(QStringLiteral("OrbitHub User Guide")); setWindowIcon(QApplication::windowIcon()); resize(900, 640); auto* layout = new QVBoxLayout(this); layout->setContentsMargins(16, 16, 16, 16); layout->setSpacing(12); auto* splitter = new QSplitter(Qt::Horizontal, this); m_sectionList = new QListWidget(splitter); m_sectionList->setMaximumWidth(220); m_browser = new QTextBrowser(splitter); m_browser->setOpenExternalLinks(false); m_browser->setOpenLinks(false); splitter->addWidget(m_sectionList); splitter->addWidget(m_browser); splitter->setStretchFactor(0, 0); splitter->setStretchFactor(1, 1); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, this); connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept); layout->addWidget(splitter, 1); layout->addWidget(buttons); loadSections(); connect(m_sectionList, &QListWidget::currentRowChanged, this, [this](int row) { showSection(row); }); connect(m_browser, &QTextBrowser::anchorClicked, this, &UserGuideDialog::onAnchorClicked); if (!m_sections.isEmpty()) { m_sectionList->setCurrentRow(0); } } void UserGuideDialog::loadSections() { QFile file(QStringLiteral(":/docs/USER_GUIDE.md")); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return; } QTextStream stream(&file); const QString content = stream.readAll(); const QStringList lines = content.split(QLatin1Char('\n')); QString currentTitle; QStringList currentLines; auto flushSection = [this, ¤tTitle, ¤tLines]() { if (currentTitle.isEmpty()) { return; } Section section; section.title = currentTitle; section.anchor = slugify(currentTitle); section.markdown = currentLines.join(QLatin1Char('\n')); m_sections.append(section); }; for (const QString& line : lines) { if (line.startsWith(QStringLiteral("## "))) { flushSection(); currentTitle = line.mid(3).trimmed(); currentLines.clear(); } currentLines.append(line); } flushSection(); for (const Section& section : m_sections) { m_sectionList->addItem(section.title); } } void UserGuideDialog::showSection(int index) { if (index < 0 || index >= m_sections.size()) { return; } m_browser->setMarkdown(m_sections[index].markdown); } void UserGuideDialog::onAnchorClicked(const QUrl& url) { if (!url.scheme().isEmpty()) { QDesktopServices::openUrl(url); return; } const QString fragment = url.fragment(); if (fragment.isEmpty()) { return; } for (int i = 0; i < m_sections.size(); ++i) { if (m_sections[i].anchor == fragment) { m_sectionList->setCurrentRow(i); return; } } }