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 "qhelpindexwidget.h"
41 #include "qhelpenginecore.h"
42 #include "qhelpengine_p.h"
43 #include "qhelpdbreader_p.h"
44 #include "qhelpcollectionhandler_p.h"
45 
46 #include <QtCore/QThread>
47 #include <QtCore/QMutex>
48 #include <QtHelp/QHelpLink>
49 #include <QtWidgets/QListView>
50 #include <QtWidgets/QHeaderView>
51 
52 #include <algorithm>
53 
54 QT_BEGIN_NAMESPACE
55 
56 class QHelpIndexProvider : public QThread
57 {
58 public:
59     QHelpIndexProvider(QHelpEnginePrivate *helpEngine);
60     ~QHelpIndexProvider() override;
61     void collectIndices(const QString &customFilterName);
62     void stopCollecting();
63     QStringList indices() const;
64 
65 private:
66     void run() override;
67 
68     QHelpEnginePrivate *m_helpEngine;
69     QString m_currentFilter;
70     QStringList m_filterAttributes;
71     QStringList m_indices;
72     mutable QMutex m_mutex;
73 };
74 
75 class QHelpIndexModelPrivate
76 {
77 public:
QHelpIndexModelPrivate(QHelpEnginePrivate * hE)78     QHelpIndexModelPrivate(QHelpEnginePrivate *hE)
79         : helpEngine(hE),
80           indexProvider(new QHelpIndexProvider(helpEngine))
81     {
82     }
83 
84     QHelpEnginePrivate *helpEngine;
85     QHelpIndexProvider *indexProvider;
86     QStringList indices;
87 };
88 
QHelpIndexProvider(QHelpEnginePrivate * helpEngine)89 QHelpIndexProvider::QHelpIndexProvider(QHelpEnginePrivate *helpEngine)
90     : QThread(helpEngine),
91       m_helpEngine(helpEngine)
92 {
93 }
94 
~QHelpIndexProvider()95 QHelpIndexProvider::~QHelpIndexProvider()
96 {
97     stopCollecting();
98 }
99 
collectIndices(const QString & customFilterName)100 void QHelpIndexProvider::collectIndices(const QString &customFilterName)
101 {
102     m_mutex.lock();
103     m_currentFilter = customFilterName;
104     m_filterAttributes = m_helpEngine->q->filterAttributes(customFilterName);
105     m_mutex.unlock();
106 
107     if (isRunning())
108         stopCollecting();
109     start(LowPriority);
110 }
111 
stopCollecting()112 void QHelpIndexProvider::stopCollecting()
113 {
114     if (!isRunning())
115         return;
116     wait();
117 }
118 
indices() const119 QStringList QHelpIndexProvider::indices() const
120 {
121     QMutexLocker lck(&m_mutex);
122     return m_indices;
123 }
124 
run()125 void QHelpIndexProvider::run()
126 {
127     m_mutex.lock();
128     const QString currentFilter = m_currentFilter;
129     const QStringList attributes = m_filterAttributes;
130     const QString collectionFile = m_helpEngine->collectionHandler->collectionFile();
131     m_indices = QStringList();
132     m_mutex.unlock();
133 
134     if (collectionFile.isEmpty())
135         return;
136 
137     QHelpCollectionHandler collectionHandler(collectionFile);
138     collectionHandler.setReadOnly(true);
139     if (!collectionHandler.openCollectionFile())
140         return;
141 
142     const QStringList result = m_helpEngine->usesFilterEngine
143             ? collectionHandler.indicesForFilter(currentFilter)
144             : collectionHandler.indicesForFilter(attributes);
145 
146     m_mutex.lock();
147     m_indices = result;
148     m_mutex.unlock();
149 }
150 
151 /*!
152     \class QHelpIndexModel
153     \since 4.4
154     \inmodule QtHelp
155     \brief The QHelpIndexModel class provides a model that
156     supplies index keywords to views.
157 
158 
159 */
160 
161 /*!
162     \fn void QHelpIndexModel::indexCreationStarted()
163 
164     This signal is emitted when the creation of a new index
165     has started. The current index is invalid from this
166     point on until the signal indexCreated() is emitted.
167 
168     \sa isCreatingIndex()
169 */
170 
171 /*!
172     \fn void QHelpIndexModel::indexCreated()
173 
174     This signal is emitted when the index has been created.
175 */
176 
QHelpIndexModel(QHelpEnginePrivate * helpEngine)177 QHelpIndexModel::QHelpIndexModel(QHelpEnginePrivate *helpEngine)
178     : QStringListModel(helpEngine)
179 {
180     d = new QHelpIndexModelPrivate(helpEngine);
181 
182     connect(d->indexProvider, &QThread::finished,
183             this, &QHelpIndexModel::insertIndices);
184 }
185 
~QHelpIndexModel()186 QHelpIndexModel::~QHelpIndexModel()
187 {
188     delete d;
189 }
190 
191 /*!
192     Creates a new index by querying the help system for
193     keywords for the specified \a customFilterName.
194 */
createIndex(const QString & customFilterName)195 void QHelpIndexModel::createIndex(const QString &customFilterName)
196 {
197     const bool running = d->indexProvider->isRunning();
198     d->indexProvider->collectIndices(customFilterName);
199     if (running)
200         return;
201 
202     d->indices = QStringList();
203     filter(QString());
204     emit indexCreationStarted();
205 }
206 
insertIndices()207 void QHelpIndexModel::insertIndices()
208 {
209     if (d->indexProvider->isRunning())
210         return;
211 
212     d->indices = d->indexProvider->indices();
213     filter(QString());
214     emit indexCreated();
215 }
216 
217 /*!
218     Returns true if the index is currently built up, otherwise
219     false.
220 */
isCreatingIndex() const221 bool QHelpIndexModel::isCreatingIndex() const
222 {
223     return d->indexProvider->isRunning();
224 }
225 
226 /*!
227     \since 5.15
228 
229     Returns the associated help engine that manages this model.
230 */
helpEngine() const231 QHelpEngineCore *QHelpIndexModel::helpEngine() const
232 {
233     return d->helpEngine->q;
234 }
235 
236 #if QT_DEPRECATED_SINCE(5, 15)
237 /*!
238     \obsolete
239     Use QHelpEngineCore::documentsForKeyword() instead.
240 */
241 QT_WARNING_PUSH
242 QT_WARNING_DISABLE_DEPRECATED
linksForKeyword(const QString & keyword) const243 QMap<QString, QUrl> QHelpIndexModel::linksForKeyword(const QString &keyword) const
244 {
245     return d->helpEngine->q->linksForKeyword(keyword);
246 }
247 QT_WARNING_POP
248 #endif
249 
250 /*!
251     Filters the indices and returns the model index of the best
252     matching keyword. In a first step, only the keywords containing
253     \a filter are kept in the model's index list. Analogously, if
254     \a wildcard is not empty, only the keywords matched are left
255     in the index list. In a second step, the best match is
256     determined and its index model returned. When specifying a
257     wildcard expression, the \a filter string is used to
258     search for the best match.
259 */
filter(const QString & filter,const QString & wildcard)260 QModelIndex QHelpIndexModel::filter(const QString &filter, const QString &wildcard)
261 {
262     if (filter.isEmpty()) {
263         setStringList(d->indices);
264         return index(-1, 0, QModelIndex());
265     }
266 
267     QStringList lst;
268     int goodMatch = -1;
269     int perfectMatch = -1;
270 
271     if (!wildcard.isEmpty()) {
272         const QRegExp regExp(wildcard, Qt::CaseInsensitive, QRegExp::Wildcard);
273         for (const QString &index : qAsConst(d->indices)) {
274             if (index.contains(regExp)) {
275                 lst.append(index);
276                 if (perfectMatch == -1 && index.startsWith(filter, Qt::CaseInsensitive)) {
277                     if (goodMatch == -1)
278                         goodMatch = lst.count() - 1;
279                     if (filter.length() == index.length()){
280                         perfectMatch = lst.count() - 1;
281                     }
282                 } else if (perfectMatch > -1 && index == filter) {
283                     perfectMatch = lst.count() - 1;
284                 }
285             }
286         }
287     } else {
288         for (const QString &index : qAsConst(d->indices)) {
289             if (index.contains(filter, Qt::CaseInsensitive)) {
290                 lst.append(index);
291                 if (perfectMatch == -1 && index.startsWith(filter, Qt::CaseInsensitive)) {
292                     if (goodMatch == -1)
293                         goodMatch = lst.count() - 1;
294                     if (filter.length() == index.length()){
295                         perfectMatch = lst.count() - 1;
296                     }
297                 } else if (perfectMatch > -1 && index == filter) {
298                     perfectMatch = lst.count() - 1;
299                 }
300             }
301         }
302 
303     }
304 
305     if (perfectMatch == -1)
306         perfectMatch = qMax(0, goodMatch);
307 
308     setStringList(lst);
309     return index(perfectMatch, 0, QModelIndex());
310 }
311 
312 
313 
314 /*!
315     \class QHelpIndexWidget
316     \inmodule QtHelp
317     \since 4.4
318     \brief The QHelpIndexWidget class provides a list view
319     displaying the QHelpIndexModel.
320 */
321 
322 /*!
323     \fn void QHelpIndexWidget::linkActivated(const QUrl &link,
324         const QString &keyword)
325 
326     \obsolete
327 
328     Use documentActivated() instead.
329 
330     This signal is emitted when an item is activated and its
331     associated \a link should be shown. To know where the link
332     belongs to, the \a keyword is given as a second parameter.
333 */
334 
335 /*!
336     \fn void QHelpIndexWidget::linksActivated(const QMap<QString, QUrl> &links,
337         const QString &keyword)
338 
339     \obsolete
340 
341     Use documentsActivated() instead.
342 
343     This signal is emitted when the item representing the \a keyword
344     is activated and the item has more than one link associated.
345     The \a links consist of the document titles and their URLs.
346 */
347 
348 /*!
349     \fn void QHelpIndexWidget::documentActivated(const QHelpLink &document,
350         const QString &keyword)
351 
352     \since 5.15
353 
354     This signal is emitted when an item is activated and its
355     associated \a document should be shown. To know where the link
356     belongs to, the \a keyword is given as a second parameter.
357 */
358 
359 /*!
360     \fn void QHelpIndexWidget::documentsActivated(const QList<QHelpLink> &documents,
361         const QString &keyword)
362 
363     \since 5.15
364 
365     This signal is emitted when the item representing the \a keyword
366     is activated and the item has more than one document associated.
367     The \a documents consist of the document titles and their URLs.
368 */
369 
QHelpIndexWidget()370 QHelpIndexWidget::QHelpIndexWidget()
371     : QListView(nullptr)
372 {
373     setEditTriggers(QAbstractItemView::NoEditTriggers);
374     setUniformItemSizes(true);
375     connect(this, &QAbstractItemView::activated,
376             this, &QHelpIndexWidget::showLink);
377 }
378 
showLink(const QModelIndex & index)379 void QHelpIndexWidget::showLink(const QModelIndex &index)
380 {
381     if (!index.isValid())
382         return;
383 
384     QHelpIndexModel *indexModel = qobject_cast<QHelpIndexModel*>(model());
385     if (!indexModel)
386         return;
387 
388     const QVariant &v = indexModel->data(index, Qt::DisplayRole);
389     const QString name = v.isValid() ? v.toString() : QString();
390 
391     const QList<QHelpLink> &docs = indexModel->helpEngine()->documentsForKeyword(name);
392     if (docs.count() > 1) {
393         emit documentsActivated(docs, name);
394         QT_WARNING_PUSH
395         QT_WARNING_DISABLE_DEPRECATED
396         QMap<QString, QUrl> links;
397         for (const auto &doc : docs)
398             static_cast<QMultiMap<QString, QUrl> &>(links).insert(doc.title, doc.url);
399         emit linksActivated(links, name);
400         QT_WARNING_POP
401     } else if (!docs.isEmpty()) {
402         emit documentActivated(docs.first(), name);
403         QT_WARNING_PUSH
404         QT_WARNING_DISABLE_DEPRECATED
405         emit linkActivated(docs.first().url, name);
406         QT_WARNING_POP
407     }
408 }
409 
410 /*!
411     Activates the current item which will result eventually in
412     the emitting of a linkActivated() or linksActivated()
413     signal.
414 */
activateCurrentItem()415 void QHelpIndexWidget::activateCurrentItem()
416 {
417     showLink(currentIndex());
418 }
419 
420 /*!
421     Filters the indices according to \a filter or \a wildcard.
422     The item with the best match is set as current item.
423 
424     \sa QHelpIndexModel::filter()
425 */
filterIndices(const QString & filter,const QString & wildcard)426 void QHelpIndexWidget::filterIndices(const QString &filter, const QString &wildcard)
427 {
428     QHelpIndexModel *indexModel = qobject_cast<QHelpIndexModel*>(model());
429     if (!indexModel)
430         return;
431     const QModelIndex &idx = indexModel->filter(filter, wildcard);
432     if (idx.isValid())
433         setCurrentIndex(idx);
434 }
435 
436 QT_END_NAMESPACE
437