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 &current = 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