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