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