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