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