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:LGPL$ |
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 Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qhelpcontentwidget.h" |
41 | #include "qhelpenginecore.h" |
42 | #include "qhelpengine_p.h" |
43 | #include "qhelpcollectionhandler_p.h" |
44 | |
45 | #include <QDir> |
46 | #include <QtCore/QStack> |
47 | #include <QtCore/QThread> |
48 | #include <QtCore/QMutex> |
49 | #include <QtWidgets/QHeaderView> |
50 | |
51 | QT_BEGIN_NAMESPACE |
52 | |
53 | class QHelpContentItemPrivate |
54 | { |
55 | public: |
56 | QHelpContentItemPrivate(const QString &t, const QUrl &l, QHelpContentItem *p) |
57 | : parent(p), |
58 | title(t), |
59 | link(l) |
60 | { |
61 | } |
62 | |
63 | void appendChild(QHelpContentItem *item) { childItems.append(t: item); } |
64 | |
65 | QList<QHelpContentItem*> childItems; |
66 | QHelpContentItem *parent; |
67 | QString title; |
68 | QUrl link; |
69 | }; |
70 | |
71 | class QHelpContentProvider : public QThread |
72 | { |
73 | Q_OBJECT |
74 | public: |
75 | QHelpContentProvider(QHelpEnginePrivate *helpEngine); |
76 | ~QHelpContentProvider() override; |
77 | void collectContents(const QString &customFilterName); |
78 | void stopCollecting(); |
79 | QHelpContentItem *takeContentItem(); |
80 | |
81 | private: |
82 | void run() override; |
83 | |
84 | QHelpEnginePrivate *m_helpEngine; |
85 | QString m_currentFilter; |
86 | QStringList m_filterAttributes; |
87 | QString m_collectionFile; |
88 | QHelpContentItem *m_rootItem = nullptr; |
89 | QMutex m_mutex; |
90 | bool m_usesFilterEngine = false; |
91 | bool m_abort = false; |
92 | }; |
93 | |
94 | class QHelpContentModelPrivate |
95 | { |
96 | public: |
97 | QHelpContentItem *rootItem = nullptr; |
98 | QHelpContentProvider *qhelpContentProvider; |
99 | }; |
100 | |
101 | |
102 | |
103 | /*! |
104 | \class QHelpContentItem |
105 | \inmodule QtHelp |
106 | \brief The QHelpContentItem class provides an item for use with QHelpContentModel. |
107 | \since 4.4 |
108 | */ |
109 | |
110 | QHelpContentItem::QHelpContentItem(const QString &name, const QUrl &link, QHelpContentItem *parent) |
111 | { |
112 | d = new QHelpContentItemPrivate(name, link, parent); |
113 | } |
114 | |
115 | /*! |
116 | Destroys the help content item. |
117 | */ |
118 | QHelpContentItem::~QHelpContentItem() |
119 | { |
120 | qDeleteAll(c: d->childItems); |
121 | delete d; |
122 | } |
123 | |
124 | /*! |
125 | Returns the child of the content item in the give \a row. |
126 | |
127 | \sa parent() |
128 | */ |
129 | QHelpContentItem *QHelpContentItem::child(int row) const |
130 | { |
131 | return d->childItems.value(i: row); |
132 | } |
133 | |
134 | /*! |
135 | Returns the number of child items. |
136 | */ |
137 | int QHelpContentItem::childCount() const |
138 | { |
139 | return d->childItems.count(); |
140 | } |
141 | |
142 | /*! |
143 | Returns the row of this item from its parents view. |
144 | */ |
145 | int QHelpContentItem::row() const |
146 | { |
147 | if (d->parent) |
148 | return d->parent->d->childItems.indexOf(t: const_cast<QHelpContentItem*>(this)); |
149 | return 0; |
150 | } |
151 | |
152 | /*! |
153 | Returns the title of the content item. |
154 | */ |
155 | QString QHelpContentItem::title() const |
156 | { |
157 | return d->title; |
158 | } |
159 | |
160 | /*! |
161 | Returns the URL of this content item. |
162 | */ |
163 | QUrl QHelpContentItem::url() const |
164 | { |
165 | return d->link; |
166 | } |
167 | |
168 | /*! |
169 | Returns the parent content item. |
170 | */ |
171 | QHelpContentItem *QHelpContentItem::parent() const |
172 | { |
173 | return d->parent; |
174 | } |
175 | |
176 | /*! |
177 | Returns the position of a given \a child. |
178 | */ |
179 | int QHelpContentItem::childPosition(QHelpContentItem *child) const |
180 | { |
181 | return d->childItems.indexOf(t: child); |
182 | } |
183 | |
184 | |
185 | |
186 | QHelpContentProvider::QHelpContentProvider(QHelpEnginePrivate *helpEngine) |
187 | : QThread(helpEngine) |
188 | { |
189 | m_helpEngine = helpEngine; |
190 | } |
191 | |
192 | QHelpContentProvider::~QHelpContentProvider() |
193 | { |
194 | stopCollecting(); |
195 | } |
196 | |
197 | void QHelpContentProvider::collectContents(const QString &customFilterName) |
198 | { |
199 | m_mutex.lock(); |
200 | m_currentFilter = customFilterName; |
201 | m_filterAttributes = m_helpEngine->q->filterAttributes(filterName: customFilterName); |
202 | m_collectionFile = m_helpEngine->collectionHandler->collectionFile(); |
203 | m_usesFilterEngine = m_helpEngine->usesFilterEngine; |
204 | m_mutex.unlock(); |
205 | |
206 | if (isRunning()) |
207 | stopCollecting(); |
208 | start(LowPriority); |
209 | } |
210 | |
211 | void QHelpContentProvider::stopCollecting() |
212 | { |
213 | if (isRunning()) { |
214 | m_mutex.lock(); |
215 | m_abort = true; |
216 | m_mutex.unlock(); |
217 | wait(); |
218 | // we need to force-set m_abort to false, because the thread might either have |
219 | // finished between the isRunning() check and the "m_abort = true" above, or the |
220 | // isRunning() check might already happen after the "m_abort = false" in the run() method, |
221 | // either way never resetting m_abort to false from within the run() method |
222 | m_abort = false; |
223 | } |
224 | delete m_rootItem; |
225 | m_rootItem = nullptr; |
226 | } |
227 | |
228 | QHelpContentItem *QHelpContentProvider::takeContentItem() |
229 | { |
230 | QMutexLocker locker(&m_mutex); |
231 | QHelpContentItem *content = m_rootItem; |
232 | m_rootItem = nullptr; |
233 | return content; |
234 | } |
235 | |
236 | // TODO: this is a copy from helpcollectionhandler, make it common |
237 | static QUrl buildQUrl(const QString &ns, const QString &folder, |
238 | const QString &relFileName, const QString &anchor) |
239 | { |
240 | QUrl url; |
241 | url.setScheme(QLatin1String("qthelp" )); |
242 | url.setAuthority(authority: ns); |
243 | url.setPath(path: QLatin1Char('/') + folder + QLatin1Char('/') + relFileName); |
244 | url.setFragment(fragment: anchor); |
245 | return url; |
246 | } |
247 | |
248 | static QUrl constructUrl(const QString &namespaceName, |
249 | const QString &folderName, |
250 | const QString &relativePath) |
251 | { |
252 | const int idx = relativePath.indexOf(c: QLatin1Char('#')); |
253 | const QString &rp = idx < 0 ? relativePath : relativePath.left(n: idx); |
254 | const QString anchor = idx < 0 ? QString() : relativePath.mid(position: idx + 1); |
255 | return buildQUrl(ns: namespaceName, folder: folderName, relFileName: rp, anchor); |
256 | } |
257 | |
258 | void QHelpContentProvider::run() |
259 | { |
260 | QString title; |
261 | QString link; |
262 | int depth = 0; |
263 | QHelpContentItem *item = nullptr; |
264 | |
265 | m_mutex.lock(); |
266 | QHelpContentItem * const rootItem = new QHelpContentItem(QString(), QString(), nullptr); |
267 | const QString currentFilter = m_currentFilter; |
268 | const QStringList attributes = m_filterAttributes; |
269 | const QString collectionFile = m_collectionFile; |
270 | const bool usesFilterEngine = m_usesFilterEngine; |
271 | delete m_rootItem; |
272 | m_rootItem = nullptr; |
273 | m_mutex.unlock(); |
274 | |
275 | if (collectionFile.isEmpty()) |
276 | return; |
277 | |
278 | QHelpCollectionHandler collectionHandler(collectionFile); |
279 | collectionHandler.setReadOnly(true); |
280 | if (!collectionHandler.openCollectionFile()) |
281 | return; |
282 | |
283 | const QList<QHelpCollectionHandler::ContentsData> result = usesFilterEngine |
284 | ? collectionHandler.contentsForFilter(filterName: currentFilter) |
285 | : collectionHandler.contentsForFilter(filterAttributes: attributes); |
286 | |
287 | for (const auto &contentsData : result) { |
288 | m_mutex.lock(); |
289 | if (m_abort) { |
290 | delete rootItem; |
291 | m_abort = false; |
292 | m_mutex.unlock(); |
293 | return; |
294 | } |
295 | m_mutex.unlock(); |
296 | |
297 | const QString namespaceName = contentsData.namespaceName; |
298 | const QString folderName = contentsData.folderName; |
299 | for (const QByteArray &contents : contentsData.contentsList) { |
300 | if (contents.size() < 1) |
301 | continue; |
302 | |
303 | int _depth = 0; |
304 | bool _root = false; |
305 | QStack<QHelpContentItem*> stack; |
306 | |
307 | QDataStream s(contents); |
308 | for (;;) { |
309 | s >> depth; |
310 | s >> link; |
311 | s >> title; |
312 | if (title.isEmpty()) |
313 | break; |
314 | const QUrl url = constructUrl(namespaceName, folderName, relativePath: link); |
315 | CHECK_DEPTH: |
316 | if (depth == 0) { |
317 | m_mutex.lock(); |
318 | item = new QHelpContentItem(title, url, rootItem); |
319 | rootItem->d->appendChild(item); |
320 | m_mutex.unlock(); |
321 | stack.push(t: item); |
322 | _depth = 1; |
323 | _root = true; |
324 | } else { |
325 | if (depth > _depth && _root) { |
326 | _depth = depth; |
327 | stack.push(t: item); |
328 | } |
329 | if (depth == _depth) { |
330 | item = new QHelpContentItem(title, url, stack.top()); |
331 | stack.top()->d->appendChild(item); |
332 | } else if (depth < _depth) { |
333 | stack.pop(); |
334 | --_depth; |
335 | goto CHECK_DEPTH; |
336 | } |
337 | } |
338 | } |
339 | } |
340 | } |
341 | |
342 | m_mutex.lock(); |
343 | m_rootItem = rootItem; |
344 | m_abort = false; |
345 | m_mutex.unlock(); |
346 | } |
347 | |
348 | /*! |
349 | \class QHelpContentModel |
350 | \inmodule QtHelp |
351 | \brief The QHelpContentModel class provides a model that supplies content to views. |
352 | \since 4.4 |
353 | */ |
354 | |
355 | /*! |
356 | \fn void QHelpContentModel::contentsCreationStarted() |
357 | |
358 | This signal is emitted when the creation of the contents has |
359 | started. The current contents are invalid from this point on |
360 | until the signal contentsCreated() is emitted. |
361 | |
362 | \sa isCreatingContents() |
363 | */ |
364 | |
365 | /*! |
366 | \fn void QHelpContentModel::contentsCreated() |
367 | |
368 | This signal is emitted when the contents have been created. |
369 | */ |
370 | |
371 | QHelpContentModel::QHelpContentModel(QHelpEnginePrivate *helpEngine) |
372 | : QAbstractItemModel(helpEngine) |
373 | { |
374 | d = new QHelpContentModelPrivate(); |
375 | d->qhelpContentProvider = new QHelpContentProvider(helpEngine); |
376 | |
377 | connect(sender: d->qhelpContentProvider, signal: &QThread::finished, |
378 | receiver: this, slot: &QHelpContentModel::insertContents); |
379 | } |
380 | |
381 | /*! |
382 | Destroys the help content model. |
383 | */ |
384 | QHelpContentModel::~QHelpContentModel() |
385 | { |
386 | delete d->rootItem; |
387 | delete d; |
388 | } |
389 | |
390 | /*! |
391 | Creates new contents by querying the help system |
392 | for contents specified for the \a customFilterName. |
393 | */ |
394 | void QHelpContentModel::createContents(const QString &customFilterName) |
395 | { |
396 | const bool running = d->qhelpContentProvider->isRunning(); |
397 | d->qhelpContentProvider->collectContents(customFilterName); |
398 | if (running) |
399 | return; |
400 | |
401 | if (d->rootItem) { |
402 | beginResetModel(); |
403 | delete d->rootItem; |
404 | d->rootItem = nullptr; |
405 | endResetModel(); |
406 | } |
407 | emit contentsCreationStarted(); |
408 | } |
409 | |
410 | void QHelpContentModel::insertContents() |
411 | { |
412 | if (d->qhelpContentProvider->isRunning()) |
413 | return; |
414 | |
415 | QHelpContentItem * const newRootItem = d->qhelpContentProvider->takeContentItem(); |
416 | if (!newRootItem) |
417 | return; |
418 | beginResetModel(); |
419 | delete d->rootItem; |
420 | d->rootItem = newRootItem; |
421 | endResetModel(); |
422 | emit contentsCreated(); |
423 | } |
424 | |
425 | /*! |
426 | Returns true if the contents are currently rebuilt, otherwise |
427 | false. |
428 | */ |
429 | bool QHelpContentModel::isCreatingContents() const |
430 | { |
431 | return d->qhelpContentProvider->isRunning(); |
432 | } |
433 | |
434 | /*! |
435 | Returns the help content item at the model index position |
436 | \a index. |
437 | */ |
438 | QHelpContentItem *QHelpContentModel::contentItemAt(const QModelIndex &index) const |
439 | { |
440 | if (index.isValid()) |
441 | return static_cast<QHelpContentItem*>(index.internalPointer()); |
442 | else |
443 | return d->rootItem; |
444 | } |
445 | |
446 | /*! |
447 | Returns the index of the item in the model specified by |
448 | the given \a row, \a column and \a parent index. |
449 | */ |
450 | QModelIndex QHelpContentModel::index(int row, int column, const QModelIndex &parent) const |
451 | { |
452 | if (!d->rootItem) |
453 | return QModelIndex(); |
454 | |
455 | QHelpContentItem *parentItem = contentItemAt(index: parent); |
456 | QHelpContentItem *item = parentItem->child(row); |
457 | if (!item) |
458 | return QModelIndex(); |
459 | return createIndex(arow: row, acolumn: column, adata: item); |
460 | } |
461 | |
462 | /*! |
463 | Returns the parent of the model item with the given |
464 | \a index, or QModelIndex() if it has no parent. |
465 | */ |
466 | QModelIndex QHelpContentModel::parent(const QModelIndex &index) const |
467 | { |
468 | QHelpContentItem *item = contentItemAt(index); |
469 | if (!item) |
470 | return QModelIndex(); |
471 | |
472 | QHelpContentItem *parentItem = static_cast<QHelpContentItem*>(item->parent()); |
473 | if (!parentItem) |
474 | return QModelIndex(); |
475 | |
476 | QHelpContentItem *grandparentItem = static_cast<QHelpContentItem*>(parentItem->parent()); |
477 | if (!grandparentItem) |
478 | return QModelIndex(); |
479 | |
480 | int row = grandparentItem->childPosition(child: parentItem); |
481 | return createIndex(arow: row, acolumn: index.column(), adata: parentItem); |
482 | } |
483 | |
484 | /*! |
485 | Returns the number of rows under the given \a parent. |
486 | */ |
487 | int QHelpContentModel::rowCount(const QModelIndex &parent) const |
488 | { |
489 | QHelpContentItem *parentItem = contentItemAt(index: parent); |
490 | if (!parentItem) |
491 | return 0; |
492 | return parentItem->childCount(); |
493 | } |
494 | |
495 | /*! |
496 | Returns the number of columns under the given \a parent. Currently returns always 1. |
497 | */ |
498 | int QHelpContentModel::columnCount(const QModelIndex &parent) const |
499 | { |
500 | Q_UNUSED(parent); |
501 | |
502 | return 1; |
503 | } |
504 | |
505 | /*! |
506 | Returns the data stored under the given \a role for |
507 | the item referred to by the \a index. |
508 | */ |
509 | QVariant QHelpContentModel::data(const QModelIndex &index, int role) const |
510 | { |
511 | if (role != Qt::DisplayRole) |
512 | return QVariant(); |
513 | |
514 | QHelpContentItem *item = contentItemAt(index); |
515 | if (!item) |
516 | return QVariant(); |
517 | return item->title(); |
518 | } |
519 | |
520 | |
521 | |
522 | /*! |
523 | \class QHelpContentWidget |
524 | \inmodule QtHelp |
525 | \brief The QHelpContentWidget class provides a tree view for displaying help content model items. |
526 | \since 4.4 |
527 | */ |
528 | |
529 | /*! |
530 | \fn void QHelpContentWidget::linkActivated(const QUrl &link) |
531 | |
532 | This signal is emitted when a content item is activated and |
533 | its associated \a link should be shown. |
534 | */ |
535 | |
536 | QHelpContentWidget::QHelpContentWidget() |
537 | : QTreeView(nullptr) |
538 | { |
539 | header()->hide(); |
540 | setUniformRowHeights(true); |
541 | connect(sender: this, signal: &QAbstractItemView::activated, |
542 | receiver: this, slot: &QHelpContentWidget::showLink); |
543 | } |
544 | |
545 | /*! |
546 | Returns the index of the content item with the \a link. |
547 | An invalid index is returned if no such an item exists. |
548 | */ |
549 | QModelIndex QHelpContentWidget::indexOf(const QUrl &link) |
550 | { |
551 | QHelpContentModel *contentModel = qobject_cast<QHelpContentModel*>(object: model()); |
552 | if (!contentModel || link.scheme() != QLatin1String("qthelp" )) |
553 | return QModelIndex(); |
554 | |
555 | m_syncIndex = QModelIndex(); |
556 | for (int i = 0; i < contentModel->rowCount(); ++i) { |
557 | QHelpContentItem *itm = contentModel->contentItemAt(index: contentModel->index(row: i, column: 0)); |
558 | if (itm && itm->url().host() == link.host()) { |
559 | if (searchContentItem(model: contentModel, parent: contentModel->index(row: i, column: 0), path: QDir::cleanPath(path: link.path()))) |
560 | return m_syncIndex; |
561 | } |
562 | } |
563 | return QModelIndex(); |
564 | } |
565 | |
566 | bool QHelpContentWidget::searchContentItem(QHelpContentModel *model, const QModelIndex &parent, |
567 | const QString &cleanPath) |
568 | { |
569 | QHelpContentItem *parentItem = model->contentItemAt(index: parent); |
570 | if (!parentItem) |
571 | return false; |
572 | |
573 | if (QDir::cleanPath(path: parentItem->url().path()) == cleanPath) { |
574 | m_syncIndex = parent; |
575 | return true; |
576 | } |
577 | |
578 | for (int i = 0; i < parentItem->childCount(); ++i) { |
579 | if (searchContentItem(model, parent: model->index(row: i, column: 0, parent), cleanPath)) |
580 | return true; |
581 | } |
582 | return false; |
583 | } |
584 | |
585 | void QHelpContentWidget::showLink(const QModelIndex &index) |
586 | { |
587 | QHelpContentModel *contentModel = qobject_cast<QHelpContentModel*>(object: model()); |
588 | if (!contentModel) |
589 | return; |
590 | |
591 | QHelpContentItem *item = contentModel->contentItemAt(index); |
592 | if (!item) |
593 | return; |
594 | QUrl url = item->url(); |
595 | if (url.isValid()) |
596 | emit linkActivated(link: url); |
597 | } |
598 | |
599 | QT_END_NAMESPACE |
600 | |
601 | #include "qhelpcontentwidget.moc" |
602 | |