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

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