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 | |
60 | QT_USE_NAMESPACE |
61 | |
62 | namespace { |
63 | |
64 | void |
65 | updateLastPagesOnUnregister(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 | |
89 | void 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 | |
100 | QString 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 | */ |
117 | QString 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 | |
131 | bool 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 | |
166 | bool 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 | |
182 | QCoreApplication* 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 | |
204 | bool 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 | |
222 | bool 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 | |
241 | void 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 | |
255 | void 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 | |
268 | enum ExitStatus { |
269 | ExitSuccess = 0, |
270 | ExitFailure, |
271 | NoExit |
272 | }; |
273 | |
274 | static 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 | |
372 | int 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 | |