From 2c17fbe8dd9fdc619efdeceeddf68ad68f6dfcc1 Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Fri, 31 Aug 2018 16:00:32 +0200 Subject: Make Core independent from QtHelp We don't want various plugins to depend on the Help plugin, but we also do not want Core to depend on QtHelp. For example when turning the Help plugin off, documentation should actually no longer be registered through QtHelp. So we need parts of the interface in Core, which must then be delegated to the actual implementation in Help. As positive side-effects the interface in Core will be slimmer, and the code in the Help plugin can later be simplified, too, because then we don't have the "Core" and the "Gui" help engines separated in different plugins anymore, which should remove the need for some setup indirections. Task-number: QTCREATORBUG-20381 Change-Id: I634c5811c45d6a3dfd6ddc682cae270e38384cbf Reviewed-by: hjk --- src/plugins/help/helpmanager.cpp | 497 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 497 insertions(+) create mode 100644 src/plugins/help/helpmanager.cpp (limited to 'src/plugins/help/helpmanager.cpp') diff --git a/src/plugins/help/helpmanager.cpp b/src/plugins/help/helpmanager.cpp new file mode 100644 index 0000000000..34be168bcb --- /dev/null +++ b/src/plugins/help/helpmanager.cpp @@ -0,0 +1,497 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "helpmanager.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +using namespace Core; + +static const char kUserDocumentationKey[] = "Help/UserDocumentation"; +static const char kUpdateDocumentationTask[] = "UpdateDocumentationTask"; + +namespace Help { +namespace Internal { + +struct HelpManagerPrivate +{ + HelpManagerPrivate() = default; + ~HelpManagerPrivate(); + + const QStringList documentationFromInstaller(); + void readSettings(); + void writeSettings(); + void cleanUpDocumentation(); + + bool m_needsSetup = true; + QHelpEngineCore *m_helpEngine = nullptr; + Utils::FileSystemWatcher *m_collectionWatcher = nullptr; + + // data for delayed initialization + QSet m_filesToRegister; + QSet m_nameSpacesToUnregister; + QHash m_customValues; + + QSet m_userRegisteredFiles; + + QMutex m_helpengineMutex; + QFuture m_registerFuture; +}; + +static HelpManager *m_instance = nullptr; +static HelpManagerPrivate *d = nullptr; + +static const char linksForKeyQuery[] = "SELECT d.Title, f.Name, e.Name, " + "d.Name, a.Anchor FROM IndexTable a, FileNameTable d, FolderTable e, " + "NamespaceTable f WHERE a.FileId=d.FileId AND d.FolderId=e.Id AND " + "a.NamespaceId=f.Id AND a.Name='%1'"; + +// -- DbCleaner + +struct DbCleaner +{ + DbCleaner(const QString &dbName) : name(dbName) {} + ~DbCleaner() { QSqlDatabase::removeDatabase(name); } + QString name; +}; + +// -- HelpManager + +HelpManager::HelpManager(QObject *parent) : + QObject(parent) +{ + QTC_CHECK(!m_instance); + m_instance = this; + d = new HelpManagerPrivate; +} + +HelpManager::~HelpManager() +{ + delete d; + m_instance = nullptr; +} + +HelpManager *HelpManager::instance() +{ + Q_ASSERT(m_instance); + return m_instance; +} + +QString HelpManager::collectionFilePath() +{ + return QDir::cleanPath(ICore::userResourcePath() + + QLatin1String("/helpcollection.qhc")); +} + +void HelpManager::registerDocumentation(const QStringList &files) +{ + if (d->m_needsSetup) { + for (const QString &filePath : files) + d->m_filesToRegister.insert(filePath); + return; + } + + QFuture future = Utils::runAsync(&HelpManager::registerDocumentationNow, files); + Utils::onResultReady(future, this, [](bool docsChanged){ + if (docsChanged) { + d->m_helpEngine->setupData(); + emit Core::HelpManager::Signals::instance()->documentationChanged(); + } + }); + ProgressManager::addTask(future, tr("Update Documentation"), + kUpdateDocumentationTask); +} + +void HelpManager::registerDocumentationNow(QFutureInterface &futureInterface, + const QStringList &files) +{ + QMutexLocker locker(&d->m_helpengineMutex); + + futureInterface.setProgressRange(0, files.count()); + futureInterface.setProgressValue(0); + + QHelpEngineCore helpEngine(collectionFilePath()); + helpEngine.setupData(); + bool docsChanged = false; + QStringList nameSpaces = helpEngine.registeredDocumentations(); + for (const QString &file : files) { + if (futureInterface.isCanceled()) + break; + futureInterface.setProgressValue(futureInterface.progressValue() + 1); + const QString &nameSpace = helpEngine.namespaceName(file); + if (nameSpace.isEmpty()) + continue; + if (!nameSpaces.contains(nameSpace)) { + if (helpEngine.registerDocumentation(file)) { + nameSpaces.append(nameSpace); + docsChanged = true; + } else { + qWarning() << "Error registering namespace '" << nameSpace + << "' from file '" << file << "':" << helpEngine.error(); + } + } else { + const QLatin1String key("CreationDate"); + const QString &newDate = helpEngine.metaData(file, key).toString(); + const QString &oldDate = helpEngine.metaData( + helpEngine.documentationFileName(nameSpace), key).toString(); + if (QDateTime::fromString(newDate, Qt::ISODate) + > QDateTime::fromString(oldDate, Qt::ISODate)) { + if (helpEngine.unregisterDocumentation(nameSpace)) { + docsChanged = true; + helpEngine.registerDocumentation(file); + } + } + } + } + futureInterface.reportResult(docsChanged); +} + +void HelpManager::unregisterDocumentation(const QStringList &nameSpaces) +{ + if (d->m_needsSetup) { + for (const QString &name : nameSpaces) + d->m_nameSpacesToUnregister.insert(name); + return; + } + + QMutexLocker locker(&d->m_helpengineMutex); + bool docsChanged = false; + for (const QString &nameSpace : nameSpaces) { + const QString filePath = d->m_helpEngine->documentationFileName(nameSpace); + if (d->m_helpEngine->unregisterDocumentation(nameSpace)) { + docsChanged = true; + d->m_userRegisteredFiles.remove(filePath); + } else { + qWarning() << "Error unregistering namespace '" << nameSpace + << "' from file '" << filePath + << "': " << d->m_helpEngine->error(); + } + } + locker.unlock(); + if (docsChanged) + emit Core::HelpManager::Signals::instance()->documentationChanged(); +} + +void HelpManager::registerUserDocumentation(const QStringList &filePaths) +{ + for (const QString &filePath : filePaths) + d->m_userRegisteredFiles.insert(filePath); + m_instance->registerDocumentation(filePaths); +} + +QSet HelpManager::userDocumentationPaths() +{ + return d->m_userRegisteredFiles; +} + +static QUrl buildQUrl(const QString &ns, const QString &folder, + const QString &relFileName, const QString &anchor) +{ + QUrl url; + url.setScheme(QLatin1String("qthelp")); + url.setAuthority(ns); + url.setPath(QLatin1Char('/') + folder + QLatin1Char('/') + relFileName); + url.setFragment(anchor); + return url; +} + +// This should go into Qt 4.8 once we start using it for Qt Creator +QMap HelpManager::linksForKeyword(const QString &key) +{ + QMap links; + QTC_ASSERT(!d->m_needsSetup, return links); + + const QLatin1String sqlite("QSQLITE"); + const QLatin1String name("HelpManager::linksForKeyword"); + + DbCleaner cleaner(name); + QSqlDatabase db = QSqlDatabase::addDatabase(sqlite, name); + if (db.driver() && db.driver()->lastError().type() == QSqlError::NoError) { + const QStringList ®isteredDocs = d->m_helpEngine->registeredDocumentations(); + for (const QString &nameSpace : registeredDocs) { + db.setDatabaseName(d->m_helpEngine->documentationFileName(nameSpace)); + if (db.open()) { + QSqlQuery query = QSqlQuery(db); + query.setForwardOnly(true); + query.exec(QString::fromLatin1(linksForKeyQuery).arg(key)); + while (query.next()) { + QString title = query.value(0).toString(); + if (title.isEmpty()) // generate a title + corresponding path + title = key + QLatin1String(" : ") + query.value(3).toString(); + links.insertMulti(title, buildQUrl(query.value(1).toString(), + query.value(2).toString(), query.value(3).toString(), + query.value(4).toString())); + } + } + } + } + return links; +} + +QMap HelpManager::linksForIdentifier(const QString &id) +{ + QMap empty; + QTC_ASSERT(!d->m_needsSetup, return empty); + return d->m_helpEngine->linksForIdentifier(id); +} + +QUrl HelpManager::findFile(const QUrl &url) +{ + QTC_ASSERT(!d->m_needsSetup, return QUrl()); + return d->m_helpEngine->findFile(url); +} + +QByteArray HelpManager::fileData(const QUrl &url) +{ + QTC_ASSERT(!d->m_needsSetup, return QByteArray()); + return d->m_helpEngine->fileData(url); +} + +void HelpManager::handleHelpRequest(const QUrl &url, Core::HelpManager::HelpViewerLocation location) +{ + emit m_instance->helpRequested(url, location); +} + +QStringList HelpManager::registeredNamespaces() +{ + QTC_ASSERT(!d->m_needsSetup, return QStringList()); + return d->m_helpEngine->registeredDocumentations(); +} + +QString HelpManager::namespaceFromFile(const QString &file) +{ + QTC_ASSERT(!d->m_needsSetup, return QString()); + return d->m_helpEngine->namespaceName(file); +} + +QString HelpManager::fileFromNamespace(const QString &nameSpace) +{ + QTC_ASSERT(!d->m_needsSetup, return QString()); + return d->m_helpEngine->documentationFileName(nameSpace); +} + +void HelpManager::setCustomValue(const QString &key, const QVariant &value) +{ + if (d->m_needsSetup) { + d->m_customValues.insert(key, value); + return; + } + if (d->m_helpEngine->setCustomValue(key, value)) + emit m_instance->collectionFileChanged(); +} + +QVariant HelpManager::customValue(const QString &key, const QVariant &value) +{ + QTC_ASSERT(!d->m_needsSetup, return QVariant()); + return d->m_helpEngine->customValue(key, value); +} + +HelpManager::Filters HelpManager::filters() +{ + QTC_ASSERT(!d->m_needsSetup, return Filters()); + + Filters filters; + const QStringList &customFilters = d->m_helpEngine->customFilters(); + for (const QString &filter : customFilters) + filters.insert(filter, d->m_helpEngine->filterAttributes(filter)); + return filters; +} + +HelpManager::Filters HelpManager::fixedFilters() +{ + Filters fixedFilters; + QTC_ASSERT(!d->m_needsSetup, return fixedFilters); + + const QLatin1String sqlite("QSQLITE"); + const QLatin1String name("HelpManager::fixedCustomFilters"); + + DbCleaner cleaner(name); + QSqlDatabase db = QSqlDatabase::addDatabase(sqlite, name); + if (db.driver() && db.driver()->lastError().type() == QSqlError::NoError) { + const QStringList ®isteredDocs = d->m_helpEngine->registeredDocumentations(); + for (const QString &nameSpace : registeredDocs) { + db.setDatabaseName(d->m_helpEngine->documentationFileName(nameSpace)); + if (db.open()) { + QSqlQuery query = QSqlQuery(db); + query.setForwardOnly(true); + query.exec(QLatin1String("SELECT Name FROM FilterNameTable")); + while (query.next()) { + const QString &filter = query.value(0).toString(); + fixedFilters.insert(filter, d->m_helpEngine->filterAttributes(filter)); + } + } + } + } + return fixedFilters; +} + +HelpManager::Filters HelpManager::userDefinedFilters() +{ + QTC_ASSERT(!d->m_needsSetup, return Filters()); + + Filters all = filters(); + const Filters &fixed = fixedFilters(); + for (Filters::const_iterator it = fixed.constBegin(); it != fixed.constEnd(); ++it) + all.remove(it.key()); + return all; +} + +void HelpManager::removeUserDefinedFilter(const QString &filter) +{ + QTC_ASSERT(!d->m_needsSetup, return); + + if (d->m_helpEngine->removeCustomFilter(filter)) + emit m_instance->collectionFileChanged(); +} + +void HelpManager::addUserDefinedFilter(const QString &filter, const QStringList &attr) +{ + QTC_ASSERT(!d->m_needsSetup, return); + + if (d->m_helpEngine->addCustomFilter(filter, attr)) + emit m_instance->collectionFileChanged(); +} + +void HelpManager::aboutToShutdown() +{ + if (d && d->m_registerFuture.isRunning()) { + d->m_registerFuture.cancel(); + d->m_registerFuture.waitForFinished(); + } +} + +// -- private + +void HelpManager::setupHelpManager() +{ + if (!d->m_needsSetup) + return; + d->m_needsSetup = false; + + d->readSettings(); + + // create the help engine + d->m_helpEngine = new QHelpEngineCore(collectionFilePath(), m_instance); + d->m_helpEngine->setupData(); + + for (const QString &filePath : d->documentationFromInstaller()) + d->m_filesToRegister.insert(filePath); + + d->cleanUpDocumentation(); + + if (!d->m_nameSpacesToUnregister.isEmpty()) { + m_instance->unregisterDocumentation(d->m_nameSpacesToUnregister.toList()); + d->m_nameSpacesToUnregister.clear(); + } + + if (!d->m_filesToRegister.isEmpty()) { + m_instance->registerDocumentation(d->m_filesToRegister.toList()); + d->m_filesToRegister.clear(); + } + + QHash::const_iterator it; + for (it = d->m_customValues.constBegin(); it != d->m_customValues.constEnd(); ++it) + setCustomValue(it.key(), it.value()); + + emit Core::HelpManager::Signals::instance()->setupFinished(); +} + +void HelpManagerPrivate::cleanUpDocumentation() +{ + // mark documentation for removal for which there is no documentation file anymore + // mark documentation for removal that is neither user registered, nor marked for registration + const QStringList ®isteredDocs = m_helpEngine->registeredDocumentations(); + for (const QString &nameSpace : registeredDocs) { + const QString filePath = m_helpEngine->documentationFileName(nameSpace); + if (!QFileInfo::exists(filePath) + || (!m_filesToRegister.contains(filePath) + && !m_userRegisteredFiles.contains(filePath))) { + m_nameSpacesToUnregister.insert(nameSpace); + } + } +} + +HelpManagerPrivate::~HelpManagerPrivate() +{ + writeSettings(); + delete m_helpEngine; + m_helpEngine = nullptr; +} + +const QStringList HelpManagerPrivate::documentationFromInstaller() +{ + QSettings *installSettings = ICore::settings(); + const QStringList documentationPaths = installSettings->value(QLatin1String("Help/InstalledDocumentation")) + .toStringList(); + QStringList documentationFiles; + for (const QString &path : documentationPaths) { + QFileInfo pathInfo(path); + if (pathInfo.isFile() && pathInfo.isReadable()) { + documentationFiles << pathInfo.absoluteFilePath(); + } else if (pathInfo.isDir()) { + const QFileInfoList files(QDir(path).entryInfoList(QStringList(QLatin1String("*.qch")), + QDir::Files | QDir::Readable)); + for (const QFileInfo &fileInfo : files) + documentationFiles << fileInfo.absoluteFilePath(); + } + } + return documentationFiles; +} + +void HelpManagerPrivate::readSettings() +{ + m_userRegisteredFiles = ICore::settings()->value(QLatin1String(kUserDocumentationKey)) + .toStringList().toSet(); +} + +void HelpManagerPrivate::writeSettings() +{ + const QStringList list = m_userRegisteredFiles.toList(); + ICore::settings()->setValue(QLatin1String(kUserDocumentationKey), list); +} + +} // Internal +} // Core -- cgit v1.2.1