1 /* Copyright 2013-2019 MultiMC Contributors
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "GroupView.h"
17
18 #include <QPainter>
19 #include <QApplication>
20 #include <QtMath>
21 #include <QMouseEvent>
22 #include <QListView>
23 #include <QPersistentModelIndex>
24 #include <QDrag>
25 #include <QMimeData>
26 #include <QCache>
27 #include <QScrollBar>
28 #include <QAccessible>
29
30 #include "VisualGroup.h"
31 #include <QDebug>
32
listsIntersect(const QList<T> & l1,const QList<T> t2)33 template <typename T> bool listsIntersect(const QList<T> &l1, const QList<T> t2)
34 {
35 for (auto &item : l1)
36 {
37 if (t2.contains(item))
38 {
39 return true;
40 }
41 }
42 return false;
43 }
44
GroupView(QWidget * parent)45 GroupView::GroupView(QWidget *parent)
46 : QAbstractItemView(parent)
47 {
48 setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
49 setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
50 setAcceptDrops(true);
51 setAutoScroll(true);
52 }
53
~GroupView()54 GroupView::~GroupView()
55 {
56 qDeleteAll(m_groups);
57 m_groups.clear();
58 }
59
setModel(QAbstractItemModel * model)60 void GroupView::setModel(QAbstractItemModel *model)
61 {
62 QAbstractItemView::setModel(model);
63 connect(model, &QAbstractItemModel::modelReset, this, &GroupView::modelReset);
64 connect(model, &QAbstractItemModel::rowsRemoved, this, &GroupView::rowsRemoved);
65 }
66
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)67 void GroupView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
68 const QVector<int> &roles)
69 {
70 scheduleDelayedItemsLayout();
71 }
rowsInserted(const QModelIndex & parent,int start,int end)72 void GroupView::rowsInserted(const QModelIndex &parent, int start, int end)
73 {
74 scheduleDelayedItemsLayout();
75 }
76
rowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)77 void GroupView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
78 {
79 scheduleDelayedItemsLayout();
80 }
81
modelReset()82 void GroupView::modelReset()
83 {
84 scheduleDelayedItemsLayout();
85 }
86
rowsRemoved()87 void GroupView::rowsRemoved()
88 {
89 scheduleDelayedItemsLayout();
90 }
91
currentChanged(const QModelIndex & current,const QModelIndex & previous)92 void GroupView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
93 {
94 QAbstractItemView::currentChanged(current, previous);
95 // TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for GroupView.
96 #ifndef QT_NO_ACCESSIBILITY
97 if (QAccessible::isActive() && current.isValid()) {
98 QAccessibleEvent event(this, QAccessible::Focus);
99 event.setChild(current.row());
100 QAccessible::updateAccessibility(&event);
101 }
102 #endif /* !QT_NO_ACCESSIBILITY */
103 }
104
105
106 class LocaleString : public QString
107 {
108 public:
LocaleString(const char * s)109 LocaleString(const char *s) : QString(s)
110 {
111 }
LocaleString(const QString & s)112 LocaleString(const QString &s) : QString(s)
113 {
114 }
115 };
116
operator <(const LocaleString & lhs,const LocaleString & rhs)117 inline bool operator<(const LocaleString &lhs, const LocaleString &rhs)
118 {
119 return (QString::localeAwareCompare(lhs, rhs) < 0);
120 }
121
updateScrollbar()122 void GroupView::updateScrollbar()
123 {
124 int previousScroll = verticalScrollBar()->value();
125 if (m_groups.isEmpty())
126 {
127 verticalScrollBar()->setRange(0, 0);
128 }
129 else
130 {
131 int totalHeight = 0;
132 // top margin
133 totalHeight += m_categoryMargin;
134 int itemScroll = 0;
135 for (auto category : m_groups)
136 {
137 category->m_verticalPosition = totalHeight;
138 totalHeight += category->totalHeight() + m_categoryMargin;
139 if(!itemScroll && category->totalHeight() != 0)
140 {
141 itemScroll = category->contentHeight() / category->numRows();
142 }
143 }
144 // do not divide by zero
145 if(itemScroll == 0)
146 itemScroll = 64;
147
148 totalHeight += m_bottomMargin;
149 verticalScrollBar()->setSingleStep ( itemScroll );
150 const int rowsPerPage = qMax ( viewport()->height() / itemScroll, 1 );
151 verticalScrollBar()->setPageStep ( rowsPerPage * itemScroll );
152
153 verticalScrollBar()->setRange(0, totalHeight - height());
154 }
155
156 verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum()));
157 }
158
updateGeometries()159 void GroupView::updateGeometries()
160 {
161 geometryCache.clear();
162
163 QMap<LocaleString, VisualGroup *> cats;
164
165 for (int i = 0; i < model()->rowCount(); ++i)
166 {
167 const QString groupName = model()->index(i, 0).data(GroupViewRoles::GroupRole).toString();
168 if (!cats.contains(groupName))
169 {
170 VisualGroup *old = this->category(groupName);
171 if (old)
172 {
173 auto cat = new VisualGroup(old);
174 cats.insert(groupName, cat);
175 cat->update();
176 }
177 else
178 {
179 auto cat = new VisualGroup(groupName, this);
180 if(fVisibility) {
181 cat->collapsed = fVisibility(groupName);
182 }
183 cats.insert(groupName, cat);
184 cat->update();
185 }
186 }
187 }
188
189 qDeleteAll(m_groups);
190 m_groups = cats.values();
191 updateScrollbar();
192 viewport()->update();
193 }
194
isIndexHidden(const QModelIndex & index) const195 bool GroupView::isIndexHidden(const QModelIndex &index) const
196 {
197 VisualGroup *cat = category(index);
198 if (cat)
199 {
200 return cat->collapsed;
201 }
202 else
203 {
204 return false;
205 }
206 }
207
category(const QModelIndex & index) const208 VisualGroup *GroupView::category(const QModelIndex &index) const
209 {
210 return category(index.data(GroupViewRoles::GroupRole).toString());
211 }
212
category(const QString & cat) const213 VisualGroup *GroupView::category(const QString &cat) const
214 {
215 for (auto group : m_groups)
216 {
217 if (group->text == cat)
218 {
219 return group;
220 }
221 }
222 return nullptr;
223 }
224
categoryAt(const QPoint & pos,VisualGroup::HitResults & result) const225 VisualGroup *GroupView::categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const
226 {
227 for (auto group : m_groups)
228 {
229 result = group->hitScan(pos);
230 if(result != VisualGroup::NoHit)
231 {
232 return group;
233 }
234 }
235 result = VisualGroup::NoHit;
236 return nullptr;
237 }
238
groupNameAt(const QPoint & point)239 QString GroupView::groupNameAt(const QPoint &point)
240 {
241 executeDelayedItemsLayout();
242
243 VisualGroup::HitResults hitresult;
244 auto group = categoryAt(point + offset(), hitresult);
245 if(group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit)))
246 {
247 return group->text;
248 }
249 return QString();
250 }
251
calculateItemsPerRow() const252 int GroupView::calculateItemsPerRow() const
253 {
254 return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing));
255 }
256
contentWidth() const257 int GroupView::contentWidth() const
258 {
259 return width() - m_leftMargin - m_rightMargin;
260 }
261
itemWidth() const262 int GroupView::itemWidth() const
263 {
264 return m_itemWidth;
265 }
266
mousePressEvent(QMouseEvent * event)267 void GroupView::mousePressEvent(QMouseEvent *event)
268 {
269 executeDelayedItemsLayout();
270
271 QPoint visualPos = event->pos();
272 QPoint geometryPos = event->pos() + offset();
273
274 QPersistentModelIndex index = indexAt(visualPos);
275
276 m_pressedIndex = index;
277 m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex);
278 m_pressedPosition = geometryPos;
279
280 VisualGroup::HitResults hitresult;
281 m_pressedCategory = categoryAt(geometryPos, hitresult);
282 if (m_pressedCategory && hitresult & VisualGroup::CheckboxHit)
283 {
284 setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState);
285 event->accept();
286 return;
287 }
288
289 if (index.isValid() && (index.flags() & Qt::ItemIsEnabled))
290 {
291 if(index != currentIndex())
292 {
293 // FIXME: better!
294 m_currentCursorColumn = -1;
295 }
296 // we disable scrollTo for mouse press so the item doesn't change position
297 // when the user is interacting with it (ie. clicking on it)
298 bool autoScroll = hasAutoScroll();
299 setAutoScroll(false);
300 selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
301
302 setAutoScroll(autoScroll);
303 QRect rect(visualPos, visualPos);
304 setSelection(rect, QItemSelectionModel::ClearAndSelect);
305
306 // signal handlers may change the model
307 emit pressed(index);
308 }
309 else
310 {
311 // Forces a finalize() even if mouse is pressed, but not on a item
312 selectionModel()->select(QModelIndex(), QItemSelectionModel::Select);
313 }
314 }
315
mouseMoveEvent(QMouseEvent * event)316 void GroupView::mouseMoveEvent(QMouseEvent *event)
317 {
318 executeDelayedItemsLayout();
319
320 QPoint topLeft;
321 QPoint visualPos = event->pos();
322 QPoint geometryPos = event->pos() + offset();
323
324 if (state() == ExpandingState || state() == CollapsingState)
325 {
326 return;
327 }
328
329 if (state() == DraggingState)
330 {
331 topLeft = m_pressedPosition - offset();
332 if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance())
333 {
334 m_pressedIndex = QModelIndex();
335 startDrag(model()->supportedDragActions());
336 setState(NoState);
337 stopAutoScroll();
338 }
339 return;
340 }
341
342 if (selectionMode() != SingleSelection)
343 {
344 topLeft = m_pressedPosition - offset();
345 }
346 else
347 {
348 topLeft = geometryPos;
349 }
350
351 if (m_pressedIndex.isValid() && (state() != DragSelectingState) &&
352 (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty())
353 {
354 setState(DraggingState);
355 return;
356 }
357
358 if ((event->buttons() & Qt::LeftButton) && selectionModel())
359 {
360 setState(DragSelectingState);
361
362 setSelection(QRect(visualPos, visualPos), QItemSelectionModel::ClearAndSelect);
363 QModelIndex index = indexAt(visualPos);
364
365 // set at the end because it might scroll the view
366 if (index.isValid() && (index != selectionModel()->currentIndex()) &&
367 (index.flags() & Qt::ItemIsEnabled))
368 {
369 selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate);
370 }
371 }
372 }
373
mouseReleaseEvent(QMouseEvent * event)374 void GroupView::mouseReleaseEvent(QMouseEvent *event)
375 {
376 executeDelayedItemsLayout();
377
378 QPoint visualPos = event->pos();
379 QPoint geometryPos = event->pos() + offset();
380 QPersistentModelIndex index = indexAt(visualPos);
381
382 VisualGroup::HitResults hitresult;
383
384 bool click = (index == m_pressedIndex && index.isValid()) ||
385 (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos, hitresult));
386
387 if (click && m_pressedCategory)
388 {
389 if (state() == ExpandingState)
390 {
391 m_pressedCategory->collapsed = false;
392 emit groupStateChanged(m_pressedCategory->text, false);
393
394 updateGeometries();
395 viewport()->update();
396 event->accept();
397 m_pressedCategory = nullptr;
398 setState(NoState);
399 return;
400 }
401 else if (state() == CollapsingState)
402 {
403 m_pressedCategory->collapsed = true;
404 emit groupStateChanged(m_pressedCategory->text, true);
405
406 updateGeometries();
407 viewport()->update();
408 event->accept();
409 m_pressedCategory = nullptr;
410 setState(NoState);
411 return;
412 }
413 }
414
415 m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate;
416
417 setState(NoState);
418
419 if (click)
420 {
421 if (event->button() == Qt::LeftButton)
422 {
423 emit clicked(index);
424 }
425 QStyleOptionViewItem option = viewOptions();
426 if (m_pressedAlreadySelected)
427 {
428 option.state |= QStyle::State_Selected;
429 }
430 if ((model()->flags(index) & Qt::ItemIsEnabled) &&
431 style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
432 {
433 emit activated(index);
434 }
435 }
436 }
437
mouseDoubleClickEvent(QMouseEvent * event)438 void GroupView::mouseDoubleClickEvent(QMouseEvent *event)
439 {
440 executeDelayedItemsLayout();
441
442 QModelIndex index = indexAt(event->pos());
443 if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index))
444 {
445 QMouseEvent me(QEvent::MouseButtonPress, event->localPos(), event->windowPos(),
446 event->screenPos(), event->button(), event->buttons(),
447 event->modifiers());
448 mousePressEvent(&me);
449 return;
450 }
451 // signal handlers may change the model
452 QPersistentModelIndex persistent = index;
453 emit doubleClicked(persistent);
454
455 QStyleOptionViewItem option = viewOptions();
456 if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this))
457 {
458 emit activated(index);
459 }
460 }
461
paintEvent(QPaintEvent * event)462 void GroupView::paintEvent(QPaintEvent *event)
463 {
464 executeDelayedItemsLayout();
465
466 QPainter painter(this->viewport());
467
468 QStyleOptionViewItem option(viewOptions());
469 option.widget = this;
470
471 int wpWidth = viewport()->width();
472 option.rect.setWidth(wpWidth);
473 for (int i = 0; i < m_groups.size(); ++i)
474 {
475 VisualGroup *category = m_groups.at(i);
476 int y = category->verticalPosition();
477 y -= verticalOffset();
478 QRect backup = option.rect;
479 int height = category->totalHeight();
480 option.rect.setTop(y);
481 option.rect.setHeight(height);
482 option.rect.setLeft(m_leftMargin);
483 option.rect.setRight(wpWidth - m_rightMargin);
484 category->drawHeader(&painter, option);
485 y += category->totalHeight() + m_categoryMargin;
486 option.rect = backup;
487 }
488
489 for (int i = 0; i < model()->rowCount(); ++i)
490 {
491 const QModelIndex index = model()->index(i, 0);
492 if (isIndexHidden(index))
493 {
494 continue;
495 }
496 Qt::ItemFlags flags = index.flags();
497 option.rect = visualRect(index);
498 option.features |= QStyleOptionViewItem::WrapText;
499 if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index))
500 {
501 option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected
502 : QStyle::State_None;
503 }
504 else
505 {
506 option.state &= ~QStyle::State_Selected;
507 }
508 option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None;
509 if (!(flags & Qt::ItemIsEnabled))
510 {
511 option.state &= ~QStyle::State_Enabled;
512 }
513 itemDelegate()->paint(&painter, option, index);
514 }
515
516 /*
517 * Drop indicators for manual reordering...
518 */
519 #if 0
520 if (!m_lastDragPosition.isNull())
521 {
522 QPair<Group *, int> pair = rowDropPos(m_lastDragPosition);
523 Group *category = pair.first;
524 int row = pair.second;
525 if (category)
526 {
527 int internalRow = row - category->firstItemIndex;
528 QLine line;
529 if (internalRow >= category->numItems())
530 {
531 QRect toTheRightOfRect = visualRect(category->lastItem());
532 line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight());
533 }
534 else
535 {
536 QRect toTheLeftOfRect = visualRect(model()->index(row, 0));
537 line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft());
538 }
539 painter.save();
540 painter.setPen(QPen(Qt::black, 3));
541 painter.drawLine(line);
542 painter.restore();
543 }
544 }
545 #endif
546 }
547
resizeEvent(QResizeEvent * event)548 void GroupView::resizeEvent(QResizeEvent *event)
549 {
550 int newItemsPerRow = calculateItemsPerRow();
551 if(newItemsPerRow != m_currentItemsPerRow)
552 {
553 m_currentCursorColumn = -1;
554 m_currentItemsPerRow = newItemsPerRow;
555 updateGeometries();
556 }
557 else
558 {
559 updateScrollbar();
560 }
561 }
562
dragEnterEvent(QDragEnterEvent * event)563 void GroupView::dragEnterEvent(QDragEnterEvent *event)
564 {
565 executeDelayedItemsLayout();
566
567 if (!isDragEventAccepted(event))
568 {
569 return;
570 }
571 m_lastDragPosition = event->pos() + offset();
572 viewport()->update();
573 event->accept();
574 }
575
dragMoveEvent(QDragMoveEvent * event)576 void GroupView::dragMoveEvent(QDragMoveEvent *event)
577 {
578 executeDelayedItemsLayout();
579
580 if (!isDragEventAccepted(event))
581 {
582 return;
583 }
584 m_lastDragPosition = event->pos() + offset();
585 viewport()->update();
586 event->accept();
587 }
588
dragLeaveEvent(QDragLeaveEvent * event)589 void GroupView::dragLeaveEvent(QDragLeaveEvent *event)
590 {
591 executeDelayedItemsLayout();
592
593 m_lastDragPosition = QPoint();
594 viewport()->update();
595 }
596
dropEvent(QDropEvent * event)597 void GroupView::dropEvent(QDropEvent *event)
598 {
599 executeDelayedItemsLayout();
600
601 m_lastDragPosition = QPoint();
602
603 stopAutoScroll();
604 setState(NoState);
605
606 if (event->source() == this)
607 {
608 if(event->possibleActions() & Qt::MoveAction)
609 {
610 QPair<VisualGroup *, int> dropPos = rowDropPos(event->pos() + offset());
611 const VisualGroup *category = dropPos.first;
612 const int row = dropPos.second;
613
614 if (row == -1)
615 {
616 viewport()->update();
617 return;
618 }
619
620 const QString categoryText = category->text;
621 if (model()->dropMimeData(event->mimeData(), Qt::MoveAction, row, 0, QModelIndex()))
622 {
623 model()->setData(model()->index(row, 0), categoryText, GroupViewRoles::GroupRole);
624 event->setDropAction(Qt::MoveAction);
625 event->accept();
626 }
627 updateGeometries();
628 viewport()->update();
629 }
630 }
631 auto mimedata = event->mimeData();
632
633 // check if the action is supported
634 if (!mimedata)
635 {
636 return;
637 }
638
639 // files dropped from outside?
640 if (mimedata->hasUrls())
641 {
642 auto urls = mimedata->urls();
643 event->accept();
644 emit droppedURLs(urls);
645 }
646 }
647
startDrag(Qt::DropActions supportedActions)648 void GroupView::startDrag(Qt::DropActions supportedActions)
649 {
650 executeDelayedItemsLayout();
651
652 QModelIndexList indexes = selectionModel()->selectedIndexes();
653 if(indexes.count() == 0)
654 return;
655
656 QMimeData *data = model()->mimeData(indexes);
657 if (!data)
658 {
659 return;
660 }
661 QRect rect;
662 QPixmap pixmap = renderToPixmap(indexes, &rect);
663 //rect.translate(offset());
664 // rect.adjust(horizontalOffset(), verticalOffset(), 0, 0);
665 QDrag *drag = new QDrag(this);
666 drag->setPixmap(pixmap);
667 drag->setMimeData(data);
668 Qt::DropAction defaultDropAction = Qt::IgnoreAction;
669 if (this->defaultDropAction() != Qt::IgnoreAction &&
670 (supportedActions & this->defaultDropAction()))
671 {
672 defaultDropAction = this->defaultDropAction();
673 }
674 if (drag->exec(supportedActions, defaultDropAction) == Qt::MoveAction)
675 {
676 const QItemSelection selection = selectionModel()->selection();
677
678 for (auto it = selection.constBegin(); it != selection.constEnd(); ++it)
679 {
680 QModelIndex parent = (*it).parent();
681 if ((*it).left() != 0)
682 {
683 continue;
684 }
685 if ((*it).right() != (model()->columnCount(parent) - 1))
686 {
687 continue;
688 }
689 int count = (*it).bottom() - (*it).top() + 1;
690 model()->removeRows((*it).top(), count, parent);
691 }
692 }
693 }
694
visualRect(const QModelIndex & index) const695 QRect GroupView::visualRect(const QModelIndex &index) const
696 {
697 const_cast<GroupView*>(this)->executeDelayedItemsLayout();
698
699 return geometryRect(index).translated(-offset());
700 }
701
geometryRect(const QModelIndex & index) const702 QRect GroupView::geometryRect(const QModelIndex &index) const
703 {
704 const_cast<GroupView*>(this)->executeDelayedItemsLayout();
705
706 if (!index.isValid() || isIndexHidden(index) || index.column() > 0)
707 {
708 return QRect();
709 }
710
711 int row = index.row();
712 if(geometryCache.contains(row))
713 {
714 return *geometryCache[row];
715 }
716
717 const VisualGroup *cat = category(index);
718 QPair<int, int> pos = cat->positionOf(index);
719 int x = pos.first;
720 // int y = pos.second;
721
722 QRect out;
723 out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index));
724 out.setLeft(m_spacing + x * (itemWidth() + m_spacing));
725 out.setSize(itemDelegate()->sizeHint(viewOptions(), index));
726 geometryCache.insert(row, new QRect(out));
727 return out;
728 }
729
indexAt(const QPoint & point) const730 QModelIndex GroupView::indexAt(const QPoint &point) const
731 {
732 const_cast<GroupView*>(this)->executeDelayedItemsLayout();
733
734 for (int i = 0; i < model()->rowCount(); ++i)
735 {
736 QModelIndex index = model()->index(i, 0);
737 if (visualRect(index).contains(point))
738 {
739 return index;
740 }
741 }
742 return QModelIndex();
743 }
744
setSelection(const QRect & rect,const QItemSelectionModel::SelectionFlags commands)745 void GroupView::setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands)
746 {
747 executeDelayedItemsLayout();
748
749 for (int i = 0; i < model()->rowCount(); ++i)
750 {
751 QModelIndex index = model()->index(i, 0);
752 QRect itemRect = visualRect(index);
753 if (itemRect.intersects(rect))
754 {
755 selectionModel()->select(index, commands);
756 update(itemRect.translated(-offset()));
757 }
758 }
759 }
760
renderToPixmap(const QModelIndexList & indices,QRect * r) const761 QPixmap GroupView::renderToPixmap(const QModelIndexList &indices, QRect *r) const
762 {
763 Q_ASSERT(r);
764 auto paintPairs = draggablePaintPairs(indices, r);
765 if (paintPairs.isEmpty())
766 {
767 return QPixmap();
768 }
769 QPixmap pixmap(r->size());
770 pixmap.fill(Qt::transparent);
771 QPainter painter(&pixmap);
772 QStyleOptionViewItem option = viewOptions();
773 option.state |= QStyle::State_Selected;
774 for (int j = 0; j < paintPairs.count(); ++j)
775 {
776 option.rect = paintPairs.at(j).first.translated(-r->topLeft());
777 const QModelIndex ¤t = paintPairs.at(j).second;
778 itemDelegate()->paint(&painter, option, current);
779 }
780 return pixmap;
781 }
782
draggablePaintPairs(const QModelIndexList & indices,QRect * r) const783 QList<QPair<QRect, QModelIndex>> GroupView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const
784 {
785 Q_ASSERT(r);
786 QRect &rect = *r;
787 QList<QPair<QRect, QModelIndex>> ret;
788 for (int i = 0; i < indices.count(); ++i)
789 {
790 const QModelIndex &index = indices.at(i);
791 const QRect current = geometryRect(index);
792 ret += qMakePair(current, index);
793 rect |= current;
794 }
795 return ret;
796 }
797
isDragEventAccepted(QDropEvent * event)798 bool GroupView::isDragEventAccepted(QDropEvent *event)
799 {
800 return true;
801 }
802
rowDropPos(const QPoint & pos)803 QPair<VisualGroup *, int> GroupView::rowDropPos(const QPoint &pos)
804 {
805 return qMakePair<VisualGroup*, int>(nullptr, -1);
806 }
807
offset() const808 QPoint GroupView::offset() const
809 {
810 return QPoint(horizontalOffset(), verticalOffset());
811 }
812
visualRegionForSelection(const QItemSelection & selection) const813 QRegion GroupView::visualRegionForSelection(const QItemSelection &selection) const
814 {
815 QRegion region;
816 for (auto &range : selection)
817 {
818 int start_row = range.top();
819 int end_row = range.bottom();
820 for (int row = start_row; row <= end_row; ++row)
821 {
822 int start_column = range.left();
823 int end_column = range.right();
824 for (int column = start_column; column <= end_column; ++column)
825 {
826 QModelIndex index = model()->index(row, column, rootIndex());
827 region += visualRect(index); // OK
828 }
829 }
830 }
831 return region;
832 }
833
moveCursor(QAbstractItemView::CursorAction cursorAction,Qt::KeyboardModifiers modifiers)834 QModelIndex GroupView::moveCursor(QAbstractItemView::CursorAction cursorAction,
835 Qt::KeyboardModifiers modifiers)
836 {
837 auto current = currentIndex();
838 if(!current.isValid())
839 {
840 return current;
841 }
842 auto cat = category(current);
843 int group_index = m_groups.indexOf(cat);
844 if(group_index < 0)
845 return current;
846
847 auto real_group = m_groups[group_index];
848 int beginning_row = 0;
849 for(auto group: m_groups)
850 {
851 if(group == real_group)
852 break;
853 beginning_row += group->numRows();
854 }
855
856 QPair<int, int> pos = cat->positionOf(current);
857 int column = pos.first;
858 int row = pos.second;
859 if(m_currentCursorColumn < 0)
860 {
861 m_currentCursorColumn = column;
862 }
863 switch(cursorAction)
864 {
865 case MoveUp:
866 {
867 if(row == 0)
868 {
869 int prevgroupindex = group_index-1;
870 while(prevgroupindex >= 0)
871 {
872 auto prevgroup = m_groups[prevgroupindex];
873 if(prevgroup->collapsed)
874 {
875 prevgroupindex--;
876 continue;
877 }
878 int newRow = prevgroup->numRows() - 1;
879 int newRowSize = prevgroup->rows[newRow].size();
880 int newColumn = m_currentCursorColumn;
881 if (m_currentCursorColumn >= newRowSize)
882 {
883 newColumn = newRowSize - 1;
884 }
885 return prevgroup->rows[newRow][newColumn];
886 }
887 }
888 else
889 {
890 int newRow = row - 1;
891 int newRowSize = cat->rows[newRow].size();
892 int newColumn = m_currentCursorColumn;
893 if (m_currentCursorColumn >= newRowSize)
894 {
895 newColumn = newRowSize - 1;
896 }
897 return cat->rows[newRow][newColumn];
898 }
899 return current;
900 }
901 case MoveDown:
902 {
903 if(row == cat->rows.size() - 1)
904 {
905 int nextgroupindex = group_index+1;
906 while (nextgroupindex < m_groups.size())
907 {
908 auto nextgroup = m_groups[nextgroupindex];
909 if(nextgroup->collapsed)
910 {
911 nextgroupindex++;
912 continue;
913 }
914 int newRowSize = nextgroup->rows[0].size();
915 int newColumn = m_currentCursorColumn;
916 if (m_currentCursorColumn >= newRowSize)
917 {
918 newColumn = newRowSize - 1;
919 }
920 return nextgroup->rows[0][newColumn];
921 }
922 }
923 else
924 {
925 int newRow = row + 1;
926 int newRowSize = cat->rows[newRow].size();
927 int newColumn = m_currentCursorColumn;
928 if (m_currentCursorColumn >= newRowSize)
929 {
930 newColumn = newRowSize - 1;
931 }
932 return cat->rows[newRow][newColumn];
933 }
934 return current;
935 }
936 case MoveLeft:
937 {
938 if(column > 0)
939 {
940 m_currentCursorColumn = column - 1;
941 return cat->rows[row][column - 1];
942 }
943 // TODO: moving to previous line
944 return current;
945 }
946 case MoveRight:
947 {
948 if(column < cat->rows[row].size() - 1)
949 {
950 m_currentCursorColumn = column + 1;
951 return cat->rows[row][column + 1];
952 }
953 // TODO: moving to next line
954 return current;
955 }
956 case MoveHome:
957 {
958 m_currentCursorColumn = 0;
959 return cat->rows[row][0];
960 }
961 case MoveEnd:
962 {
963 auto last = cat->rows[row].size() - 1;
964 m_currentCursorColumn = last;
965 return cat->rows[row][last];
966 }
967 default:
968 break;
969 }
970 return current;
971 }
972
horizontalOffset() const973 int GroupView::horizontalOffset() const
974 {
975 return horizontalScrollBar()->value();
976 }
977
verticalOffset() const978 int GroupView::verticalOffset() const
979 {
980 return verticalScrollBar()->value();
981 }
982
scrollContentsBy(int dx,int dy)983 void GroupView::scrollContentsBy(int dx, int dy)
984 {
985 scrollDirtyRegion(dx, dy);
986 viewport()->scroll(dx, dy);
987 }
988
scrollTo(const QModelIndex & index,ScrollHint hint)989 void GroupView::scrollTo(const QModelIndex &index, ScrollHint hint)
990 {
991 if (!index.isValid())
992 return;
993
994 const QRect rect = visualRect(index);
995 if (hint == EnsureVisible && viewport()->rect().contains(rect))
996 {
997 viewport()->update(rect);
998 return;
999 }
1000
1001 verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint));
1002 }
1003
verticalScrollToValue(const QModelIndex & index,const QRect & rect,QListView::ScrollHint hint) const1004 int GroupView::verticalScrollToValue(const QModelIndex &index, const QRect &rect,
1005 QListView::ScrollHint hint) const
1006 {
1007 const QRect area = viewport()->rect();
1008 const bool above = (hint == QListView::EnsureVisible && rect.top() < area.top());
1009 const bool below = (hint == QListView::EnsureVisible && rect.bottom() > area.bottom());
1010
1011 int verticalValue = verticalScrollBar()->value();
1012 QRect adjusted = rect.adjusted(-spacing(), -spacing(), spacing(), spacing());
1013 if (hint == QListView::PositionAtTop || above)
1014 verticalValue += adjusted.top();
1015 else if (hint == QListView::PositionAtBottom || below)
1016 verticalValue += qMin(adjusted.top(), adjusted.bottom() - area.height() + 1);
1017 else if (hint == QListView::PositionAtCenter)
1018 verticalValue += adjusted.top() - ((area.height() - adjusted.height()) / 2);
1019 return verticalValue;
1020 }
1021