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