1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2020 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtWidgets module 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 | #include <qglobal.h> |
41 | #include "qcolumnview.h" |
42 | |
43 | #if QT_CONFIG(columnview) |
44 | |
45 | #include "qcolumnview_p.h" |
46 | #include "qcolumnviewgrip_p.h" |
47 | |
48 | #include <qlistview.h> |
49 | #include <qabstractitemdelegate.h> |
50 | #include <qscrollbar.h> |
51 | #include <qpainter.h> |
52 | #include <qdebug.h> |
53 | |
54 | QT_BEGIN_NAMESPACE |
55 | |
56 | /*! |
57 | \since 4.3 |
58 | \class QColumnView |
59 | \brief The QColumnView class provides a model/view implementation of a column view. |
60 | \ingroup model-view |
61 | \ingroup advanced |
62 | \inmodule QtWidgets |
63 | |
64 | QColumnView displays a model in a number of QListViews, one for each |
65 | hierarchy in the tree. This is sometimes referred to as a cascading list. |
66 | |
67 | The QColumnView class is one of the \l{Model/View Classes} |
68 | and is part of Qt's \l{Model/View Programming}{model/view framework}. |
69 | |
70 | QColumnView implements the interfaces defined by the |
71 | QAbstractItemView class to allow it to display data provided by |
72 | models derived from the QAbstractItemModel class. |
73 | |
74 | \image qcolumnview.png |
75 | |
76 | \sa {Model/View Programming} |
77 | */ |
78 | |
79 | /*! |
80 | Constructs a column view with a \a parent to represent a model's |
81 | data. Use setModel() to set the model. |
82 | |
83 | \sa QAbstractItemModel |
84 | */ |
85 | QColumnView::QColumnView(QWidget * parent) |
86 | : QAbstractItemView(*new QColumnViewPrivate, parent) |
87 | { |
88 | Q_D(QColumnView); |
89 | d->initialize(); |
90 | } |
91 | |
92 | /*! |
93 | \internal |
94 | */ |
95 | QColumnView::QColumnView(QColumnViewPrivate & dd, QWidget * parent) |
96 | : QAbstractItemView(dd, parent) |
97 | { |
98 | Q_D(QColumnView); |
99 | d->initialize(); |
100 | } |
101 | |
102 | void QColumnViewPrivate::initialize() |
103 | { |
104 | Q_Q(QColumnView); |
105 | q->setTextElideMode(Qt::ElideMiddle); |
106 | #if QT_CONFIG(animation) |
107 | QObject::connect(¤tAnimation, SIGNAL(finished()), q, SLOT(_q_changeCurrentColumn())); |
108 | currentAnimation.setTargetObject(hbar); |
109 | currentAnimation.setPropertyName("value" ); |
110 | currentAnimation.setEasingCurve(QEasingCurve::InOutQuad); |
111 | #endif // animation |
112 | delete itemDelegate; |
113 | q->setItemDelegate(new QColumnViewDelegate(q)); |
114 | } |
115 | |
116 | /*! |
117 | Destroys the column view. |
118 | */ |
119 | QColumnView::~QColumnView() |
120 | { |
121 | } |
122 | |
123 | /*! |
124 | \property QColumnView::resizeGripsVisible |
125 | \brief the way to specify if the list views gets resize grips or not |
126 | |
127 | By default, \c visible is set to true |
128 | |
129 | \sa setRootIndex() |
130 | */ |
131 | void QColumnView::setResizeGripsVisible(bool visible) |
132 | { |
133 | Q_D(QColumnView); |
134 | if (d->showResizeGrips == visible) |
135 | return; |
136 | d->showResizeGrips = visible; |
137 | for (int i = 0; i < d->columns.count(); ++i) { |
138 | QAbstractItemView *view = d->columns[i]; |
139 | if (visible) { |
140 | QColumnViewGrip *grip = new QColumnViewGrip(view); |
141 | view->setCornerWidget(grip); |
142 | connect(grip, SIGNAL(gripMoved(int)), this, SLOT(_q_gripMoved(int))); |
143 | } else { |
144 | QWidget *widget = view->cornerWidget(); |
145 | view->setCornerWidget(nullptr); |
146 | widget->deleteLater(); |
147 | } |
148 | } |
149 | } |
150 | |
151 | bool QColumnView::resizeGripsVisible() const |
152 | { |
153 | Q_D(const QColumnView); |
154 | return d->showResizeGrips; |
155 | } |
156 | |
157 | /*! |
158 | \reimp |
159 | */ |
160 | void QColumnView::setModel(QAbstractItemModel *model) |
161 | { |
162 | Q_D(QColumnView); |
163 | if (model == d->model) |
164 | return; |
165 | d->closeColumns(); |
166 | QAbstractItemView::setModel(model); |
167 | } |
168 | |
169 | /*! |
170 | \reimp |
171 | */ |
172 | void QColumnView::setRootIndex(const QModelIndex &index) |
173 | { |
174 | Q_D(QColumnView); |
175 | if (!model()) |
176 | return; |
177 | |
178 | d->closeColumns(); |
179 | Q_ASSERT(d->columns.count() == 0); |
180 | |
181 | QAbstractItemView *view = d->createColumn(index, true); |
182 | if (view->selectionModel()) |
183 | view->selectionModel()->deleteLater(); |
184 | if (view->model()) |
185 | view->setSelectionModel(selectionModel()); |
186 | |
187 | QAbstractItemView::setRootIndex(index); |
188 | d->updateScrollbars(); |
189 | } |
190 | |
191 | /*! |
192 | \reimp |
193 | */ |
194 | bool QColumnView::isIndexHidden(const QModelIndex &index) const |
195 | { |
196 | Q_UNUSED(index); |
197 | return false; |
198 | } |
199 | |
200 | /*! |
201 | \reimp |
202 | */ |
203 | QModelIndex QColumnView::indexAt(const QPoint &point) const |
204 | { |
205 | Q_D(const QColumnView); |
206 | for (int i = 0; i < d->columns.size(); ++i) { |
207 | QPoint topLeft = d->columns.at(i)->frameGeometry().topLeft(); |
208 | QPoint adjustedPoint(point.x() - topLeft.x(), point.y() - topLeft.y()); |
209 | QModelIndex index = d->columns.at(i)->indexAt(adjustedPoint); |
210 | if (index.isValid()) |
211 | return index; |
212 | } |
213 | return QModelIndex(); |
214 | } |
215 | |
216 | /*! |
217 | \reimp |
218 | */ |
219 | QRect QColumnView::visualRect(const QModelIndex &index) const |
220 | { |
221 | if (!index.isValid()) |
222 | return QRect(); |
223 | |
224 | Q_D(const QColumnView); |
225 | for (int i = 0; i < d->columns.size(); ++i) { |
226 | QRect rect = d->columns.at(i)->visualRect(index); |
227 | if (!rect.isNull()) { |
228 | rect.translate(d->columns.at(i)->frameGeometry().topLeft()); |
229 | return rect; |
230 | } |
231 | } |
232 | return QRect(); |
233 | } |
234 | |
235 | /*! |
236 | \reimp |
237 | */ |
238 | void QColumnView::scrollContentsBy(int dx, int dy) |
239 | { |
240 | Q_D(QColumnView); |
241 | if (d->columns.isEmpty() || dx == 0) |
242 | return; |
243 | |
244 | dx = isRightToLeft() ? -dx : dx; |
245 | for (int i = 0; i < d->columns.count(); ++i) |
246 | d->columns.at(i)->move(d->columns.at(i)->x() + dx, 0); |
247 | d->offset += dx; |
248 | QAbstractItemView::scrollContentsBy(dx, dy); |
249 | } |
250 | |
251 | /*! |
252 | \reimp |
253 | */ |
254 | void QColumnView::scrollTo(const QModelIndex &index, ScrollHint hint) |
255 | { |
256 | Q_D(QColumnView); |
257 | Q_UNUSED(hint); |
258 | if (!index.isValid() || d->columns.isEmpty()) |
259 | return; |
260 | |
261 | #if QT_CONFIG(animation) |
262 | if (d->currentAnimation.state() == QPropertyAnimation::Running) |
263 | return; |
264 | |
265 | d->currentAnimation.stop(); |
266 | #endif // animation |
267 | |
268 | // Fill up what is needed to get to index |
269 | d->closeColumns(index, true); |
270 | |
271 | QModelIndex indexParent = index.parent(); |
272 | // Find the left edge of the column that contains index |
273 | int currentColumn = 0; |
274 | int leftEdge = 0; |
275 | while (currentColumn < d->columns.size()) { |
276 | if (indexParent == d->columns.at(currentColumn)->rootIndex()) |
277 | break; |
278 | leftEdge += d->columns.at(currentColumn)->width(); |
279 | ++currentColumn; |
280 | } |
281 | |
282 | // Don't let us scroll above the root index |
283 | if (currentColumn == d->columns.size()) |
284 | return; |
285 | |
286 | int indexColumn = currentColumn; |
287 | // Find the width of what we want to show (i.e. the right edge) |
288 | int visibleWidth = d->columns.at(currentColumn)->width(); |
289 | // We want to always try to show two columns |
290 | if (currentColumn + 1 < d->columns.size()) { |
291 | ++currentColumn; |
292 | visibleWidth += d->columns.at(currentColumn)->width(); |
293 | } |
294 | |
295 | int rightEdge = leftEdge + visibleWidth; |
296 | if (isRightToLeft()) { |
297 | leftEdge = viewport()->width() - leftEdge; |
298 | rightEdge = leftEdge - visibleWidth; |
299 | qSwap(rightEdge, leftEdge); |
300 | } |
301 | |
302 | // If it is already visible don't animate |
303 | if (leftEdge > -horizontalOffset() |
304 | && rightEdge <= ( -horizontalOffset() + viewport()->size().width())) { |
305 | d->columns.at(indexColumn)->scrollTo(index); |
306 | d->_q_changeCurrentColumn(); |
307 | return; |
308 | } |
309 | |
310 | int newScrollbarValue = 0; |
311 | if (isRightToLeft()) { |
312 | if (leftEdge < 0) { |
313 | // scroll to the right |
314 | newScrollbarValue = viewport()->size().width() - leftEdge; |
315 | } else { |
316 | // scroll to the left |
317 | newScrollbarValue = rightEdge + horizontalOffset(); |
318 | } |
319 | } else { |
320 | if (leftEdge > -horizontalOffset()) { |
321 | // scroll to the right |
322 | newScrollbarValue = rightEdge - viewport()->size().width(); |
323 | } else { |
324 | // scroll to the left |
325 | newScrollbarValue = leftEdge; |
326 | } |
327 | } |
328 | |
329 | #if QT_CONFIG(animation) |
330 | if (const int animationDuration = style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, this)) { |
331 | d->currentAnimation.setDuration(animationDuration); |
332 | d->currentAnimation.setEndValue(newScrollbarValue); |
333 | d->currentAnimation.start(); |
334 | } else |
335 | #endif // animation |
336 | { |
337 | horizontalScrollBar()->setValue(newScrollbarValue); |
338 | } |
339 | } |
340 | |
341 | /*! |
342 | \reimp |
343 | Move left should go to the parent index |
344 | Move right should go to the child index or down if there is no child |
345 | */ |
346 | QModelIndex QColumnView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) |
347 | { |
348 | // the child views which have focus get to deal with this first and if |
349 | // they don't accept it then it comes up this view and we only grip left/right |
350 | Q_UNUSED(modifiers); |
351 | if (!model()) |
352 | return QModelIndex(); |
353 | |
354 | QModelIndex current = currentIndex(); |
355 | if (isRightToLeft()) { |
356 | if (cursorAction == MoveLeft) |
357 | cursorAction = MoveRight; |
358 | else if (cursorAction == MoveRight) |
359 | cursorAction = MoveLeft; |
360 | } |
361 | switch (cursorAction) { |
362 | case MoveLeft: |
363 | if (current.parent().isValid() && current.parent() != rootIndex()) |
364 | return (current.parent()); |
365 | else |
366 | return current; |
367 | |
368 | case MoveRight: |
369 | if (model()->hasChildren(current)) |
370 | return model()->index(0, 0, current); |
371 | else |
372 | return current.sibling(current.row() + 1, current.column()); |
373 | |
374 | default: |
375 | break; |
376 | } |
377 | |
378 | return QModelIndex(); |
379 | } |
380 | |
381 | /*! |
382 | \reimp |
383 | */ |
384 | void QColumnView::resizeEvent(QResizeEvent *event) |
385 | { |
386 | Q_D(QColumnView); |
387 | d->doLayout(); |
388 | d->updateScrollbars(); |
389 | if (!isRightToLeft()) { |
390 | int diff = event->oldSize().width() - event->size().width(); |
391 | if (diff < 0 && horizontalScrollBar()->isVisible() |
392 | && horizontalScrollBar()->value() == horizontalScrollBar()->maximum()) { |
393 | horizontalScrollBar()->setMaximum(horizontalScrollBar()->maximum() + diff); |
394 | } |
395 | } |
396 | QAbstractItemView::resizeEvent(event); |
397 | } |
398 | |
399 | /*! |
400 | \internal |
401 | */ |
402 | void QColumnViewPrivate::updateScrollbars() |
403 | { |
404 | Q_Q(QColumnView); |
405 | #if QT_CONFIG(animation) |
406 | if (currentAnimation.state() == QPropertyAnimation::Running) |
407 | return; |
408 | #endif // animation |
409 | |
410 | // find the total horizontal length of the laid out columns |
411 | int horizontalLength = 0; |
412 | if (!columns.isEmpty()) { |
413 | horizontalLength = (columns.constLast()->x() + columns.constLast()->width()) - columns.constFirst()->x(); |
414 | if (horizontalLength <= 0) // reverse mode |
415 | horizontalLength = (columns.constFirst()->x() + columns.constFirst()->width()) - columns.constLast()->x(); |
416 | } |
417 | |
418 | QSize viewportSize = viewport->size(); |
419 | if (horizontalLength < viewportSize.width() && hbar->value() == 0) { |
420 | hbar->setRange(0, 0); |
421 | } else { |
422 | int visibleLength = qMin(horizontalLength + q->horizontalOffset(), viewportSize.width()); |
423 | int hiddenLength = horizontalLength - visibleLength; |
424 | if (hiddenLength != hbar->maximum()) |
425 | hbar->setRange(0, hiddenLength); |
426 | } |
427 | if (!columns.isEmpty()) { |
428 | int pageStepSize = columns.at(0)->width(); |
429 | if (pageStepSize != hbar->pageStep()) |
430 | hbar->setPageStep(pageStepSize); |
431 | } |
432 | bool visible = (hbar->maximum() > 0); |
433 | if (visible != hbar->isVisible()) |
434 | hbar->setVisible(visible); |
435 | } |
436 | |
437 | /*! |
438 | \reimp |
439 | */ |
440 | int QColumnView::horizontalOffset() const |
441 | { |
442 | Q_D(const QColumnView); |
443 | return d->offset; |
444 | } |
445 | |
446 | /*! |
447 | \reimp |
448 | */ |
449 | int QColumnView::verticalOffset() const |
450 | { |
451 | return 0; |
452 | } |
453 | |
454 | /*! |
455 | \reimp |
456 | */ |
457 | QRegion QColumnView::visualRegionForSelection(const QItemSelection &selection) const |
458 | { |
459 | int ranges = selection.count(); |
460 | |
461 | if (ranges == 0) |
462 | return QRect(); |
463 | |
464 | // Note that we use the top and bottom functions of the selection range |
465 | // since the data is stored in rows. |
466 | int firstRow = selection.at(0).top(); |
467 | int lastRow = selection.at(0).top(); |
468 | for (int i = 0; i < ranges; ++i) { |
469 | firstRow = qMin(firstRow, selection.at(i).top()); |
470 | lastRow = qMax(lastRow, selection.at(i).bottom()); |
471 | } |
472 | |
473 | QModelIndex firstIdx = model()->index(qMin(firstRow, lastRow), 0, rootIndex()); |
474 | QModelIndex lastIdx = model()->index(qMax(firstRow, lastRow), 0, rootIndex()); |
475 | |
476 | if (firstIdx == lastIdx) |
477 | return visualRect(firstIdx); |
478 | |
479 | QRegion firstRegion = visualRect(firstIdx); |
480 | QRegion lastRegion = visualRect(lastIdx); |
481 | return firstRegion.united(lastRegion); |
482 | } |
483 | |
484 | /*! |
485 | \reimp |
486 | */ |
487 | void QColumnView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) |
488 | { |
489 | Q_UNUSED(rect); |
490 | Q_UNUSED(command); |
491 | } |
492 | |
493 | /*! |
494 | \reimp |
495 | */ |
496 | void QColumnView::setSelectionModel(QItemSelectionModel *newSelectionModel) |
497 | { |
498 | Q_D(const QColumnView); |
499 | for (int i = 0; i < d->columns.size(); ++i) { |
500 | if (d->columns.at(i)->selectionModel() == selectionModel()) { |
501 | d->columns.at(i)->setSelectionModel(newSelectionModel); |
502 | break; |
503 | } |
504 | } |
505 | QAbstractItemView::setSelectionModel(newSelectionModel); |
506 | } |
507 | |
508 | /*! |
509 | \reimp |
510 | */ |
511 | QSize QColumnView::sizeHint() const |
512 | { |
513 | Q_D(const QColumnView); |
514 | QSize sizeHint; |
515 | for (int i = 0; i < d->columns.size(); ++i) { |
516 | sizeHint += d->columns.at(i)->sizeHint(); |
517 | } |
518 | return sizeHint.expandedTo(QAbstractItemView::sizeHint()); |
519 | } |
520 | |
521 | /*! |
522 | \internal |
523 | Move all widgets from the corner grip and to the right |
524 | */ |
525 | void QColumnViewPrivate::_q_gripMoved(int offset) |
526 | { |
527 | Q_Q(QColumnView); |
528 | |
529 | QObject *grip = q->sender(); |
530 | Q_ASSERT(grip); |
531 | |
532 | if (q->isRightToLeft()) |
533 | offset = -1 * offset; |
534 | |
535 | bool found = false; |
536 | for (int i = 0; i < columns.size(); ++i) { |
537 | if (!found && columns.at(i)->cornerWidget() == grip) { |
538 | found = true; |
539 | columnSizes[i] = columns.at(i)->width(); |
540 | if (q->isRightToLeft()) |
541 | columns.at(i)->move(columns.at(i)->x() + offset, 0); |
542 | continue; |
543 | } |
544 | if (!found) |
545 | continue; |
546 | |
547 | int currentX = columns.at(i)->x(); |
548 | columns.at(i)->move(currentX + offset, 0); |
549 | } |
550 | |
551 | updateScrollbars(); |
552 | } |
553 | |
554 | /*! |
555 | \internal |
556 | |
557 | Find where the current columns intersect parent's columns |
558 | |
559 | Delete any extra columns and insert any needed columns. |
560 | */ |
561 | void QColumnViewPrivate::closeColumns(const QModelIndex &parent, bool build) |
562 | { |
563 | if (columns.isEmpty()) |
564 | return; |
565 | |
566 | bool clearAll = !parent.isValid(); |
567 | bool passThroughRoot = false; |
568 | |
569 | QList<QModelIndex> dirsToAppend; |
570 | |
571 | // Find the last column that matches the parent's tree |
572 | int currentColumn = -1; |
573 | QModelIndex parentIndex = parent; |
574 | while (currentColumn == -1 && parentIndex.isValid()) { |
575 | if (columns.isEmpty()) |
576 | break; |
577 | parentIndex = parentIndex.parent(); |
578 | if (root == parentIndex) |
579 | passThroughRoot = true; |
580 | if (!parentIndex.isValid()) |
581 | break; |
582 | for (int i = columns.size() - 1; i >= 0; --i) { |
583 | if (columns.at(i)->rootIndex() == parentIndex) { |
584 | currentColumn = i; |
585 | break; |
586 | } |
587 | } |
588 | if (currentColumn == -1) |
589 | dirsToAppend.append(parentIndex); |
590 | } |
591 | |
592 | // Someone wants to go to an index that can be reached without changing |
593 | // the root index, don't allow them |
594 | if (!clearAll && !passThroughRoot && currentColumn == -1) |
595 | return; |
596 | |
597 | if (currentColumn == -1 && parent.isValid()) |
598 | currentColumn = 0; |
599 | |
600 | // Optimization so we don't go deleting and then creating the same thing |
601 | bool alreadyExists = false; |
602 | if (build && columns.size() > currentColumn + 1) { |
603 | bool viewingParent = (columns.at(currentColumn + 1)->rootIndex() == parent); |
604 | bool viewingChild = (!model->hasChildren(parent) |
605 | && !columns.at(currentColumn + 1)->rootIndex().isValid()); |
606 | if (viewingParent || viewingChild) { |
607 | currentColumn++; |
608 | alreadyExists = true; |
609 | } |
610 | } |
611 | |
612 | // Delete columns that don't match our path |
613 | for (int i = columns.size() - 1; i > currentColumn; --i) { |
614 | QAbstractItemView* notShownAnymore = columns.at(i); |
615 | columns.removeAt(i); |
616 | notShownAnymore->setVisible(false); |
617 | if (notShownAnymore != previewColumn) |
618 | notShownAnymore->deleteLater(); |
619 | } |
620 | |
621 | if (columns.isEmpty()) { |
622 | offset = 0; |
623 | updateScrollbars(); |
624 | } |
625 | |
626 | // Now fill in missing columns |
627 | while (!dirsToAppend.isEmpty()) { |
628 | QAbstractItemView *newView = createColumn(dirsToAppend.takeLast(), true); |
629 | if (!dirsToAppend.isEmpty()) |
630 | newView->setCurrentIndex(dirsToAppend.constLast()); |
631 | } |
632 | |
633 | if (build && !alreadyExists) |
634 | createColumn(parent, false); |
635 | } |
636 | |
637 | void QColumnViewPrivate::_q_clicked(const QModelIndex &index) |
638 | { |
639 | Q_Q(QColumnView); |
640 | QModelIndex parent = index.parent(); |
641 | QAbstractItemView *columnClicked = nullptr; |
642 | for (int column = 0; column < columns.count(); ++column) { |
643 | if (columns.at(column)->rootIndex() == parent) { |
644 | columnClicked = columns[column]; |
645 | break; |
646 | } |
647 | } |
648 | if (q->selectionModel() && columnClicked) { |
649 | QItemSelectionModel::SelectionFlags flags = QItemSelectionModel::Current; |
650 | if (columnClicked->selectionModel()->isSelected(index)) |
651 | flags |= QItemSelectionModel::Select; |
652 | q->selectionModel()->setCurrentIndex(index, flags); |
653 | } |
654 | } |
655 | |
656 | /*! |
657 | \internal |
658 | Create a new column for \a index. A grip is attached if requested and it is shown |
659 | if requested. |
660 | |
661 | Return the new view |
662 | |
663 | \sa createColumn(), setPreviewWidget() |
664 | \sa doLayout() |
665 | */ |
666 | QAbstractItemView *QColumnViewPrivate::createColumn(const QModelIndex &index, bool show) |
667 | { |
668 | Q_Q(QColumnView); |
669 | QAbstractItemView *view = nullptr; |
670 | if (model->hasChildren(index)) { |
671 | view = q->createColumn(index); |
672 | q->connect(view, SIGNAL(clicked(QModelIndex)), |
673 | q, SLOT(_q_clicked(QModelIndex))); |
674 | } else { |
675 | if (!previewColumn) |
676 | setPreviewWidget(new QWidget(q)); |
677 | view = previewColumn; |
678 | view->setMinimumWidth(qMax(view->minimumWidth(), previewWidget->minimumWidth())); |
679 | } |
680 | |
681 | q->connect(view, SIGNAL(activated(QModelIndex)), |
682 | q, SIGNAL(activated(QModelIndex))); |
683 | q->connect(view, SIGNAL(clicked(QModelIndex)), |
684 | q, SIGNAL(clicked(QModelIndex))); |
685 | q->connect(view, SIGNAL(doubleClicked(QModelIndex)), |
686 | q, SIGNAL(doubleClicked(QModelIndex))); |
687 | q->connect(view, SIGNAL(entered(QModelIndex)), |
688 | q, SIGNAL(entered(QModelIndex))); |
689 | q->connect(view, SIGNAL(pressed(QModelIndex)), |
690 | q, SIGNAL(pressed(QModelIndex))); |
691 | |
692 | view->setFocusPolicy(Qt::NoFocus); |
693 | view->setParent(viewport); |
694 | Q_ASSERT(view); |
695 | |
696 | // Setup corner grip |
697 | if (showResizeGrips) { |
698 | QColumnViewGrip *grip = new QColumnViewGrip(view); |
699 | view->setCornerWidget(grip); |
700 | q->connect(grip, SIGNAL(gripMoved(int)), q, SLOT(_q_gripMoved(int))); |
701 | } |
702 | |
703 | if (columnSizes.count() > columns.count()) { |
704 | view->setGeometry(0, 0, columnSizes.at(columns.count()), viewport->height()); |
705 | } else { |
706 | int initialWidth = view->sizeHint().width(); |
707 | if (q->isRightToLeft()) |
708 | view->setGeometry(viewport->width() - initialWidth, 0, initialWidth, viewport->height()); |
709 | else |
710 | view->setGeometry(0, 0, initialWidth, viewport->height()); |
711 | columnSizes.resize(qMax(columnSizes.count(), columns.count() + 1)); |
712 | columnSizes[columns.count()] = initialWidth; |
713 | } |
714 | if (!columns.isEmpty() && columns.constLast()->isHidden()) |
715 | columns.constLast()->setVisible(true); |
716 | |
717 | columns.append(view); |
718 | doLayout(); |
719 | updateScrollbars(); |
720 | if (show && view->isHidden()) |
721 | view->setVisible(true); |
722 | return view; |
723 | } |
724 | |
725 | /*! |
726 | \fn void QColumnView::updatePreviewWidget(const QModelIndex &index) |
727 | |
728 | This signal is emitted when the preview widget should be updated to |
729 | provide rich information about \a index |
730 | |
731 | \sa previewWidget() |
732 | */ |
733 | |
734 | /*! |
735 | To use a custom widget for the final column when you select |
736 | an item overload this function and return a widget. |
737 | \a index is the root index that will be assigned to the view. |
738 | |
739 | Return the new view. QColumnView will automatically take ownership of the widget. |
740 | |
741 | \sa setPreviewWidget() |
742 | */ |
743 | QAbstractItemView *QColumnView::createColumn(const QModelIndex &index) |
744 | { |
745 | QListView *view = new QListView(viewport()); |
746 | |
747 | initializeColumn(view); |
748 | |
749 | view->setRootIndex(index); |
750 | if (model()->canFetchMore(index)) |
751 | model()->fetchMore(index); |
752 | |
753 | return view; |
754 | } |
755 | |
756 | /*! |
757 | Copies the behavior and options of the column view and applies them to |
758 | the \a column such as the iconSize(), textElideMode() and |
759 | alternatingRowColors(). This can be useful when reimplementing |
760 | createColumn(). |
761 | |
762 | \since 4.4 |
763 | \sa createColumn() |
764 | */ |
765 | void QColumnView::initializeColumn(QAbstractItemView *column) const |
766 | { |
767 | Q_D(const QColumnView); |
768 | |
769 | column->setFrameShape(QFrame::NoFrame); |
770 | column->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); |
771 | column->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); |
772 | column->setMinimumWidth(100); |
773 | column->setAttribute(Qt::WA_MacShowFocusRect, false); |
774 | |
775 | #if QT_CONFIG(draganddrop) |
776 | column->setDragDropMode(dragDropMode()); |
777 | column->setDragDropOverwriteMode(dragDropOverwriteMode()); |
778 | column->setDropIndicatorShown(showDropIndicator()); |
779 | #endif |
780 | column->setAlternatingRowColors(alternatingRowColors()); |
781 | column->setAutoScroll(hasAutoScroll()); |
782 | column->setEditTriggers(editTriggers()); |
783 | column->setHorizontalScrollMode(horizontalScrollMode()); |
784 | column->setIconSize(iconSize()); |
785 | column->setSelectionBehavior(selectionBehavior()); |
786 | column->setSelectionMode(selectionMode()); |
787 | column->setTabKeyNavigation(tabKeyNavigation()); |
788 | column->setTextElideMode(textElideMode()); |
789 | column->setVerticalScrollMode(verticalScrollMode()); |
790 | |
791 | column->setModel(model()); |
792 | |
793 | // Copy the custom delegate per row |
794 | for (auto i = d->rowDelegates.cbegin(), end = d->rowDelegates.cend(); i != end; ++i) |
795 | column->setItemDelegateForRow(i.key(), i.value()); |
796 | |
797 | // set the delegate to be the columnview delegate |
798 | QAbstractItemDelegate *delegate = column->itemDelegate(); |
799 | column->setItemDelegate(d->itemDelegate); |
800 | delete delegate; |
801 | } |
802 | |
803 | /*! |
804 | Returns the preview widget, or \nullptr if there is none. |
805 | |
806 | \sa setPreviewWidget(), updatePreviewWidget() |
807 | */ |
808 | QWidget *QColumnView::previewWidget() const |
809 | { |
810 | Q_D(const QColumnView); |
811 | return d->previewWidget; |
812 | } |
813 | |
814 | /*! |
815 | Sets the preview \a widget. |
816 | |
817 | The \a widget becomes a child of the column view, and will be |
818 | destroyed when the column area is deleted or when a new widget is |
819 | set. |
820 | |
821 | \sa previewWidget(), updatePreviewWidget() |
822 | */ |
823 | void QColumnView::setPreviewWidget(QWidget *widget) |
824 | { |
825 | Q_D(QColumnView); |
826 | d->setPreviewWidget(widget); |
827 | } |
828 | |
829 | /*! |
830 | \internal |
831 | */ |
832 | void QColumnViewPrivate::setPreviewWidget(QWidget *widget) |
833 | { |
834 | Q_Q(QColumnView); |
835 | if (previewColumn) { |
836 | if (!columns.isEmpty() && columns.constLast() == previewColumn) |
837 | columns.removeLast(); |
838 | previewColumn->deleteLater(); |
839 | } |
840 | QColumnViewPreviewColumn *column = new QColumnViewPreviewColumn(q); |
841 | column->setPreviewWidget(widget); |
842 | previewColumn = column; |
843 | previewColumn->hide(); |
844 | previewColumn->setFrameShape(QFrame::NoFrame); |
845 | previewColumn->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); |
846 | previewColumn->setSelectionMode(QAbstractItemView::NoSelection); |
847 | previewColumn->setMinimumWidth(qMax(previewColumn->verticalScrollBar()->width(), |
848 | previewColumn->minimumWidth())); |
849 | previewWidget = widget; |
850 | previewWidget->setParent(previewColumn->viewport()); |
851 | } |
852 | |
853 | /*! |
854 | Sets the column widths to the values given in the \a list. Extra values in the list are |
855 | kept and used when the columns are created. |
856 | |
857 | If list contains too few values, only width of the rest of the columns will not be modified. |
858 | |
859 | \sa columnWidths(), createColumn() |
860 | */ |
861 | void QColumnView::setColumnWidths(const QList<int> &list) |
862 | { |
863 | Q_D(QColumnView); |
864 | int i = 0; |
865 | const int listCount = list.count(); |
866 | const int count = qMin(listCount, d->columns.count()); |
867 | for (; i < count; ++i) { |
868 | d->columns.at(i)->resize(list.at(i), d->columns.at(i)->height()); |
869 | d->columnSizes[i] = list.at(i); |
870 | } |
871 | |
872 | d->columnSizes.reserve(listCount); |
873 | for (; i < listCount; ++i) |
874 | d->columnSizes.append(list.at(i)); |
875 | } |
876 | |
877 | /*! |
878 | Returns a list of the width of all the columns in this view. |
879 | |
880 | \sa setColumnWidths() |
881 | */ |
882 | QList<int> QColumnView::columnWidths() const |
883 | { |
884 | Q_D(const QColumnView); |
885 | QList<int> list; |
886 | const int columnCount = d->columns.count(); |
887 | list.reserve(columnCount); |
888 | for (int i = 0; i < columnCount; ++i) |
889 | list.append(d->columnSizes.at(i)); |
890 | return list; |
891 | } |
892 | |
893 | /*! |
894 | \reimp |
895 | */ |
896 | void QColumnView::rowsInserted(const QModelIndex &parent, int start, int end) |
897 | { |
898 | QAbstractItemView::rowsInserted(parent, start, end); |
899 | d_func()->checkColumnCreation(parent); |
900 | } |
901 | |
902 | /*! |
903 | \reimp |
904 | */ |
905 | void QColumnView::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) |
906 | { |
907 | Q_D(QColumnView); |
908 | if (!current.isValid()) { |
909 | QAbstractItemView::currentChanged(current, previous); |
910 | return; |
911 | } |
912 | |
913 | QModelIndex currentParent = current.parent(); |
914 | // optimize for just moving up/down in a list where the child view doesn't change |
915 | if (currentParent == previous.parent() |
916 | && model()->hasChildren(current) && model()->hasChildren(previous)) { |
917 | for (int i = 0; i < d->columns.size(); ++i) { |
918 | if (currentParent == d->columns.at(i)->rootIndex()) { |
919 | if (d->columns.size() > i + 1) { |
920 | QAbstractItemView::currentChanged(current, previous); |
921 | return; |
922 | } |
923 | break; |
924 | } |
925 | } |
926 | } |
927 | |
928 | // Scrolling to the right we need to have an empty spot |
929 | bool found = false; |
930 | if (currentParent == previous) { |
931 | for (int i = 0; i < d->columns.size(); ++i) { |
932 | if (currentParent == d->columns.at(i)->rootIndex()) { |
933 | found = true; |
934 | if (d->columns.size() < i + 2) { |
935 | d->createColumn(current, false); |
936 | } |
937 | break; |
938 | } |
939 | } |
940 | } |
941 | if (!found) |
942 | d->closeColumns(current, true); |
943 | |
944 | if (!model()->hasChildren(current)) |
945 | emit updatePreviewWidget(current); |
946 | |
947 | QAbstractItemView::currentChanged(current, previous); |
948 | } |
949 | |
950 | /* |
951 | We have change the current column and need to update focus and selection models |
952 | on the new current column. |
953 | */ |
954 | void QColumnViewPrivate::_q_changeCurrentColumn() |
955 | { |
956 | Q_Q(QColumnView); |
957 | if (columns.isEmpty()) |
958 | return; |
959 | |
960 | QModelIndex current = q->currentIndex(); |
961 | if (!current.isValid()) |
962 | return; |
963 | |
964 | // We might have scrolled far to the left so we need to close all of the children |
965 | closeColumns(current, true); |
966 | |
967 | // Set up the "current" column with focus |
968 | int currentColumn = qMax(0, columns.size() - 2); |
969 | QAbstractItemView *parentColumn = columns.at(currentColumn); |
970 | if (q->hasFocus()) |
971 | parentColumn->setFocus(Qt::OtherFocusReason); |
972 | q->setFocusProxy(parentColumn); |
973 | |
974 | // find the column that is our current selection model and give it a new one. |
975 | for (int i = 0; i < columns.size(); ++i) { |
976 | if (columns.at(i)->selectionModel() == q->selectionModel()) { |
977 | QItemSelectionModel *replacementSelectionModel = |
978 | new QItemSelectionModel(parentColumn->model()); |
979 | replacementSelectionModel->setCurrentIndex( |
980 | q->selectionModel()->currentIndex(), QItemSelectionModel::Current); |
981 | replacementSelectionModel->select( |
982 | q->selectionModel()->selection(), QItemSelectionModel::Select); |
983 | QAbstractItemView *view = columns.at(i); |
984 | view->setSelectionModel(replacementSelectionModel); |
985 | view->setFocusPolicy(Qt::NoFocus); |
986 | if (columns.size() > i + 1) { |
987 | const QModelIndex newRootIndex = columns.at(i + 1)->rootIndex(); |
988 | if (newRootIndex.isValid()) |
989 | view->setCurrentIndex(newRootIndex); |
990 | } |
991 | break; |
992 | } |
993 | } |
994 | parentColumn->selectionModel()->deleteLater(); |
995 | parentColumn->setFocusPolicy(Qt::StrongFocus); |
996 | parentColumn->setSelectionModel(q->selectionModel()); |
997 | // We want the parent selection to stay highlighted (but dimmed depending upon the color theme) |
998 | if (currentColumn > 0) { |
999 | parentColumn = columns.at(currentColumn - 1); |
1000 | if (parentColumn->currentIndex() != current.parent()) |
1001 | parentColumn->setCurrentIndex(current.parent()); |
1002 | } |
1003 | |
1004 | if (columns.constLast()->isHidden()) { |
1005 | columns.constLast()->setVisible(true); |
1006 | } |
1007 | if (columns.constLast()->selectionModel()) |
1008 | columns.constLast()->selectionModel()->clear(); |
1009 | updateScrollbars(); |
1010 | } |
1011 | |
1012 | /*! |
1013 | \reimp |
1014 | */ |
1015 | void QColumnView::selectAll() |
1016 | { |
1017 | if (!model() || !selectionModel()) |
1018 | return; |
1019 | |
1020 | QModelIndexList indexList = selectionModel()->selectedIndexes(); |
1021 | QModelIndex parent = rootIndex(); |
1022 | QItemSelection selection; |
1023 | if (indexList.count() >= 1) |
1024 | parent = indexList.at(0).parent(); |
1025 | if (indexList.count() == 1) { |
1026 | parent = indexList.at(0); |
1027 | if (!model()->hasChildren(parent)) |
1028 | parent = parent.parent(); |
1029 | else |
1030 | selection.append(QItemSelectionRange(parent, parent)); |
1031 | } |
1032 | |
1033 | QModelIndex tl = model()->index(0, 0, parent); |
1034 | QModelIndex br = model()->index(model()->rowCount(parent) - 1, |
1035 | model()->columnCount(parent) - 1, |
1036 | parent); |
1037 | selection.append(QItemSelectionRange(tl, br)); |
1038 | selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect); |
1039 | } |
1040 | |
1041 | /* |
1042 | * private object implementation |
1043 | */ |
1044 | QColumnViewPrivate::QColumnViewPrivate() |
1045 | : QAbstractItemViewPrivate() |
1046 | ,showResizeGrips(true) |
1047 | ,offset(0) |
1048 | ,previewWidget(nullptr) |
1049 | ,previewColumn(nullptr) |
1050 | { |
1051 | } |
1052 | |
1053 | QColumnViewPrivate::~QColumnViewPrivate() |
1054 | { |
1055 | } |
1056 | |
1057 | /*! |
1058 | \internal |
1059 | |
1060 | */ |
1061 | void QColumnViewPrivate::_q_columnsInserted(const QModelIndex &parent, int start, int end) |
1062 | { |
1063 | QAbstractItemViewPrivate::_q_columnsInserted(parent, start, end); |
1064 | checkColumnCreation(parent); |
1065 | } |
1066 | |
1067 | /*! |
1068 | \internal |
1069 | |
1070 | Makes sure we create a corresponding column as a result of changing the model. |
1071 | |
1072 | */ |
1073 | void QColumnViewPrivate::checkColumnCreation(const QModelIndex &parent) |
1074 | { |
1075 | if (parent == q_func()->currentIndex() && model->hasChildren(parent)) { |
1076 | //the parent has children and is the current |
1077 | //let's try to find out if there is already a mapping that is good |
1078 | for (int i = 0; i < columns.count(); ++i) { |
1079 | QAbstractItemView *view = columns.at(i); |
1080 | if (view->rootIndex() == parent) { |
1081 | if (view == previewColumn) { |
1082 | //let's recreate the parent |
1083 | closeColumns(parent, false); |
1084 | createColumn(parent, true /*show*/); |
1085 | } |
1086 | break; |
1087 | } |
1088 | } |
1089 | } |
1090 | } |
1091 | |
1092 | /*! |
1093 | \internal |
1094 | Place all of the columns where they belong inside of the viewport, resize as necessary. |
1095 | */ |
1096 | void QColumnViewPrivate::doLayout() |
1097 | { |
1098 | Q_Q(QColumnView); |
1099 | if (!model || columns.isEmpty()) |
1100 | return; |
1101 | |
1102 | int viewportHeight = viewport->height(); |
1103 | int x = columns.at(0)->x(); |
1104 | |
1105 | if (q->isRightToLeft()) { |
1106 | x = viewport->width() + q->horizontalOffset(); |
1107 | for (int i = 0; i < columns.size(); ++i) { |
1108 | QAbstractItemView *view = columns.at(i); |
1109 | x -= view->width(); |
1110 | if (x != view->x() || viewportHeight != view->height()) |
1111 | view->setGeometry(x, 0, view->width(), viewportHeight); |
1112 | } |
1113 | } else { |
1114 | for (int i = 0; i < columns.size(); ++i) { |
1115 | QAbstractItemView *view = columns.at(i); |
1116 | int currentColumnWidth = view->width(); |
1117 | if (x != view->x() || viewportHeight != view->height()) |
1118 | view->setGeometry(x, 0, currentColumnWidth, viewportHeight); |
1119 | x += currentColumnWidth; |
1120 | } |
1121 | } |
1122 | } |
1123 | |
1124 | /*! |
1125 | \internal |
1126 | |
1127 | Draws a delegate with a > if an object has children. |
1128 | |
1129 | \sa {Model/View Programming}, QStyledItemDelegate |
1130 | */ |
1131 | void QColumnViewDelegate::paint(QPainter *painter, |
1132 | const QStyleOptionViewItem &option, |
1133 | const QModelIndex &index) const |
1134 | { |
1135 | bool reverse = (option.direction == Qt::RightToLeft); |
1136 | int width = ((option.rect.height() * 2) / 3); |
1137 | // Modify the options to give us room to add an arrow |
1138 | QStyleOptionViewItem opt = option; |
1139 | if (reverse) |
1140 | opt.rect.adjust(width,0,0,0); |
1141 | else |
1142 | opt.rect.adjust(0,0,-width,0); |
1143 | |
1144 | if (!(index.model()->flags(index) & Qt::ItemIsEnabled)) { |
1145 | opt.showDecorationSelected = true; |
1146 | opt.state |= QStyle::State_Selected; |
1147 | } |
1148 | |
1149 | QStyledItemDelegate::paint(painter, opt, index); |
1150 | |
1151 | if (reverse) |
1152 | opt.rect = QRect(option.rect.x(), option.rect.y(), width, option.rect.height()); |
1153 | else |
1154 | opt.rect = QRect(option.rect.x() + option.rect.width() - width, option.rect.y(), |
1155 | width, option.rect.height()); |
1156 | |
1157 | // Draw > |
1158 | if (index.model()->hasChildren(index)) { |
1159 | const QWidget *view = opt.widget; |
1160 | QStyle *style = view ? view->style() : QApplication::style(); |
1161 | style->drawPrimitive(QStyle::PE_IndicatorColumnViewArrow, &opt, painter, view); |
1162 | } |
1163 | } |
1164 | |
1165 | QT_END_NAMESPACE |
1166 | |
1167 | #include "moc_qcolumnview.cpp" |
1168 | |
1169 | #endif // QT_CONFIG(columnview) |
1170 | |