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