Internal
Public Access
Adds docs/USER_GUIDE.md, a 10-section end-user guide (getting started, managing/organizing profiles, SSH and RDP connections, session management, settings, troubleshooting). It's embedded into the app binary via a Qt resource file and rendered by a new Help -> User Guide window: a topic sidebar plus content pane, not a single scrolling document, with cross-reference links between sections routed to sidebar selection rather than relying on Qt's Markdown importer's lack of heading anchors. A separate, non-shipped tool (tools/user-guide-pdf/) renders the same source to a standalone PDF via QTextDocument + QPrinter, wrapped by packaging/docs/build-user-guide-pdf.sh. Kept fully outside the main CMake target so Qt6::PrintSupport never becomes a runtime dependency of the shipped app (confirmed via ldd). The PDF itself isn't committed -- generated per release like the platform installers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
150 lines
3.8 KiB
C++
150 lines
3.8 KiB
C++
#include "user_guide_dialog.h"
|
|
|
|
#include <QApplication>
|
|
#include <QDesktopServices>
|
|
#include <QDialogButtonBox>
|
|
#include <QFile>
|
|
#include <QListWidget>
|
|
#include <QSplitter>
|
|
#include <QTextBrowser>
|
|
#include <QTextStream>
|
|
#include <QUrl>
|
|
#include <QVBoxLayout>
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|