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