1 /*
2  * Strawberry Music Player
3  * This file was part of Clementine.
4  * Copyright 2010, David Sansome <me@davidsansome.com>
5  *
6  * Strawberry is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Strawberry is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Strawberry.  If not, see <http://www.gnu.org/licenses/>.
18  *
19  */
20 
21 #include "config.h"
22 
23 #include <QtGlobal>
24 #include <QWidget>
25 #include <QListView>
26 #include <QAbstractItemModel>
27 #include <QAbstractItemView>
28 #include <QAbstractItemDelegate>
29 #include <QItemSelectionModel>
30 #include <QList>
31 #include <QArrayData>
32 #include <QVariant>
33 #include <QString>
34 #include <QFont>
35 #include <QFontMetrics>
36 #include <QLocale>
37 #include <QPainter>
38 #include <QPalette>
39 #include <QVector>
40 #include <QRect>
41 #include <QPen>
42 #include <QPoint>
43 #include <QScrollBar>
44 #include <QSize>
45 #include <QStyle>
46 #include <QStyleOption>
47 #include <QFlags>
48 #include <QtEvents>
49 
50 #include "core/multisortfilterproxy.h"
51 #include "groupediconview.h"
52 
53 const int GroupedIconView::kBarThickness = 2;
54 const int GroupedIconView::kBarMarginTop = 3;
55 
GroupedIconView(QWidget * parent)56 GroupedIconView::GroupedIconView(QWidget *parent)
57     : QListView(parent),
58       proxy_model_(new MultiSortFilterProxy(this)),
59       default_header_height_(fontMetrics().height() +
60       kBarMarginTop + kBarThickness),
61       header_spacing_(10),
62       header_indent_(5),
63       item_indent_(10),
64       header_text_("%1") {
65 
66   setFlow(LeftToRight);
67   setViewMode(IconMode);
68   setResizeMode(Adjust);
69   setWordWrap(true);
70   setDragEnabled(false);
71 
72   proxy_model_->AddSortSpec(Role_Group);
73   proxy_model_->setDynamicSortFilter(true);
74 
75   QObject::connect(proxy_model_, &MultiSortFilterProxy::modelReset, this, &GroupedIconView::LayoutItems);
76 
77 }
78 
AddSortSpec(const int role,const Qt::SortOrder order)79 void GroupedIconView::AddSortSpec(const int role, const Qt::SortOrder order) {
80   proxy_model_->AddSortSpec(role, order);
81 }
82 
setModel(QAbstractItemModel * model)83 void GroupedIconView::setModel(QAbstractItemModel *model) {
84 
85   proxy_model_->setSourceModel(model);
86   proxy_model_->sort(0);
87 
88   QListView::setModel(proxy_model_);
89   LayoutItems();
90 
91 }
92 
header_height() const93 int GroupedIconView::header_height() const {
94   return default_header_height_;
95 }
96 
DrawHeader(QPainter * painter,const QRect rect,const QFont & font,const QPalette & palette,const QString & text)97 void GroupedIconView::DrawHeader(QPainter *painter, const QRect rect, const QFont &font, const QPalette &palette, const QString &text) {
98 
99   painter->save();
100 
101   // Bold font
102   QFont bold_font(font);
103   bold_font.setBold(true);
104   QFontMetrics metrics(bold_font);
105 
106   QRect text_rect(rect);
107   text_rect.setHeight(metrics.height());
108   text_rect.moveTop(rect.top() + (rect.height() - text_rect.height() - kBarThickness - kBarMarginTop) / 2);
109   text_rect.setLeft(text_rect.left() + 3);
110 
111   // Draw text
112   painter->setFont(bold_font);
113   painter->drawText(text_rect, text);
114 
115   // Draw a line underneath
116   const QPoint start(rect.left(), text_rect.bottom() + kBarMarginTop);
117   const QPoint end(rect.right(), start.y());
118 
119   painter->setRenderHint(QPainter::Antialiasing, true);
120   painter->setPen(QPen(palette.color(QPalette::Disabled, QPalette::Text), kBarThickness, Qt::SolidLine, Qt::RoundCap));
121   painter->setOpacity(0.5);
122   painter->drawLine(start, end);
123 
124   painter->restore();
125 }
126 
resizeEvent(QResizeEvent * e)127 void GroupedIconView::resizeEvent(QResizeEvent *e) {
128   QListView::resizeEvent(e);
129   LayoutItems();
130 }
131 
rowsInserted(const QModelIndex & parent,int start,int end)132 void GroupedIconView::rowsInserted(const QModelIndex &parent, int start, int end) {
133   QListView::rowsInserted(parent, start, end);
134   LayoutItems();
135 }
136 
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> &)137 void GroupedIconView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int>&) {
138   QListView::dataChanged(topLeft, bottomRight);
139   LayoutItems();
140 }
141 
LayoutItems()142 void GroupedIconView::LayoutItems() {
143 
144   if (!model()) {
145     return;
146   }
147 
148   const int count = model()->rowCount();
149 
150   QString last_group;
151   QPoint next_position(0, 0);
152   int max_row_height = 0;
153 
154   visual_rects_.clear();
155   visual_rects_.reserve(count);
156   headers_.clear();
157 
158   for (int i = 0; i < count; ++i) {
159     const QModelIndex idx(model()->index(i, 0));
160     const QString group = idx.data(Role_Group).toString();
161     const QSize size(rectForIndex(idx).size());
162 
163     // Is this the first item in a new group?
164     if (group != last_group) {
165       // Add the group header.
166       Header header;
167       header.y = next_position.y() + max_row_height + header_indent_;
168       header.first_row = i;
169       header.text = group;
170 
171       if (!last_group.isNull()) {
172         header.y += header_spacing_;
173       }
174 
175       headers_ << header;
176 
177       // Remember this group so we don't add it again.
178       last_group = group;
179 
180       // Move the next item immediately below the header.
181       next_position.setX(0);
182       next_position.setY(header.y + header_height() + header_indent_ + header_spacing_);
183       max_row_height = 0;
184     }
185 
186     // Take into account padding and spacing
187     QPoint this_position(next_position);
188     if (this_position.x() == 0) {
189       this_position.setX(this_position.x() + item_indent_);
190     }
191     else {
192       this_position.setX(this_position.x() + spacing());
193     }
194 
195     // Should this item wrap?
196     if (next_position.x() != 0 && this_position.x() + size.width() >= viewport()->width()) {
197       next_position.setX(0);
198       next_position.setY(next_position.y() + max_row_height);
199       this_position = next_position;
200       this_position.setX(this_position.x() + item_indent_);
201 
202       max_row_height = 0;
203     }
204 
205     // Set this item's geometry
206     visual_rects_.append(QRect(this_position, size));
207 
208     // Update next index
209     next_position.setX(this_position.x() + size.width());
210     max_row_height = qMax(max_row_height, size.height());
211   }
212 
213   verticalScrollBar()->setRange(0, next_position.y() + max_row_height - viewport()->height());
214   update();
215 
216 }
217 
visualRect(const QModelIndex & idx) const218 QRect GroupedIconView::visualRect(const QModelIndex &idx) const {
219 
220   if (idx.row() < 0 || idx.row() >= visual_rects_.count()) {
221     return QRect();
222   }
223   return visual_rects_[idx.row()].translated(-horizontalOffset(), -verticalOffset());
224 
225 }
226 
indexAt(const QPoint & p) const227 QModelIndex GroupedIconView::indexAt(const QPoint &p) const {
228 
229   const QPoint viewport_p = p + QPoint(horizontalOffset(), verticalOffset());
230 
231   const int count = visual_rects_.count();
232   for (int i = 0; i<count; ++i) {
233     if (visual_rects_[i].contains(viewport_p)) {
234       return model()->index(i, 0);
235     }
236   }
237   return QModelIndex();
238 
239 }
240 
paintEvent(QPaintEvent * e)241 void GroupedIconView::paintEvent(QPaintEvent *e) {
242 
243   // This code was adapted from QListView::paintEvent(), changed to use the visualRect() of items, and to draw headers.
244 
245   QStyleOptionViewItem option;
246 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
247   initViewItemOption(&option);
248 #else
249   option = viewOptions();
250 #endif
251   if (isWrapping()) {
252     option.features = QStyleOptionViewItem::WrapText;
253   }
254   option.locale = locale();
255   option.locale.setNumberOptions(QLocale::OmitGroupSeparator);
256   option.widget = this;
257 
258   QPainter painter(viewport());
259 
260   const QRect viewport_rect(e->rect().translated(horizontalOffset(), verticalOffset()));
261   QVector<QModelIndex> toBeRendered = IntersectingItems(viewport_rect);
262 
263   const QModelIndex current = currentIndex();
264   const QAbstractItemModel *itemModel = model();
265   const QItemSelectionModel *selections = selectionModel();
266   const bool focus = (hasFocus() || viewport()->hasFocus()) && current.isValid();
267   const QStyle::State opt_state = option.state;
268   const QAbstractItemView::State viewState = state();
269   const bool enabled = (opt_state & QStyle::State_Enabled) != 0;
270 
271   int maxSize = (flow() == TopToBottom) ? viewport()->size().width() - 2 * spacing() : viewport()->size().height() - 2 * spacing();
272 
273   QVector<QModelIndex>::const_iterator end = toBeRendered.constEnd();
274   for (QVector<QModelIndex>::const_iterator it = toBeRendered.constBegin(); it != end; ++it) {
275     if (!it->isValid()) {
276       continue;
277     }
278 
279     option.rect = visualRect(*it);
280 
281     if (flow() == TopToBottom) {
282       option.rect.setWidth(qMin(maxSize, option.rect.width()));
283     }
284     else {
285       option.rect.setHeight(qMin(maxSize, option.rect.height()));
286     }
287 
288     option.state = opt_state;
289     if (selections && selections->isSelected(*it))
290       option.state |= QStyle::State_Selected;
291     if (enabled) {
292       QPalette::ColorGroup cg = QPalette::Active;
293       if ((itemModel->flags(*it) & Qt::ItemIsEnabled) == 0) {
294         option.state &= ~QStyle::State_Enabled;
295         cg = QPalette::Disabled;
296       }
297       else {
298         cg = QPalette::Normal;
299       }
300       option.palette.setCurrentColorGroup(cg);
301     }
302     if (focus && current == *it) {
303       option.state |= QStyle::State_HasFocus;
304       if (viewState == EditingState) {
305         option.state |= QStyle::State_Editing;
306       }
307     }
308 
309     itemDelegate()->paint(&painter, option, *it);
310   }
311 
312   // Draw headers
313   for (const Header &header : headers_) {
314     const QRect header_rect = QRect(header_indent_, header.y, viewport()->width() - header_indent_ * 2, header_height());
315 
316     // Is this header contained in the area we're drawing?
317     if (!header_rect.intersects(viewport_rect)) {
318       continue;
319     }
320 
321     // Draw the header
322     DrawHeader(&painter,
323                header_rect.translated(-horizontalOffset(), -verticalOffset()),
324                font(),
325                palette(),
326                model()->index(header.first_row, 0).data(Role_Group).toString());
327   }
328 
329 }
330 
setSelection(const QRect & rect,QItemSelectionModel::SelectionFlags command)331 void GroupedIconView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) {
332 
333   QVector<QModelIndex> indexes(IntersectingItems(rect.translated(horizontalOffset(), verticalOffset())));
334 
335   QItemSelection selection;
336   selection.reserve(indexes.count());
337 
338   for (const QModelIndex &idx : indexes) {
339     selection << QItemSelectionRange(idx);
340   }
341 
342   selectionModel()->select(selection, command);
343 
344 }
345 
IntersectingItems(const QRect rect) const346 QVector<QModelIndex> GroupedIconView::IntersectingItems(const QRect rect) const {
347 
348   QVector<QModelIndex> ret;
349 
350   const int count = visual_rects_.count();
351   for (int i = 0; i < count; ++i) {
352     if (rect.intersects(visual_rects_[i])) {
353       ret.append(model()->index(i, 0));
354     }
355   }
356 
357   return ret;
358 
359 }
360 
visualRegionForSelection(const QItemSelection & selection) const361 QRegion GroupedIconView::visualRegionForSelection(const QItemSelection &selection) const {
362 
363   QRegion ret;
364   for (const QModelIndex &idx : selection.indexes()) {
365     ret += visual_rects_[idx.row()];
366   }
367   return ret;
368 
369 }
370 
moveCursor(CursorAction action,Qt::KeyboardModifiers)371 QModelIndex GroupedIconView::moveCursor(CursorAction action, Qt::KeyboardModifiers) {
372 
373   if (model()->rowCount() == 0) {
374     return QModelIndex();
375   }
376 
377   int ret = currentIndex().row();
378   if (ret == -1) {
379     ret = 0;
380   }
381 
382   switch (action) {
383     case MoveUp:    ret = IndexAboveOrBelow(ret, -1); break;
384     case MovePrevious:
385     case MoveLeft:  ret --; break;
386     case MoveDown:  ret = IndexAboveOrBelow(ret, +1); break;
387     case MoveNext:
388     case MoveRight: ret ++; break;
389     case MovePageUp:
390     case MoveHome:  ret = 0; break;
391     case MovePageDown:
392     case MoveEnd:   ret = model()->rowCount() - 1; break;
393   }
394 
395   return model()->index(qBound(0, ret, model()->rowCount()), 0);
396 
397 }
398 
IndexAboveOrBelow(int index,const int d) const399 int GroupedIconView::IndexAboveOrBelow(int index, const int d) const {
400 
401   const QRect orig_rect(visual_rects_[index]);
402 
403   while (index >= 0 && index < visual_rects_.count()) {
404     const QRect rect(visual_rects_[index]);
405     const QPoint center(rect.center());
406 
407     if ((center.y() <= orig_rect.top() || center.y() >= orig_rect.bottom()) && center.x() >= orig_rect.left() && center.x() <= orig_rect.right()) {
408       return index;
409     }
410 
411     index += d;
412   }
413 
414   return index;
415 
416 }
417