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

source code of qttools/src/shared/findwidget/itemviewfindwidget.cpp