diff options
Diffstat (limited to 'src/plugins/help/helpmanager.cpp')
-rw-r--r-- | src/plugins/help/helpmanager.cpp | 497 |
1 files changed, 497 insertions, 0 deletions
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 <coreplugin/icore.h> +#include <coreplugin/progressmanager/progressmanager.h> +#include <utils/algorithm.h> +#include <utils/filesystemwatcher.h> +#include <utils/qtcassert.h> +#include <utils/runextensions.h> + +#include <QDateTime> +#include <QDebug> +#include <QDir> +#include <QFileInfo> +#include <QFutureWatcher> +#include <QStringList> +#include <QUrl> + +#include <QHelpEngineCore> + +#include <QMutexLocker> +#include <QSqlDatabase> +#include <QSqlDriver> +#include <QSqlError> +#include <QSqlQuery> + +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<QString> m_filesToRegister; + QSet<QString> m_nameSpacesToUnregister; + QHash<QString, QVariant> m_customValues; + + QSet<QString> m_userRegisteredFiles; + + QMutex m_helpengineMutex; + QFuture<bool> 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<bool> 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<bool> &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<QString> 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<QString, QUrl> HelpManager::linksForKeyword(const QString &key) +{ + QMap<QString, QUrl> 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<QString, QUrl> HelpManager::linksForIdentifier(const QString &id) +{ + QMap<QString, QUrl> 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<QString, QVariant>::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 |