Help system for small projects
-
Help System for small projects
Lately I have been struggling with the help system. Unfortunately the help system of Qt did not work as expected. Or maybe I wasn't smart enough to use it ...
Anyway - I wanted to extend QHelpEngine, but then had to realize that the most important functions are not virtual - so no chance for customization.
I also didn't like that Qt's help system doesn't support images, so I decided to do it myself. I started with qdoc format, but then I had to hear that qdoc would be an internal tool. I was recommended doxygen. Only I didn't want to document my source code, but create a help system for end users.
For this doxygen is not the right tool. Much too complex and does a lot of things I don't want. Not even sure it can do what I want ...
I wanted a simple tool that doesn't require much time investment besides the actual writing. Ascidoc was under discussion, but the source code doesn't look very readable. Ended up with markdown. Some commands were already known from forums, the rest was quickly explored. The nice thing about markdown is that the source code still remains readable.
So I switched to markdown. Pretty soon I realized that the original didn't quite meet my needs. Fortunately, there is the GitHub extension. With that, I was able to get started.
My idea was that all files that become necessary in the help system should be included in the help file. So I looked for support for archives for Qt. Unfortunately Qt does not offer anything officially. Also, user requests for such support have been ignored for years. This is especially sad when you find out that the code has been available in Qt for years. Just marked as internal :(
I tried it anyway and am thrilled.
my directory structure looks like this:
docs +---src | +---images +---html
In the directory docs is the qhp file, and auxiliary files for generating the help file.
In the directory src are the markdown files. Since my project is not that big, the localized and the original files are located there. For the sake of simplicity, I wrote in my language and only later created the English version. As it should turn out, this was the right way.
While researching markdown I stumbled across a hint to an editor that offers a live preview for markdown without the need to install a webserver or anything alike. The editor is called Atom and is in my opinion indispensable :)
It unfortunately can't do an overwrite mode - which takes quite a bit of getting used to. Nevertheless it was a pure pleasure to write the help texts with it.Why was it right to start writing in my language? As it turned out, neither qdoc nor markdown can handle the German special characters. Declaring the HTML file as utf-8 didn't help either.
So I wrote a filter that handles the HTML output of markdown. Later it turned out that Atom generates a <br> at every automatic line break, which looks very unattractive. So I extended the filter to remove all <br>.
Here is a taste of what the online help looks like now:
Another reason why it was advantageous to start in my language: deepl understands markdown. Therefore I was able to translate all files via copy/paste and had already translated about 70%. The rework was quick and easy thanks to Atom.
Let's get to the source code.
I based the interface on QHelpEngine and analogously my HelpEngine generates widgets for the tree of pages and for the list of keywords.
As a frame I chose QDockable, which shows the two widgets from HelpEngine in a notebook on the left and an extension of the QTextBrowser on the right. Extension because the function loadResource has to be extended to load also links and images from the help file.
I liked the qhp file as used by Qt. It already contains (almost) everything necessary. Since I wanted to support multilingualism, I had to modify the file slightly. Here is my description file:
<?xml version="1.0" encoding="utf-8" ?> <QtHelpProject version="1.0"> <namespace>de.schwarzrot.falconview.0.1</namespace> <virtualFolder>FalconView</virtualFolder> <filterSection> <filterAttribute>FalconView</filterAttribute> <filterAttribute>0.1</filterAttribute> <toc> <section title="User Guide" ref="index"> <section title="linuxCNC" ref="integration"/> <section title="Center-Area" ref="reference"> <section title="NC-Editor" ref="nceditor"/> <section title="3D-Preview" ref="preview"/> <section title="JogView" ref="jogview"/> <section title="Settings" ref="settingsnotebook"> <section title="Fixtures" ref="fixturemanager"/> <section title="Preferences" ref="preferences"/> <section title="LC-Tools" ref="lctooltable"/> </section> </section> <section title="Toolbars" ref="toolbars"> <section title="ApplicationMode" ref="tbappmode"/> <section title="RunMode" ref="tbrunmode"/> <section title="Machine" ref="tbmachine"/> <section title="Extended" ref="tbextend"/> </section> <section title="Status-Info" ref="info"> <section title="Position" ref="sipos"/> <section title="ToolInfo" ref="sitool"/> <section title="act.Codes" ref="sicodes"/> <section title="SpeedInfo" ref="sispeed"/> </section> <section title="use case" ref="usage"/> </section> </toc> <keywords> <keyword name="3D Preview" id="3D Preview" ref="preview"/> <keyword name="FileManager" id="fileManager" ref="filemanager"/> <keyword name="FixtureManager" id="FixtureManager" ref="fixturemanager"/> <keyword name="JogView" id="JogView" ref="jogview"/> <keyword name="LCToolTable" id="LCToolTable" ref="lctooltable"/> <keyword name="MDIEditor" id="MDIEditor" ref="mdiedit"/> <keyword name="PathEditor" id="PathEditor" ref="nceditor"/> <keyword name="PreferencesEditor" id="PreferencesEditor" ref="preferences"/> <keyword name="SettingsNotebook" id="SettingsNotebook" ref="settingsnotebook"/> <keyword name="TestEdit" id="TestEdit" ref="nceditor"/> <keyword name="ToolEditor" id="ToolEditor" ref="tooleditor"/> <keyword name="ToolManager" id="ToolManager" ref="toolmanager"/> </keywords> <files> <file>*.html</file> </files> </filterSection> </QtHelpProject>
Namespace, virtualFolder and filterSection I took over unchanged, even if I don't use any of them. Only the <toc> and <keywords> sections are important to me. As you can see, I removed the file extension from the ref attributes. This way I can support multilingualism without the application having to load/know different help files.
The help usage in application is simple. In MainWindow the help widget is created:
dlgHelp = new HelpDialog(this);
In the form for the MainWindow a menu entry for help is created, so in application code only the help function has to be connected to the menu entry:
ui->actionHelp->setShortcut(QKeySequence::HelpContents); connect(ui->actionHelp, &QAction::triggered, dlgHelp, &HelpDialog::showHelp);
In my application, I want to display the appropriate help at each page change. For this I have captured the objectName of the pages as technical terms and translated them (manual extension of the *.ts file). Furthermore the technical terms are contained in the above *.qhp file as keywords. In the application I call the help then as follows:
void DynCenterWidget::showEvent(QShowEvent* e) { QWidget::showEvent(e); if (e->type() == QEvent::Show) Core().help4Keyword(objectName()); }
As mentioned before, loadResource of QTextBrowser needs to be changed. Unfortunately, it turned out that QTextBrowser does not handle CSS styles that are stored in the meta area of a html-page. After some failed attempts I chose the variant where a DefaultStyleSheet is loaded in the browser. This led to the desired success:
HelpBrowser::HelpBrowser(QWidget* parent) : QTextBrowser(parent) , engine(nullptr) { document()->setDefaultStyleSheet(defaultStyles); } const QString HelpBrowser::defaultStyles("table, th, td {" "border-width: 1px;" "border-color: #CCCCCC;" "border-collapse: collapse;" "}" "img {vertical-align: middle; }" "th, td {" "padding: 15px;" "}");
For the internal help classes I have split the tasks. HelpEngine encapsulates the archive access, while the widget classes process the *.qhp file. The result of the processing is stored in the tree, respectively the list, on the one hand, and on the other hand a directory is created, which is transmitted to HelpEngine, so that it can answer queries for keywords or pages and process them in a localized way.
Declaration: HelpEngine
Definition: HelpEngine
The help files are organized as follows:
- preview.md - english version - preview_de.md - localized version
HelpEngine tries to load the localized version first. If this fails (e.g. because locale es is used instead of de), then the english standard is loaded.
Another problem (for me) was that I tried many different icons and images on the pages, but didn't want to have them all in the help file. So I had to figure out which images were actually used in the help files. All in all a bit too much to remember and type in each time. So I created a little helper script (gfm is the markdown interpreter with github extension, fixhtml is my filter for special characters):
#!/bin/bash project=FalconView extIN=md extOut=html for i in $(ls src/*.$extIN) do t=html${i#src} d=${t%.$extIN}.$extOut cat $i | gfm | fixhtml -e htmlEntities.def > $d done cd src images=`grep -Ro -P "\(images/(.*)\)" | sed 's%.*(images/%%' | sed 's%)%%'` cd .. cp ${project}.qhp html mkdir html/images 2>/dev/null for i in $images do echo $i cp src/images/$i html/images done cd html zip -u9 ../FalconView.qzh *.html images/* FalconView.qhp cp -a ../FalconView.qzh .
For completeness - htmlEntites.def:
ä ä ö ö ü ü Ä Ä Ö Ö Ü Ü ß ß <br>
... and the code of the filter:
#include <QCoreApplication> #include <QTextStream> #include <QFile> #include <QDebug> #include <iostream> static QString findFile(const QStringList& args) { int mx = args.size(); for (int i=0; i < mx; ++i) { if (args.at(i) == "-e") { if ((i+1) < mx) return args.at(i+1); } } return QString(); } static int readEntities(const QString& fileName, QMap<QString, QString>& m) { QFile file(fileName); if (!file.exists()) return 0; if (!file.open(QIODevice::ReadOnly)) return 0; QTextStream in(&file); QString line = in.readLine(); QStringList parts; QString tok; while (!line.isNull()) { parts = line.split(" "); if (parts.size() < 1) continue; if (parts.size() > 1) tok = parts[1]; else tok = QString(); m[parts[0]] = tok; line = in.readLine(); } file.close(); return m.size(); } static void processSource(QTextStream& in, QMap<QString, QString>& m) { QString line = in.readLine(); while (!line.isNull()) { for (auto rp = m.constKeyValueBegin(); rp != m.constKeyValueEnd(); rp++) { line.replace(rp->first, rp->second); qDebug() << "cv: " << line; } std::cout << line.toStdString().c_str() << std::endl; line = in.readLine(); } } int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); QMap<QString, QString> charMap; QString fileName = findFile(QCoreApplication::arguments()); if (readEntities(fileName, charMap)) { QTextStream ts(stdin); processSource(ts, charMap); return 0; } return -1; }