Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
itemviewfindwidget.cpp
Go to the documentation of this file.
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// Qt-Security score:significant reason:default
4
5/*! \class ItemViewFindWidget
6
7 \brief A search bar that is commonly added below the searchable item view.
8
9 \internal
10
11 This widget implements a search bar which becomes visible when the user
12 wants to start searching. It is a modern replacement for the commonly used
13 search dialog. It is usually placed below a QAbstractItemView using a QVBoxLayout.
14
15 The QAbstractItemView instance will need to be associated with this class using
16 setItemView().
17
18 The search is incremental and can be set to case sensitive or whole words
19 using buttons available on the search bar.
20
21 The item traversal order should fit QTreeView, QTableView and QListView alike.
22 More complex tree structures will work as well, assuming the branch structure
23 is painted left to the items, without crossing lines.
24
25 \sa QAbstractItemView
26 */
27
29
30#include <QtWidgets/qabstractitemview.h>
31#include <QtWidgets/qcheckbox.h>
32#include <QtWidgets/qtreeview.h>
33
34#include <QtCore/qregularexpression.h>
35
36#include <algorithm>
37
38QT_BEGIN_NAMESPACE
39
40using namespace Qt::StringLiterals;
41
42/*!
43 Constructs a ItemViewFindWidget.
44
45 \a flags is passed to the AbstractFindWidget constructor.
46 \a parent is passed to the QWidget constructor.
47 */
48ItemViewFindWidget::ItemViewFindWidget(FindFlags flags, QWidget *parent)
49 : AbstractFindWidget(flags, parent)
50 , m_itemView(0)
51{
52}
53
54/*!
55 Associates a QAbstractItemView with this find widget. Searches done using this find
56 widget will then apply to the given QAbstractItemView.
57
58 An event filter is set on the QAbstractItemView which intercepts the ESC key while
59 the find widget is active, and uses it to deactivate the find widget.
60
61 If the find widget is already associated with a QAbstractItemView, the event filter
62 is removed from this QAbstractItemView first.
63
64 \a itemView may be NULL.
65 */
66void ItemViewFindWidget::setItemView(QAbstractItemView *itemView)
67{
68 if (m_itemView)
69 m_itemView->removeEventFilter(this);
70
71 m_itemView = itemView;
72
73 if (m_itemView)
74 m_itemView->installEventFilter(this);
75}
76
77/*!
78 \reimp
79 */
81{
82 if (m_itemView)
83 m_itemView->setFocus();
84
86}
87
88// Sorting is needed to find the start/end of the selection.
89// This is utter black magic. And it is damn slow.
90static bool indexLessThan(const QModelIndex &a, const QModelIndex &b)
91{
92 // First determine the nesting of each index in the tree.
93 QModelIndex aa = a;
94 int aDepth = 0;
95 while (aa.parent() != QModelIndex()) {
96 // As a side effect, check if one of the items is the parent of the other.
97 // Children are always displayed below their parents, so sort them further down.
98 if (aa.parent() == b)
99 return true;
100 aa = aa.parent();
101 aDepth++;
102 }
103 QModelIndex ba = b;
104 int bDepth = 0;
105 while (ba.parent() != QModelIndex()) {
106 if (ba.parent() == a)
107 return false;
108 ba = ba.parent();
109 bDepth++;
110 }
111 // Now find indices at comparable depth.
112 for (aa = a; aDepth > bDepth; aDepth--)
113 aa = aa.parent();
114 for (ba = b; aDepth < bDepth; bDepth--)
115 ba = ba.parent();
116 // If they have the same parent, sort them within a top-to-bottom, left-to-right rectangle.
117 if (aa.parent() == ba.parent()) {
118 if (aa.row() < ba.row())
119 return true;
120 if (aa.row() > ba.row())
121 return false;
122 return aa.column() < ba.column();
123 }
124 // Now try to find indices that have the same grandparent. This ends latest at the root node.
125 while (aa.parent().parent() != ba.parent().parent()) {
126 aa = aa.parent();
127 ba = ba.parent();
128 }
129 // A bigger row is always displayed further down.
130 if (aa.parent().row() < ba.parent().row())
131 return true;
132 if (aa.parent().row() > ba.parent().row())
133 return false;
134 // Here's the trick: a child spawned from a bigger column is displayed further *up*.
135 // That's because the tree lines are on the left and are supposed not to cross each other.
136 // This case is mostly academical, as "all" models spawn children from the first column.
137 return aa.parent().column() > ba.parent().column();
138}
139
140/*!
141 \reimp
142 */
143void ItemViewFindWidget::find(const QString &ttf, bool skipCurrent, bool backward, bool *found, bool *wrapped)
144{
145 if (!m_itemView || !m_itemView->model()->hasChildren())
146 return;
147
148 QModelIndex idx;
149 if (skipCurrent && m_itemView->selectionModel()->hasSelection()) {
150 QModelIndexList il = m_itemView->selectionModel()->selectedIndexes();
151 std::sort(il.begin(), il.end(), indexLessThan);
152 idx = backward ? il.first() : il.last();
153 } else {
154 idx = m_itemView->currentIndex();
155 }
156
157 *found = true;
158 QModelIndex newIdx = idx;
159
160 if (!ttf.isEmpty()) {
161 if (newIdx.isValid()) {
162 int column = newIdx.column();
163 if (skipCurrent)
164 if (QTreeView *tv = qobject_cast<QTreeView *>(m_itemView))
165 if (tv->allColumnsShowFocus())
166 column = backward ? 0 : m_itemView->model()->columnCount(newIdx.parent()) - 1;
167 newIdx = findHelper(ttf, skipCurrent, backward,
168 newIdx.parent(), newIdx.row(), column);
169 }
170 if (!newIdx.isValid()) {
171 int row = backward ? m_itemView->model()->rowCount() : 0;
172 int column = backward ? 0 : -1;
173 newIdx = findHelper(ttf, true, backward, m_itemView->rootIndex(), row, column);
174 if (!newIdx.isValid()) {
175 *found = false;
176 newIdx = idx;
177 } else {
178 *wrapped = true;
179 }
180 }
181 }
182
183 if (!isVisible())
184 show();
185
186 m_itemView->setCurrentIndex(newIdx);
187}
188
189// You are not expected to understand the following two functions.
190// The traversal order is described in the indexLessThan() comments above.
191
192static inline bool skipForward(const QAbstractItemModel *model, QModelIndex &parent, int &row, int &column)
193{
194 forever {
195 column++;
196 if (column < model->columnCount(parent))
197 return true;
198 forever {
199 while (--column >= 0) {
200 QModelIndex nIdx = model->index(row, column, parent);
201 if (nIdx.isValid()) {
202 if (model->hasChildren(nIdx)) {
203 row = 0;
204 column = 0;
205 parent = nIdx;
206 return true;
207 }
208 }
209 }
210 if (++row < model->rowCount(parent))
211 break;
212 if (!parent.isValid())
213 return false;
214 row = parent.row();
215 column = parent.column();
216 parent = parent.parent();
217 }
218 }
219}
220
221static inline bool skipBackward(const QAbstractItemModel *model, QModelIndex &parent, int &row, int &column)
222{
223 column--;
224 if (column == -1) {
225 if (--row < 0) {
226 if (!parent.isValid())
227 return false;
228 row = parent.row();
229 column = parent.column();
230 parent = parent.parent();
231 }
232 while (++column < model->columnCount(parent)) {
233 QModelIndex nIdx = model->index(row, column, parent);
234 if (nIdx.isValid()) {
235 if (model->hasChildren(nIdx)) {
236 row = model->rowCount(nIdx) - 1;
237 column = -1;
238 parent = nIdx;
239 }
240 }
241 }
242 column--;
243 }
244 return true;
245}
246
247// QAbstractItemModel::match() does not support backwards searching. Still using it would
248// be just a bit inefficient (not much worse than when no match is found).
249// The bigger problem is that QAbstractItemView does not provide a method to sort a
250// set of indices in traversal order (to find the start and end of the selection).
251// Consequently, we do everything by ourselves to be consistent. Of course, this puts
252// constraints on the allowable visualizations.
253QModelIndex ItemViewFindWidget::findHelper(const QString &textToFind, bool skipCurrent, bool backward,
254 QModelIndex parent, int row, int column)
255{
256 const QAbstractItemModel *model = m_itemView->model();
257 forever {
258 if (skipCurrent) {
259 if (backward) {
260 if (!skipBackward(model, parent, row, column))
261 return QModelIndex();
262 } else {
263 if (!skipForward(model, parent, row, column))
264 return QModelIndex();
265 }
266 }
267
268 QModelIndex idx = model->index(row, column, parent);
269 if (idx.isValid()) {
270 Qt::CaseSensitivity cs = caseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive;
271
272 if (wholeWords()) {
273 QString rx = "\\b"_L1 + QRegularExpression::escape(textToFind)
274 + "\\b"_L1;
275 QRegularExpression re(rx);
276 if (cs == Qt::CaseInsensitive)
277 re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
278 if (idx.data().toString().indexOf(re) >= 0)
279 return idx;
280 } else {
281 if (idx.data().toString().indexOf(textToFind, 0, cs) >= 0)
282 return idx;
283 }
284 }
285
286 skipCurrent = true;
287 }
288}
289
290QT_END_NAMESPACE
A search bar that is commonly added below a searchable widget.
virtual void deactivate()
Deactivates the find widget, making it invisible and handing focus to any associated QTextEdit.
A search bar that is commonly added below the searchable item view.
void setItemView(QAbstractItemView *itemView)
Associates a QAbstractItemView with this find widget.
void find(const QString &textToFind, bool skipCurrent, bool backward, bool *found, bool *wrapped) override
\reimp
void deactivate() override
\reimp
static bool skipForward(const QAbstractItemModel *model, QModelIndex &parent, int &row, int &column)
static bool skipBackward(const QAbstractItemModel *model, QModelIndex &parent, int &row, int &column)
static bool indexLessThan(const QModelIndex &a, const QModelIndex &b)