1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Assistant of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28#include "tracer.h"
29
30#include <QtCore/QDir>
31#include <QtCore/QFileInfo>
32#include <QtCore/QLibraryInfo>
33#include <QtCore/QLocale>
34#include <QtCore/QScopedPointer>
35#include <QtCore/QStringList>
36#include <QtCore/QTranslator>
37#include <QtCore/QUrl>
38
39#include <QtWidgets/QApplication>
40#include <QtGui/QDesktopServices>
41
42#include <QtHelp/QHelpEngine>
43#include <QtHelp/QHelpSearchEngine>
44
45#include <QtSql/QSqlDatabase>
46
47#if defined(BROWSER_QTWEBKIT)
48#include <QtGui/QFont>
49#include <QWebSettings>
50#endif
51
52#include "../shared/collectionconfiguration.h"
53#include "helpenginewrapper.h"
54#include "mainwindow.h"
55#include "cmdlineparser.h"
56
57// #define TRACING_REQUESTED
58// #define DEBUG_TRANSLATIONS
59
60QT_USE_NAMESPACE
61
62namespace {
63
64void
65updateLastPagesOnUnregister(QHelpEngineCore& helpEngine, const QString& nsName)
66{
67 TRACE_OBJ
68 int lastPage = CollectionConfiguration::lastTabPage(helpEngine);
69 QStringList currentPages = CollectionConfiguration::lastShownPages(helpEngine);
70 if (!currentPages.isEmpty()) {
71 QStringList zoomList = CollectionConfiguration::lastZoomFactors(helpEngine);
72 while (zoomList.count() < currentPages.count())
73 zoomList.append(t: CollectionConfiguration::DefaultZoomFactor);
74
75 for (int i = currentPages.count(); --i >= 0;) {
76 if (QUrl(currentPages.at(i)).host() == nsName) {
77 zoomList.removeAt(i);
78 currentPages.removeAt(i);
79 lastPage = (lastPage == (i + 1)) ? 1 : lastPage;
80 }
81 }
82
83 CollectionConfiguration::setLastShownPages(helpEngine, lastShownPages: currentPages);
84 CollectionConfiguration::setLastTabPage(helpEngine, lastPage);
85 CollectionConfiguration::setLastZoomFactors(helPEngine&: helpEngine, lastZoomFactors: zoomList);
86 }
87}
88
89void stripNonexistingDocs(QHelpEngineCore& collection)
90{
91 TRACE_OBJ
92 const QStringList &namespaces = collection.registeredDocumentations();
93 for (const QString &ns : namespaces) {
94 QFileInfo fi(collection.documentationFileName(namespaceName: ns));
95 if (!fi.exists() || !fi.isFile())
96 collection.unregisterDocumentation(namespaceName: ns);
97 }
98}
99
100QString indexFilesFolder(const QString &collectionFile)
101{
102 TRACE_OBJ
103 QString indexFilesFolder = QLatin1String(".fulltextsearch");
104 if (!collectionFile.isEmpty()) {
105 QFileInfo fi(collectionFile);
106 indexFilesFolder = QLatin1Char('.') +
107 fi.fileName().left(n: fi.fileName().lastIndexOf(s: QLatin1String(".qhc")));
108 }
109 return indexFilesFolder;
110}
111
112/*
113 * Returns the expected absolute file path of the cached collection file
114 * correspondinging to the given collection's file.
115 * It may or may not exist yet.
116 */
117QString constructCachedCollectionFilePath(const QHelpEngineCore &collection)
118{
119 TRACE_OBJ
120 const QString &filePath = collection.collectionFile();
121 const QString &fileName = QFileInfo(filePath).fileName();
122 const QString &cacheDir = CollectionConfiguration::cacheDir(helpEngine: collection);
123 const QString &dir = !cacheDir.isEmpty()
124 && CollectionConfiguration::cacheDirIsRelativeToCollection(helpEngine: collection)
125 ? QFileInfo(filePath).dir().absolutePath()
126 + QDir::separator() + cacheDir
127 : MainWindow::collectionFileDirectory(createDir: false, cacheDir);
128 return dir + QDir::separator() + fileName;
129}
130
131bool synchronizeDocs(QHelpEngineCore &collection,
132 QHelpEngineCore &cachedCollection,
133 CmdLineParser &cmd)
134{
135 TRACE_OBJ
136 const QDateTime &lastCollectionRegisterTime =
137 CollectionConfiguration::lastRegisterTime(helpEngine: collection);
138 if (!lastCollectionRegisterTime.isValid() || lastCollectionRegisterTime
139 < CollectionConfiguration::lastRegisterTime(helpEngine: cachedCollection))
140 return true;
141
142 const QStringList &docs = collection.registeredDocumentations();
143 const QStringList &cachedDocs = cachedCollection.registeredDocumentations();
144
145 /*
146 * Ensure that the cached collection contains all docs that
147 * the collection contains.
148 */
149 for (const QString &doc : docs) {
150 if (!cachedDocs.contains(str: doc)) {
151 const QString &docFile = collection.documentationFileName(namespaceName: doc);
152 if (!cachedCollection.registerDocumentation(documentationFileName: docFile)) {
153 cmd.showMessage(msg: QCoreApplication::translate(context: "Assistant",
154 key: "Error registering documentation file '%1': %2").
155 arg(a: docFile).arg(a: cachedCollection.error()), error: true);
156 return false;
157 }
158 }
159 }
160
161 CollectionConfiguration::updateLastRegisterTime(helpEngine&: cachedCollection);
162
163 return true;
164}
165
166bool removeSearchIndex(const QString &collectionFile)
167{
168 TRACE_OBJ
169 QString path = QFileInfo(collectionFile).path();
170 path += QLatin1Char('/') + indexFilesFolder(collectionFile);
171
172 QDir dir(path);
173 if (!dir.exists())
174 return false;
175
176 const QStringList &list = dir.entryList(filters: QDir::Files | QDir::Hidden);
177 for (const QString &item : list)
178 dir.remove(fileName: item);
179 return true;
180}
181
182QCoreApplication* createApplication(int &argc, char *argv[])
183{
184 TRACE_OBJ
185#ifndef Q_OS_WIN
186 // Look for arguments that imply command-line mode.
187 const char * cmdModeArgs[] = {
188 "-help", "-register", "-unregister", "-remove-search-index",
189 "-rebuild-search-index"
190 };
191 for (int i = 1; i < argc; ++i) {
192 for (size_t j = 0; j < sizeof cmdModeArgs/sizeof *cmdModeArgs; ++j) {
193 if (strcmp(s1: argv[i], s2: cmdModeArgs[j]) == 0)
194 return new QCoreApplication(argc, argv);
195 }
196 }
197#endif
198 QApplication *app = new QApplication(argc, argv);
199 app->connect(sender: app, signal: &QGuiApplication::lastWindowClosed,
200 slot: &QCoreApplication::quit);
201 return app;
202}
203
204bool registerDocumentation(QHelpEngineCore &collection, CmdLineParser &cmd,
205 bool printSuccess)
206{
207 TRACE_OBJ
208 if (!collection.registerDocumentation(documentationFileName: cmd.helpFile())) {
209 cmd.showMessage(msg: QCoreApplication::translate(context: "Assistant",
210 key: "Could not register documentation file\n%1\n\nReason:\n%2")
211 .arg(a: cmd.helpFile()).arg(a: collection.error()), error: true);
212 return false;
213 }
214 if (printSuccess)
215 cmd.showMessage(msg: QCoreApplication::translate(context: "Assistant",
216 key: "Documentation successfully registered."),
217 error: false);
218 CollectionConfiguration::updateLastRegisterTime(helpEngine&: collection);
219 return true;
220}
221
222bool unregisterDocumentation(QHelpEngineCore &collection,
223 const QString &namespaceName, CmdLineParser &cmd, bool printSuccess)
224{
225 TRACE_OBJ
226 if (!collection.unregisterDocumentation(namespaceName)) {
227 cmd.showMessage(msg: QCoreApplication::translate(context: "Assistant",
228 key: "Could not unregister documentation"
229 " file\n%1\n\nReason:\n%2").
230 arg(a: cmd.helpFile()).arg(a: collection.error()), error: true);
231 return false;
232 }
233 updateLastPagesOnUnregister(helpEngine&: collection, nsName: namespaceName);
234 if (printSuccess)
235 cmd.showMessage(msg: QCoreApplication::translate(context: "Assistant",
236 key: "Documentation successfully unregistered."),
237 error: false);
238 return true;
239}
240
241void setupTranslation(const QString &fileName, const QString &dir)
242{
243 QTranslator *translator = new QTranslator(QCoreApplication::instance());
244 if (translator->load(filename: fileName, directory: dir))
245 QCoreApplication::installTranslator(messageFile: translator);
246#ifdef DEBUG_TRANSLATIONS
247 else if (!fileName.endsWith(QLatin1String("en_US"))
248 && !fileName.endsWith(QLatin1String("_C"))) {
249 qDebug("Could not load translation file %s in directory %s.",
250 qPrintable(fileName), qPrintable(dir));
251 }
252#endif
253}
254
255void setupTranslations()
256{
257 TRACE_OBJ
258 const QString& locale = QLocale::system().name();
259 const QString &resourceDir
260 = QLibraryInfo::location(QLibraryInfo::TranslationsPath);
261 setupTranslation(fileName: QLatin1String("assistant_") + locale, dir: resourceDir);
262 setupTranslation(fileName: QLatin1String("qt_") + locale, dir: resourceDir);
263 setupTranslation(fileName: QLatin1String("qt_help_") + locale, dir: resourceDir);
264}
265
266} // Anonymous namespace.
267
268enum ExitStatus {
269 ExitSuccess = 0,
270 ExitFailure,
271 NoExit
272};
273
274static ExitStatus preliminarySetup(CmdLineParser *cmd)
275{
276 /*
277 * Create the collection objects that we need. We always have the
278 * cached collection file. Depending on whether the user specified
279 * one, we also may have an input collection file.
280 */
281 const QString collectionFile = cmd->collectionFile();
282 const bool collectionFileGiven = !collectionFile.isEmpty();
283 QScopedPointer<QHelpEngineCore> collection;
284 if (collectionFileGiven) {
285 collection.reset(other: new QHelpEngineCore(collectionFile));
286 collection->setProperty(name: "_q_readonly", value: QVariant::fromValue<bool>(value: true));
287 if (!collection->setupData()) {
288 cmd->showMessage(msg: QCoreApplication::translate(context: "Assistant",
289 key: "Error reading collection file '%1': %2.")
290 .arg(a: collectionFile).arg(a: collection->error()), error: true);
291 return ExitFailure;
292 }
293 }
294 const QString &cachedCollectionFile = collectionFileGiven
295 ? constructCachedCollectionFilePath(collection: *collection)
296 : MainWindow::defaultHelpCollectionFileName();
297 if (collectionFileGiven && !QFileInfo(cachedCollectionFile).exists()
298 && !collection->copyCollectionFile(fileName: cachedCollectionFile)) {
299 cmd->showMessage(msg: QCoreApplication::translate(context: "Assistant",
300 key: "Error creating collection file '%1': %2.")
301 .arg(a: cachedCollectionFile).arg(a: collection->error()), error: true);
302 return ExitFailure;
303 }
304 QHelpEngineCore cachedCollection(cachedCollectionFile);
305 if (!cachedCollection.setupData()) {
306 cmd->showMessage(msg: QCoreApplication::translate(context: "Assistant",
307 key: "Error reading collection file '%1': %2.")
308 .arg(a: cachedCollectionFile)
309 .arg(a: cachedCollection.error()), error: true);
310 return ExitFailure;
311 }
312
313 stripNonexistingDocs(collection&: cachedCollection);
314 if (collectionFileGiven) {
315 if (CollectionConfiguration::isNewer(newer: *collection, older: cachedCollection))
316 CollectionConfiguration::copyConfiguration(source: *collection,
317 target&: cachedCollection);
318 if (!synchronizeDocs(collection&: *collection, cachedCollection, cmd&: *cmd))
319 return ExitFailure;
320 }
321
322 if (cmd->registerRequest() != CmdLineParser::None) {
323 const QStringList &cachedDocs =
324 cachedCollection.registeredDocumentations();
325 const QString &namespaceName =
326 QHelpEngineCore::namespaceName(documentationFileName: cmd->helpFile());
327 if (cmd->registerRequest() == CmdLineParser::Register) {
328 if (collectionFileGiven
329 && !registerDocumentation(collection&: *collection, cmd&: *cmd, printSuccess: true))
330 return ExitFailure;
331 if (!cachedDocs.contains(str: namespaceName)
332 && !registerDocumentation(collection&: cachedCollection, cmd&: *cmd, printSuccess: !collectionFileGiven))
333 return ExitFailure;
334 return ExitSuccess;
335 }
336 if (cmd->registerRequest() == CmdLineParser::Unregister) {
337 if (collectionFileGiven
338 && !unregisterDocumentation(collection&: *collection, namespaceName, cmd&: *cmd, printSuccess: true))
339 return ExitFailure;
340 if (cachedDocs.contains(str: namespaceName)
341 && !unregisterDocumentation(collection&: cachedCollection, namespaceName,
342 cmd&: *cmd, printSuccess: !collectionFileGiven))
343 return ExitFailure;
344 return ExitSuccess;
345 }
346 }
347
348 if (cmd->removeSearchIndex()) {
349 return removeSearchIndex(collectionFile: cachedCollectionFile)
350 ? ExitSuccess : ExitFailure;
351 }
352
353 if (!QSqlDatabase::isDriverAvailable(name: QLatin1String("QSQLITE"))) {
354 cmd->showMessage(msg: QCoreApplication::translate(context: "Assistant",
355 key: "Cannot load sqlite database driver!"),
356 error: true);
357 return ExitFailure;
358 }
359
360 if (!cmd->currentFilter().isEmpty()) {
361 if (collectionFileGiven)
362 collection->setCurrentFilter(cmd->currentFilter());
363 cachedCollection.setCurrentFilter(cmd->currentFilter());
364 }
365
366 if (collectionFileGiven)
367 cmd->setCollectionFile(cachedCollectionFile);
368
369 return NoExit;
370}
371
372int main(int argc, char *argv[])
373{
374 QCoreApplication::setAttribute(attribute: Qt::AA_EnableHighDpiScaling);
375 QCoreApplication::setAttribute(attribute: Qt::AA_UseHighDpiPixmaps);
376 QCoreApplication::setAttribute(attribute: Qt::AA_DisableWindowContextHelpButton);
377 TRACE_OBJ
378 QScopedPointer<QCoreApplication> a(createApplication(argc, argv));
379#if QT_CONFIG(library)
380 a->addLibraryPath(a->applicationDirPath() + QLatin1String("/plugins"));
381#endif
382 setupTranslations();
383
384#if defined(BROWSER_QTWEBKIT)
385 if (qobject_cast<QApplication *>(a.data())) {
386 QFont f;
387 f.setStyleHint(QFont::SansSerif);
388 QWebSettings::globalSettings()->setFontFamily(QWebSettings::StandardFont, f.defaultFamily());
389 }
390#endif // BROWSER_QTWEBKIT
391
392 // Parse arguments.
393 CmdLineParser cmd(a->arguments());
394 CmdLineParser::Result res = cmd.parse();
395 if (res == CmdLineParser::Help)
396 return 0;
397 else if (res == CmdLineParser::Error)
398 return -1;
399
400 const ExitStatus status = preliminarySetup(cmd: &cmd);
401 switch (status) {
402 case ExitFailure: return EXIT_FAILURE;
403 case ExitSuccess: return EXIT_SUCCESS;
404 default: break;
405 }
406
407 MainWindow *w = new MainWindow(&cmd);
408 w->show();
409
410 /*
411 * We need to be careful here: The main window has to be deleted before
412 * the help engine wrapper, which has to be deleted before the
413 * QApplication.
414 */
415 const int retval = a->exec();
416 delete w;
417 HelpEngineWrapper::removeInstance();
418 return retval;
419}
420

source code of qttools/src/assistant/assistant/main.cpp