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 "qhelplink.h" |
7 | |
8 | #if QT_CONFIG(future) |
9 | #include <QtCore/qfuturewatcher.h> |
10 | #endif |
11 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | class QHelpIndexModelPrivate |
15 | { |
16 | #if QT_CONFIG(future) |
17 | using FutureProvider = std::function<QFuture<QStringList>()>; |
18 | |
19 | struct WatcherDeleter |
20 | { |
21 | void operator()(QFutureWatcherBase *watcher) { |
22 | watcher->disconnect(); |
23 | watcher->cancel(); |
24 | watcher->waitForFinished(); |
25 | delete watcher; |
26 | } |
27 | }; |
28 | #endif |
29 | |
30 | public: |
31 | #if QT_CONFIG(future) |
32 | void createIndex(const FutureProvider &futureProvider); |
33 | #endif |
34 | |
35 | QHelpIndexModel *q = nullptr; |
36 | QHelpEngineCore *helpEngine = nullptr; |
37 | QStringList indices = {}; |
38 | #if QT_CONFIG(future) |
39 | std::unique_ptr<QFutureWatcher<QStringList>, WatcherDeleter> watcher = {}; |
40 | #endif |
41 | }; |
42 | |
43 | #if QT_CONFIG(future) |
44 | void QHelpIndexModelPrivate::createIndex(const FutureProvider &futureProvider) |
45 | { |
46 | const bool wasRunning = bool(watcher); |
47 | watcher.reset(p: new QFutureWatcher<QStringList>); |
48 | QObject::connect(sender: watcher.get(), signal: &QFutureWatcherBase::finished, context: q, slot: [this] { |
49 | if (!watcher->isCanceled()) { |
50 | indices = watcher->result(); |
51 | q->filter(filter: {}); |
52 | } |
53 | watcher.release()->deleteLater(); |
54 | emit q->indexCreated(); |
55 | }); |
56 | watcher->setFuture(futureProvider()); |
57 | |
58 | if (wasRunning) |
59 | return; |
60 | |
61 | indices.clear(); |
62 | q->filter(filter: {}); |
63 | emit q->indexCreationStarted(); |
64 | } |
65 | #endif |
66 | |
67 | /*! |
68 | \class QHelpIndexModel |
69 | \since 4.4 |
70 | \inmodule QtHelp |
71 | \brief The QHelpIndexModel class provides a model that |
72 | supplies index keywords to views. |
73 | */ |
74 | |
75 | /*! |
76 | \fn void QHelpIndexModel::indexCreationStarted() |
77 | |
78 | This signal is emitted when the creation of a new index |
79 | has started. The current index is invalid from this |
80 | point on until the signal indexCreated() is emitted. |
81 | |
82 | \sa isCreatingIndex() |
83 | */ |
84 | |
85 | /*! |
86 | \fn void QHelpIndexModel::indexCreated() |
87 | |
88 | This signal is emitted when the index has been created. |
89 | */ |
90 | |
91 | QHelpIndexModel::QHelpIndexModel(QHelpEngineCore *helpEngine) |
92 | : QStringListModel(helpEngine) |
93 | , d(new QHelpIndexModelPrivate{.q: this, .helpEngine: helpEngine}) |
94 | {} |
95 | |
96 | QHelpIndexModel::~QHelpIndexModel() |
97 | { |
98 | delete d; |
99 | } |
100 | |
101 | /*! |
102 | \since 6.8 |
103 | |
104 | Creates a new index by querying the help system for keywords for the current filter. |
105 | */ |
106 | void QHelpIndexModel::createIndexForCurrentFilter() |
107 | { |
108 | #if QT_CONFIG(future) |
109 | d->createIndex(futureProvider: [this] { return d->helpEngine->requestIndexForCurrentFilter(); }); |
110 | #endif |
111 | } |
112 | |
113 | /*! |
114 | Creates a new index by querying the help system for |
115 | keywords for the specified custom \a filter name. |
116 | */ |
117 | void QHelpIndexModel::createIndex(const QString &filter) |
118 | { |
119 | #if QT_CONFIG(future) |
120 | d->createIndex(futureProvider: [this, filter] { return d->helpEngine->requestIndex(filter); }); |
121 | #endif |
122 | } |
123 | |
124 | // TODO: Remove me |
125 | void QHelpIndexModel::insertIndices() |
126 | {} |
127 | |
128 | /*! |
129 | Returns true if the index is currently built up, otherwise |
130 | false. |
131 | */ |
132 | bool QHelpIndexModel::isCreatingIndex() const |
133 | { |
134 | #if QT_CONFIG(future) |
135 | return bool(d->watcher); |
136 | #else |
137 | return false; |
138 | #endif |
139 | } |
140 | |
141 | /*! |
142 | \since 5.15 |
143 | |
144 | Returns the associated help engine that manages this model. |
145 | */ |
146 | QHelpEngineCore *QHelpIndexModel::helpEngine() const |
147 | { |
148 | return d->helpEngine; |
149 | } |
150 | |
151 | /*! |
152 | Filters the indices and returns the model index of the best |
153 | matching keyword. In a first step, only the keywords containing |
154 | \a filter are kept in the model's index list. Analogously, if |
155 | \a wildcard is not empty, only the keywords matched are left |
156 | in the index list. In a second step, the best match is |
157 | determined and its index model returned. When specifying a |
158 | wildcard expression, the \a filter string is used to |
159 | search for the best match. |
160 | */ |
161 | QModelIndex QHelpIndexModel::filter(const QString &filter, const QString &wildcard) |
162 | { |
163 | if (filter.isEmpty()) { |
164 | setStringList(d->indices); |
165 | return index(row: -1, column: 0, parent: {}); |
166 | } |
167 | |
168 | using Checker = std::function<bool(const QString &)>; |
169 | const auto checkIndices = [this, filter](const Checker &checker) { |
170 | QStringList filteredList; |
171 | int goodMatch = -1; |
172 | int perfectMatch = -1; |
173 | for (const QString &index : std::as_const(t&: d->indices)) { |
174 | if (checker(index)) { |
175 | filteredList.append(t: index); |
176 | if (perfectMatch == -1 && index.startsWith(s: filter, cs: Qt::CaseInsensitive)) { |
177 | if (goodMatch == -1) |
178 | goodMatch = filteredList.size() - 1; |
179 | if (filter.size() == index.size()) |
180 | perfectMatch = filteredList.size() - 1; |
181 | } else if (perfectMatch > -1 && index == filter) { |
182 | perfectMatch = filteredList.size() - 1; |
183 | } |
184 | } |
185 | } |
186 | setStringList(filteredList); |
187 | return perfectMatch >= 0 ? perfectMatch : qMax(a: 0, b: goodMatch); |
188 | }; |
189 | |
190 | int perfectMatch = -1; |
191 | if (!wildcard.isEmpty()) { |
192 | const auto re = QRegularExpression::wildcardToRegularExpression(str: wildcard, |
193 | options: QRegularExpression::UnanchoredWildcardConversion); |
194 | const QRegularExpression regExp(re, QRegularExpression::CaseInsensitiveOption); |
195 | perfectMatch = checkIndices([regExp](const QString &index) { |
196 | return index.contains(re: regExp); |
197 | }); |
198 | } else { |
199 | perfectMatch = checkIndices([filter](const QString &index) { |
200 | return index.contains(s: filter, cs: Qt::CaseInsensitive); |
201 | }); |
202 | } |
203 | return index(row: perfectMatch, column: 0, parent: {}); |
204 | } |
205 | |
206 | /*! |
207 | \class QHelpIndexWidget |
208 | \inmodule QtHelp |
209 | \since 4.4 |
210 | \brief The QHelpIndexWidget class provides a list view |
211 | displaying the QHelpIndexModel. |
212 | */ |
213 | |
214 | /*! |
215 | \fn void QHelpIndexWidget::linkActivated(const QUrl &link, |
216 | const QString &keyword) |
217 | |
218 | \deprecated |
219 | |
220 | Use documentActivated() instead. |
221 | |
222 | This signal is emitted when an item is activated and its |
223 | associated \a link should be shown. To know where the link |
224 | belongs to, the \a keyword is given as a second parameter. |
225 | */ |
226 | |
227 | /*! |
228 | \fn void QHelpIndexWidget::documentActivated(const QHelpLink &document, |
229 | const QString &keyword) |
230 | |
231 | \since 5.15 |
232 | |
233 | This signal is emitted when an item is activated and its |
234 | associated \a document should be shown. To know where the link |
235 | belongs to, the \a keyword is given as a second parameter. |
236 | */ |
237 | |
238 | /*! |
239 | \fn void QHelpIndexWidget::documentsActivated(const QList<QHelpLink> &documents, |
240 | const QString &keyword) |
241 | |
242 | \since 5.15 |
243 | |
244 | This signal is emitted when the item representing the \a keyword |
245 | is activated and the item has more than one document associated. |
246 | The \a documents consist of the document titles and their URLs. |
247 | */ |
248 | |
249 | QHelpIndexWidget::QHelpIndexWidget() |
250 | { |
251 | setEditTriggers(QAbstractItemView::NoEditTriggers); |
252 | setUniformItemSizes(true); |
253 | connect(sender: this, signal: &QAbstractItemView::activated, context: this, slot: &QHelpIndexWidget::showLink); |
254 | } |
255 | |
256 | void QHelpIndexWidget::showLink(const QModelIndex &index) |
257 | { |
258 | if (!index.isValid()) |
259 | return; |
260 | |
261 | QHelpIndexModel *indexModel = qobject_cast<QHelpIndexModel*>(object: model()); |
262 | if (!indexModel) |
263 | return; |
264 | |
265 | const QVariant &v = indexModel->data(index, role: Qt::DisplayRole); |
266 | const QString name = v.isValid() ? v.toString() : QString(); |
267 | |
268 | const QList<QHelpLink> &docs = indexModel->helpEngine()->documentsForKeyword(keyword: name); |
269 | if (docs.size() > 1) { |
270 | emit documentsActivated(documents: docs, keyword: name); |
271 | #if QT_DEPRECATED_SINCE(5, 15) |
272 | QT_WARNING_PUSH |
273 | QT_WARNING_DISABLE_DEPRECATED |
274 | QMultiMap<QString, QUrl> links; |
275 | for (const auto &doc : docs) |
276 | links.insert(key: doc.title, value: doc.url); |
277 | emit linksActivated(links, keyword: name); |
278 | QT_WARNING_POP |
279 | #endif |
280 | } else if (!docs.isEmpty()) { |
281 | emit documentActivated(document: docs.first(), keyword: name); |
282 | #if QT_DEPRECATED_SINCE(5, 15) |
283 | QT_WARNING_PUSH |
284 | QT_WARNING_DISABLE_DEPRECATED |
285 | emit linkActivated(link: docs.first().url, keyword: name); |
286 | QT_WARNING_POP |
287 | #endif |
288 | } |
289 | } |
290 | |
291 | /*! |
292 | Activates the current item which will result eventually in |
293 | the emitting of a linkActivated() or linksActivated() |
294 | signal. |
295 | */ |
296 | void QHelpIndexWidget::activateCurrentItem() |
297 | { |
298 | showLink(index: currentIndex()); |
299 | } |
300 | |
301 | /*! |
302 | Filters the indices according to \a filter or \a wildcard. |
303 | The item with the best match is set as current item. |
304 | |
305 | \sa QHelpIndexModel::filter() |
306 | */ |
307 | void QHelpIndexWidget::filterIndices(const QString &filter, const QString &wildcard) |
308 | { |
309 | QHelpIndexModel *indexModel = qobject_cast<QHelpIndexModel *>(object: model()); |
310 | if (!indexModel) |
311 | return; |
312 | const QModelIndex &idx = indexModel->filter(filter, wildcard); |
313 | if (idx.isValid()) |
314 | setCurrentIndex(idx); |
315 | } |
316 | |
317 | QT_END_NAMESPACE |
318 | |