| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Assistant of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** 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 Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** 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-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| #include "qhelpcontentwidget.h" |
| #include "qhelpenginecore.h" |
| #include "qhelpengine_p.h" |
| #include "qhelpcollectionhandler_p.h" |
| |
| #include <QDir> |
| #include <QtCore/QStack> |
| #include <QtCore/QThread> |
| #include <QtCore/QMutex> |
| #include <QtWidgets/QHeaderView> |
| |
| QT_BEGIN_NAMESPACE |
| |
| class QHelpContentItemPrivate |
| { |
| public: |
| QHelpContentItemPrivate(const QString &t, const QUrl &l, QHelpContentItem *p) |
| : parent(p), |
| title(t), |
| link(l) |
| { |
| } |
| |
| void appendChild(QHelpContentItem *item) { childItems.append(item); } |
| |
| QList<QHelpContentItem*> childItems; |
| QHelpContentItem *parent; |
| QString title; |
| QUrl link; |
| }; |
| |
| class QHelpContentProvider : public QThread |
| { |
| Q_OBJECT |
| public: |
| QHelpContentProvider(QHelpEnginePrivate *helpEngine); |
| ~QHelpContentProvider() override; |
| void collectContents(const QString &customFilterName); |
| void stopCollecting(); |
| QHelpContentItem *takeContentItem(); |
| |
| private: |
| void run() override; |
| |
| QHelpEnginePrivate *m_helpEngine; |
| QString m_currentFilter; |
| QStringList m_filterAttributes; |
| QString m_collectionFile; |
| QHelpContentItem *m_rootItem = nullptr; |
| QMutex m_mutex; |
| bool m_usesFilterEngine = false; |
| bool m_abort = false; |
| }; |
| |
| class QHelpContentModelPrivate |
| { |
| public: |
| QHelpContentItem *rootItem = nullptr; |
| QHelpContentProvider *qhelpContentProvider; |
| }; |
| |
| |
| |
| /*! |
| \class QHelpContentItem |
| \inmodule QtHelp |
| \brief The QHelpContentItem class provides an item for use with QHelpContentModel. |
| \since 4.4 |
| */ |
| |
| QHelpContentItem::QHelpContentItem(const QString &name, const QUrl &link, QHelpContentItem *parent) |
| { |
| d = new QHelpContentItemPrivate(name, link, parent); |
| } |
| |
| /*! |
| Destroys the help content item. |
| */ |
| QHelpContentItem::~QHelpContentItem() |
| { |
| qDeleteAll(d->childItems); |
| delete d; |
| } |
| |
| /*! |
| Returns the child of the content item in the give \a row. |
| |
| \sa parent() |
| */ |
| QHelpContentItem *QHelpContentItem::child(int row) const |
| { |
| return d->childItems.value(row); |
| } |
| |
| /*! |
| Returns the number of child items. |
| */ |
| int QHelpContentItem::childCount() const |
| { |
| return d->childItems.count(); |
| } |
| |
| /*! |
| Returns the row of this item from its parents view. |
| */ |
| int QHelpContentItem::row() const |
| { |
| if (d->parent) |
| return d->parent->d->childItems.indexOf(const_cast<QHelpContentItem*>(this)); |
| return 0; |
| } |
| |
| /*! |
| Returns the title of the content item. |
| */ |
| QString QHelpContentItem::title() const |
| { |
| return d->title; |
| } |
| |
| /*! |
| Returns the URL of this content item. |
| */ |
| QUrl QHelpContentItem::url() const |
| { |
| return d->link; |
| } |
| |
| /*! |
| Returns the parent content item. |
| */ |
| QHelpContentItem *QHelpContentItem::parent() const |
| { |
| return d->parent; |
| } |
| |
| /*! |
| Returns the position of a given \a child. |
| */ |
| int QHelpContentItem::childPosition(QHelpContentItem *child) const |
| { |
| return d->childItems.indexOf(child); |
| } |
| |
| |
| |
| QHelpContentProvider::QHelpContentProvider(QHelpEnginePrivate *helpEngine) |
| : QThread(helpEngine) |
| { |
| m_helpEngine = helpEngine; |
| } |
| |
| QHelpContentProvider::~QHelpContentProvider() |
| { |
| stopCollecting(); |
| } |
| |
| void QHelpContentProvider::collectContents(const QString &customFilterName) |
| { |
| m_mutex.lock(); |
| m_currentFilter = customFilterName; |
| m_filterAttributes = m_helpEngine->q->filterAttributes(customFilterName); |
| m_collectionFile = m_helpEngine->collectionHandler->collectionFile(); |
| m_usesFilterEngine = m_helpEngine->usesFilterEngine; |
| m_mutex.unlock(); |
| |
| if (isRunning()) |
| stopCollecting(); |
| start(LowPriority); |
| } |
| |
| void QHelpContentProvider::stopCollecting() |
| { |
| if (isRunning()) { |
| m_mutex.lock(); |
| m_abort = true; |
| m_mutex.unlock(); |
| wait(); |
| // we need to force-set m_abort to false, because the thread might either have |
| // finished between the isRunning() check and the "m_abort = true" above, or the |
| // isRunning() check might already happen after the "m_abort = false" in the run() method, |
| // either way never resetting m_abort to false from within the run() method |
| m_abort = false; |
| } |
| delete m_rootItem; |
| m_rootItem = nullptr; |
| } |
| |
| QHelpContentItem *QHelpContentProvider::takeContentItem() |
| { |
| QMutexLocker locker(&m_mutex); |
| QHelpContentItem *content = m_rootItem; |
| m_rootItem = nullptr; |
| return content; |
| } |
| |
| // TODO: this is a copy from helpcollectionhandler, make it common |
| 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; |
| } |
| |
| static QUrl constructUrl(const QString &namespaceName, |
| const QString &folderName, |
| const QString &relativePath) |
| { |
| const int idx = relativePath.indexOf(QLatin1Char('#')); |
| const QString &rp = idx < 0 ? relativePath : relativePath.left(idx); |
| const QString anchor = idx < 0 ? QString() : relativePath.mid(idx + 1); |
| return buildQUrl(namespaceName, folderName, rp, anchor); |
| } |
| |
| void QHelpContentProvider::run() |
| { |
| QString title; |
| QString link; |
| int depth = 0; |
| QHelpContentItem *item = nullptr; |
| |
| m_mutex.lock(); |
| QHelpContentItem * const rootItem = new QHelpContentItem(QString(), QString(), nullptr); |
| const QString currentFilter = m_currentFilter; |
| const QStringList attributes = m_filterAttributes; |
| const QString collectionFile = m_collectionFile; |
| const bool usesFilterEngine = m_usesFilterEngine; |
| delete m_rootItem; |
| m_rootItem = nullptr; |
| m_mutex.unlock(); |
| |
| if (collectionFile.isEmpty()) |
| return; |
| |
| QHelpCollectionHandler collectionHandler(collectionFile); |
| collectionHandler.setReadOnly(true); |
| if (!collectionHandler.openCollectionFile()) |
| return; |
| |
| const QList<QHelpCollectionHandler::ContentsData> result = usesFilterEngine |
| ? collectionHandler.contentsForFilter(currentFilter) |
| : collectionHandler.contentsForFilter(attributes); |
| |
| for (const auto &contentsData : result) { |
| m_mutex.lock(); |
| if (m_abort) { |
| delete rootItem; |
| m_abort = false; |
| m_mutex.unlock(); |
| return; |
| } |
| m_mutex.unlock(); |
| |
| const QString namespaceName = contentsData.namespaceName; |
| const QString folderName = contentsData.folderName; |
| for (const QByteArray &contents : contentsData.contentsList) { |
| if (contents.size() < 1) |
| continue; |
| |
| int _depth = 0; |
| bool _root = false; |
| QStack<QHelpContentItem*> stack; |
| |
| QDataStream s(contents); |
| for (;;) { |
| s >> depth; |
| s >> link; |
| s >> title; |
| if (title.isEmpty()) |
| break; |
| const QUrl url = constructUrl(namespaceName, folderName, link); |
| CHECK_DEPTH: |
| if (depth == 0) { |
| m_mutex.lock(); |
| item = new QHelpContentItem(title, url, rootItem); |
| rootItem->d->appendChild(item); |
| m_mutex.unlock(); |
| stack.push(item); |
| _depth = 1; |
| _root = true; |
| } else { |
| if (depth > _depth && _root) { |
| _depth = depth; |
| stack.push(item); |
| } |
| if (depth == _depth) { |
| item = new QHelpContentItem(title, url, stack.top()); |
| stack.top()->d->appendChild(item); |
| } else if (depth < _depth) { |
| stack.pop(); |
| --_depth; |
| goto CHECK_DEPTH; |
| } |
| } |
| } |
| } |
| } |
| |
| m_mutex.lock(); |
| m_rootItem = rootItem; |
| m_abort = false; |
| m_mutex.unlock(); |
| } |
| |
| /*! |
| \class QHelpContentModel |
| \inmodule QtHelp |
| \brief The QHelpContentModel class provides a model that supplies content to views. |
| \since 4.4 |
| */ |
| |
| /*! |
| \fn void QHelpContentModel::contentsCreationStarted() |
| |
| This signal is emitted when the creation of the contents has |
| started. The current contents are invalid from this point on |
| until the signal contentsCreated() is emitted. |
| |
| \sa isCreatingContents() |
| */ |
| |
| /*! |
| \fn void QHelpContentModel::contentsCreated() |
| |
| This signal is emitted when the contents have been created. |
| */ |
| |
| QHelpContentModel::QHelpContentModel(QHelpEnginePrivate *helpEngine) |
| : QAbstractItemModel(helpEngine) |
| { |
| d = new QHelpContentModelPrivate(); |
| d->qhelpContentProvider = new QHelpContentProvider(helpEngine); |
| |
| connect(d->qhelpContentProvider, &QThread::finished, |
| this, &QHelpContentModel::insertContents); |
| } |
| |
| /*! |
| Destroys the help content model. |
| */ |
| QHelpContentModel::~QHelpContentModel() |
| { |
| delete d->rootItem; |
| delete d; |
| } |
| |
| /*! |
| Creates new contents by querying the help system |
| for contents specified for the \a customFilterName. |
| */ |
| void QHelpContentModel::createContents(const QString &customFilterName) |
| { |
| const bool running = d->qhelpContentProvider->isRunning(); |
| d->qhelpContentProvider->collectContents(customFilterName); |
| if (running) |
| return; |
| |
| if (d->rootItem) { |
| beginResetModel(); |
| delete d->rootItem; |
| d->rootItem = nullptr; |
| endResetModel(); |
| } |
| emit contentsCreationStarted(); |
| } |
| |
| void QHelpContentModel::insertContents() |
| { |
| if (d->qhelpContentProvider->isRunning()) |
| return; |
| |
| QHelpContentItem * const newRootItem = d->qhelpContentProvider->takeContentItem(); |
| if (!newRootItem) |
| return; |
| beginResetModel(); |
| delete d->rootItem; |
| d->rootItem = newRootItem; |
| endResetModel(); |
| emit contentsCreated(); |
| } |
| |
| /*! |
| Returns true if the contents are currently rebuilt, otherwise |
| false. |
| */ |
| bool QHelpContentModel::isCreatingContents() const |
| { |
| return d->qhelpContentProvider->isRunning(); |
| } |
| |
| /*! |
| Returns the help content item at the model index position |
| \a index. |
| */ |
| QHelpContentItem *QHelpContentModel::contentItemAt(const QModelIndex &index) const |
| { |
| if (index.isValid()) |
| return static_cast<QHelpContentItem*>(index.internalPointer()); |
| else |
| return d->rootItem; |
| } |
| |
| /*! |
| Returns the index of the item in the model specified by |
| the given \a row, \a column and \a parent index. |
| */ |
| QModelIndex QHelpContentModel::index(int row, int column, const QModelIndex &parent) const |
| { |
| if (!d->rootItem) |
| return QModelIndex(); |
| |
| QHelpContentItem *parentItem = contentItemAt(parent); |
| QHelpContentItem *item = parentItem->child(row); |
| if (!item) |
| return QModelIndex(); |
| return createIndex(row, column, item); |
| } |
| |
| /*! |
| Returns the parent of the model item with the given |
| \a index, or QModelIndex() if it has no parent. |
| */ |
| QModelIndex QHelpContentModel::parent(const QModelIndex &index) const |
| { |
| QHelpContentItem *item = contentItemAt(index); |
| if (!item) |
| return QModelIndex(); |
| |
| QHelpContentItem *parentItem = static_cast<QHelpContentItem*>(item->parent()); |
| if (!parentItem) |
| return QModelIndex(); |
| |
| QHelpContentItem *grandparentItem = static_cast<QHelpContentItem*>(parentItem->parent()); |
| if (!grandparentItem) |
| return QModelIndex(); |
| |
| int row = grandparentItem->childPosition(parentItem); |
| return createIndex(row, index.column(), parentItem); |
| } |
| |
| /*! |
| Returns the number of rows under the given \a parent. |
| */ |
| int QHelpContentModel::rowCount(const QModelIndex &parent) const |
| { |
| QHelpContentItem *parentItem = contentItemAt(parent); |
| if (!parentItem) |
| return 0; |
| return parentItem->childCount(); |
| } |
| |
| /*! |
| Returns the number of columns under the given \a parent. Currently returns always 1. |
| */ |
| int QHelpContentModel::columnCount(const QModelIndex &parent) const |
| { |
| Q_UNUSED(parent); |
| |
| return 1; |
| } |
| |
| /*! |
| Returns the data stored under the given \a role for |
| the item referred to by the \a index. |
| */ |
| QVariant QHelpContentModel::data(const QModelIndex &index, int role) const |
| { |
| if (role != Qt::DisplayRole) |
| return QVariant(); |
| |
| QHelpContentItem *item = contentItemAt(index); |
| if (!item) |
| return QVariant(); |
| return item->title(); |
| } |
| |
| |
| |
| /*! |
| \class QHelpContentWidget |
| \inmodule QtHelp |
| \brief The QHelpContentWidget class provides a tree view for displaying help content model items. |
| \since 4.4 |
| */ |
| |
| /*! |
| \fn void QHelpContentWidget::linkActivated(const QUrl &link) |
| |
| This signal is emitted when a content item is activated and |
| its associated \a link should be shown. |
| */ |
| |
| QHelpContentWidget::QHelpContentWidget() |
| : QTreeView(nullptr) |
| { |
| header()->hide(); |
| setUniformRowHeights(true); |
| connect(this, &QAbstractItemView::activated, |
| this, &QHelpContentWidget::showLink); |
| } |
| |
| /*! |
| Returns the index of the content item with the \a link. |
| An invalid index is returned if no such an item exists. |
| */ |
| QModelIndex QHelpContentWidget::indexOf(const QUrl &link) |
| { |
| QHelpContentModel *contentModel = qobject_cast<QHelpContentModel*>(model()); |
| if (!contentModel || link.scheme() != QLatin1String("qthelp")) |
| return QModelIndex(); |
| |
| m_syncIndex = QModelIndex(); |
| for (int i = 0; i < contentModel->rowCount(); ++i) { |
| QHelpContentItem *itm = contentModel->contentItemAt(contentModel->index(i, 0)); |
| if (itm && itm->url().host() == link.host()) { |
| if (searchContentItem(contentModel, contentModel->index(i, 0), QDir::cleanPath(link.path()))) |
| return m_syncIndex; |
| } |
| } |
| return QModelIndex(); |
| } |
| |
| bool QHelpContentWidget::searchContentItem(QHelpContentModel *model, const QModelIndex &parent, |
| const QString &cleanPath) |
| { |
| QHelpContentItem *parentItem = model->contentItemAt(parent); |
| if (!parentItem) |
| return false; |
| |
| if (QDir::cleanPath(parentItem->url().path()) == cleanPath) { |
| m_syncIndex = parent; |
| return true; |
| } |
| |
| for (int i = 0; i < parentItem->childCount(); ++i) { |
| if (searchContentItem(model, model->index(i, 0, parent), cleanPath)) |
| return true; |
| } |
| return false; |
| } |
| |
| void QHelpContentWidget::showLink(const QModelIndex &index) |
| { |
| QHelpContentModel *contentModel = qobject_cast<QHelpContentModel*>(model()); |
| if (!contentModel) |
| return; |
| |
| QHelpContentItem *item = contentModel->contentItemAt(index); |
| if (!item) |
| return; |
| QUrl url = item->url(); |
| if (url.isValid()) |
| emit linkActivated(url); |
| } |
| |
| QT_END_NAMESPACE |
| |
| #include "qhelpcontentwidget.moc" |