2011-06-25 18:47:00 +02:00
|
|
|
/* This file is part of Clementine.
|
|
|
|
Copyright 2010, David Sansome <me@davidsansome.com>
|
|
|
|
|
|
|
|
Clementine is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
Clementine is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with Clementine. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "groupediconview.h"
|
2011-07-26 13:17:28 +02:00
|
|
|
#include "core/multisortfilterproxy.h"
|
2011-06-25 18:47:00 +02:00
|
|
|
|
|
|
|
#include <QPainter>
|
|
|
|
#include <QPaintEvent>
|
|
|
|
#include <QScrollBar>
|
|
|
|
#include <QSortFilterProxyModel>
|
|
|
|
#include <QtDebug>
|
|
|
|
|
|
|
|
const int GroupedIconView::kBarThickness = 2;
|
|
|
|
const int GroupedIconView::kBarMarginTop = 3;
|
|
|
|
|
|
|
|
GroupedIconView::GroupedIconView(QWidget* parent)
|
2014-02-07 16:34:20 +01:00
|
|
|
: QListView(parent),
|
|
|
|
proxy_model_(new MultiSortFilterProxy(this)),
|
|
|
|
default_header_height_(fontMetrics().height() + kBarMarginTop +
|
|
|
|
kBarThickness),
|
|
|
|
header_spacing_(10),
|
|
|
|
header_indent_(5),
|
|
|
|
item_indent_(10),
|
|
|
|
header_text_("%1") {
|
2011-06-25 18:47:00 +02:00
|
|
|
setFlow(LeftToRight);
|
|
|
|
setViewMode(IconMode);
|
|
|
|
setResizeMode(Adjust);
|
|
|
|
setWordWrap(true);
|
|
|
|
setDragEnabled(false);
|
|
|
|
|
2011-07-26 13:17:28 +02:00
|
|
|
proxy_model_->AddSortSpec(Role_Group);
|
2011-06-25 18:47:00 +02:00
|
|
|
proxy_model_->setDynamicSortFilter(true);
|
|
|
|
|
|
|
|
connect(proxy_model_, SIGNAL(modelReset()), SLOT(LayoutItems()));
|
|
|
|
}
|
|
|
|
|
2011-07-26 13:17:28 +02:00
|
|
|
void GroupedIconView::AddSortSpec(int role, Qt::SortOrder order) {
|
|
|
|
proxy_model_->AddSortSpec(role, order);
|
|
|
|
}
|
|
|
|
|
2011-06-25 18:47:00 +02:00
|
|
|
void GroupedIconView::setModel(QAbstractItemModel* model) {
|
|
|
|
proxy_model_->setSourceModel(model);
|
|
|
|
proxy_model_->sort(0);
|
|
|
|
|
|
|
|
QListView::setModel(proxy_model_);
|
|
|
|
LayoutItems();
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
int GroupedIconView::header_height() const { return default_header_height_; }
|
2011-06-25 18:47:00 +02:00
|
|
|
|
2011-10-30 17:51:43 +01:00
|
|
|
void GroupedIconView::DrawHeader(QPainter* painter, const QRect& rect,
|
|
|
|
const QFont& font, const QPalette& palette,
|
|
|
|
const QString& text) {
|
2011-06-25 18:47:00 +02:00
|
|
|
painter->save();
|
|
|
|
|
|
|
|
// Bold font
|
2011-10-30 17:51:43 +01:00
|
|
|
QFont bold_font(font);
|
2011-06-25 18:47:00 +02:00
|
|
|
bold_font.setBold(true);
|
|
|
|
QFontMetrics metrics(bold_font);
|
|
|
|
|
2011-10-30 17:51:43 +01:00
|
|
|
QRect text_rect(rect);
|
|
|
|
text_rect.setHeight(metrics.height());
|
2014-02-07 16:34:20 +01:00
|
|
|
text_rect.moveTop(
|
|
|
|
rect.top() +
|
|
|
|
(rect.height() - text_rect.height() - kBarThickness - kBarMarginTop) / 2);
|
2011-10-30 17:51:43 +01:00
|
|
|
text_rect.setLeft(text_rect.left() + 3);
|
|
|
|
|
2011-06-25 18:47:00 +02:00
|
|
|
// Draw text
|
|
|
|
painter->setFont(bold_font);
|
2011-10-30 17:51:43 +01:00
|
|
|
painter->drawText(text_rect, text);
|
2011-06-25 18:47:00 +02:00
|
|
|
|
|
|
|
// Draw a line underneath
|
2011-10-30 17:51:43 +01:00
|
|
|
const QPoint start(rect.left(), text_rect.bottom() + kBarMarginTop);
|
2011-06-25 18:47:00 +02:00
|
|
|
const QPoint end(rect.right(), start.y());
|
|
|
|
|
|
|
|
painter->setRenderHint(QPainter::Antialiasing, true);
|
2011-10-30 17:51:43 +01:00
|
|
|
painter->setPen(QPen(palette.color(QPalette::Disabled, QPalette::Text),
|
2011-06-25 18:47:00 +02:00
|
|
|
kBarThickness, Qt::SolidLine, Qt::RoundCap));
|
|
|
|
painter->setOpacity(0.5);
|
|
|
|
painter->drawLine(start, end);
|
|
|
|
|
|
|
|
painter->restore();
|
|
|
|
}
|
|
|
|
|
|
|
|
void GroupedIconView::resizeEvent(QResizeEvent* e) {
|
|
|
|
QListView::resizeEvent(e);
|
|
|
|
LayoutItems();
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GroupedIconView::rowsInserted(const QModelIndex& parent, int start,
|
|
|
|
int end) {
|
2011-06-25 18:47:00 +02:00
|
|
|
QListView::rowsInserted(parent, start, end);
|
|
|
|
LayoutItems();
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GroupedIconView::dataChanged(const QModelIndex& topLeft,
|
|
|
|
const QModelIndex& bottomRight) {
|
2011-06-26 17:06:59 +02:00
|
|
|
QListView::dataChanged(topLeft, bottomRight);
|
|
|
|
LayoutItems();
|
|
|
|
}
|
|
|
|
|
2011-06-25 18:47:00 +02:00
|
|
|
void GroupedIconView::LayoutItems() {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (!model()) return;
|
2011-06-25 18:47:00 +02:00
|
|
|
|
|
|
|
const int count = model()->rowCount();
|
|
|
|
|
|
|
|
QString last_group;
|
|
|
|
QPoint next_position(0, 0);
|
|
|
|
int max_row_height = 0;
|
|
|
|
|
|
|
|
visual_rects_.clear();
|
|
|
|
visual_rects_.reserve(count);
|
|
|
|
headers_.clear();
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < count; ++i) {
|
2011-06-25 18:47:00 +02:00
|
|
|
const QModelIndex index(model()->index(i, 0));
|
|
|
|
const QString group = index.data(Role_Group).toString();
|
|
|
|
const QSize size(rectForIndex(index).size());
|
|
|
|
|
|
|
|
// Is this the first item in a new group?
|
|
|
|
if (group != last_group) {
|
|
|
|
// Add the group header.
|
|
|
|
Header header;
|
|
|
|
header.y = next_position.y() + max_row_height + header_indent_;
|
|
|
|
header.first_row = i;
|
|
|
|
header.text = group;
|
|
|
|
|
|
|
|
if (!last_group.isNull()) {
|
|
|
|
header.y += header_spacing_;
|
|
|
|
}
|
|
|
|
|
|
|
|
headers_ << header;
|
|
|
|
|
|
|
|
// Remember this group so we don't add it again.
|
|
|
|
last_group = group;
|
|
|
|
|
|
|
|
// Move the next item immediately below the header.
|
|
|
|
next_position.setX(0);
|
2014-02-07 16:34:20 +01:00
|
|
|
next_position.setY(header.y + header_height() + header_indent_ +
|
|
|
|
header_spacing_);
|
2011-06-25 18:47:00 +02:00
|
|
|
max_row_height = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Take into account padding and spacing
|
|
|
|
QPoint this_position(next_position);
|
|
|
|
if (this_position.x() == 0) {
|
|
|
|
this_position.setX(this_position.x() + item_indent_);
|
|
|
|
} else {
|
|
|
|
this_position.setX(this_position.x() + spacing());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Should this item wrap?
|
2014-02-07 16:34:20 +01:00
|
|
|
if (next_position.x() != 0 &&
|
|
|
|
this_position.x() + size.width() >= viewport()->width()) {
|
2011-06-25 18:47:00 +02:00
|
|
|
next_position.setX(0);
|
|
|
|
next_position.setY(next_position.y() + max_row_height);
|
|
|
|
this_position = next_position;
|
|
|
|
this_position.setX(this_position.x() + item_indent_);
|
|
|
|
|
|
|
|
max_row_height = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set this item's geometry
|
|
|
|
visual_rects_.append(QRect(this_position, size));
|
|
|
|
|
|
|
|
// Update next index
|
|
|
|
next_position.setX(this_position.x() + size.width());
|
|
|
|
max_row_height = qMax(max_row_height, size.height());
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
verticalScrollBar()->setRange(
|
|
|
|
0, next_position.y() + max_row_height - viewport()->height());
|
2011-06-25 18:47:00 +02:00
|
|
|
update();
|
|
|
|
}
|
|
|
|
|
|
|
|
QRect GroupedIconView::visualRect(const QModelIndex& index) const {
|
2014-02-07 16:34:20 +01:00
|
|
|
if (index.row() < 0 || index.row() >= visual_rects_.count()) return QRect();
|
|
|
|
return visual_rects_[index.row()].translated(-horizontalOffset(),
|
|
|
|
-verticalOffset());
|
2011-06-25 18:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
QModelIndex GroupedIconView::indexAt(const QPoint& p) const {
|
|
|
|
const QPoint viewport_p = p + QPoint(horizontalOffset(), verticalOffset());
|
|
|
|
|
|
|
|
const int count = visual_rects_.count();
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < count; ++i) {
|
2011-06-25 18:47:00 +02:00
|
|
|
if (visual_rects_[i].contains(viewport_p)) {
|
|
|
|
return model()->index(i, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return QModelIndex();
|
|
|
|
}
|
|
|
|
|
|
|
|
void GroupedIconView::paintEvent(QPaintEvent* e) {
|
|
|
|
// This code was adapted from QListView::paintEvent(), changed to use the
|
|
|
|
// visualRect() of items, and to draw headers.
|
|
|
|
|
|
|
|
QStyleOptionViewItemV4 option(viewOptions());
|
2014-02-07 16:34:20 +01:00
|
|
|
if (isWrapping()) option.features = QStyleOptionViewItemV2::WrapText;
|
2011-06-25 18:47:00 +02:00
|
|
|
option.locale = locale();
|
|
|
|
option.locale.setNumberOptions(QLocale::OmitGroupSeparator);
|
|
|
|
option.widget = this;
|
|
|
|
|
|
|
|
QPainter painter(viewport());
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
const QRect viewport_rect(
|
|
|
|
e->rect().translated(horizontalOffset(), verticalOffset()));
|
2011-06-25 18:47:00 +02:00
|
|
|
QVector<QModelIndex> toBeRendered = IntersectingItems(viewport_rect);
|
|
|
|
|
|
|
|
const QModelIndex current = currentIndex();
|
2014-02-07 16:34:20 +01:00
|
|
|
const QAbstractItemModel* itemModel = model();
|
|
|
|
const QItemSelectionModel* selections = selectionModel();
|
|
|
|
const bool focus =
|
|
|
|
(hasFocus() || viewport()->hasFocus()) && current.isValid();
|
2011-06-25 18:47:00 +02:00
|
|
|
const QStyle::State state = option.state;
|
|
|
|
const QAbstractItemView::State viewState = this->state();
|
|
|
|
const bool enabled = (state & QStyle::State_Enabled) != 0;
|
|
|
|
|
|
|
|
int maxSize = (flow() == TopToBottom)
|
2014-02-07 16:34:20 +01:00
|
|
|
? viewport()->size().width() - 2 * spacing()
|
|
|
|
: viewport()->size().height() - 2 * spacing();
|
2011-06-25 18:47:00 +02:00
|
|
|
|
|
|
|
QVector<QModelIndex>::const_iterator end = toBeRendered.constEnd();
|
2014-02-07 16:34:20 +01:00
|
|
|
for (QVector<QModelIndex>::const_iterator it = toBeRendered.constBegin();
|
|
|
|
it != end; ++it) {
|
2011-06-25 18:47:00 +02:00
|
|
|
if (!it->isValid()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
option.rect = visualRect(*it);
|
|
|
|
|
|
|
|
if (flow() == TopToBottom)
|
|
|
|
option.rect.setWidth(qMin(maxSize, option.rect.width()));
|
|
|
|
else
|
|
|
|
option.rect.setHeight(qMin(maxSize, option.rect.height()));
|
|
|
|
|
|
|
|
option.state = state;
|
|
|
|
if (selections && selections->isSelected(*it))
|
|
|
|
option.state |= QStyle::State_Selected;
|
|
|
|
if (enabled) {
|
|
|
|
QPalette::ColorGroup cg;
|
|
|
|
if ((itemModel->flags(*it) & Qt::ItemIsEnabled) == 0) {
|
|
|
|
option.state &= ~QStyle::State_Enabled;
|
|
|
|
cg = QPalette::Disabled;
|
|
|
|
} else {
|
|
|
|
cg = QPalette::Normal;
|
|
|
|
}
|
|
|
|
option.palette.setCurrentColorGroup(cg);
|
|
|
|
}
|
|
|
|
if (focus && current == *it) {
|
|
|
|
option.state |= QStyle::State_HasFocus;
|
2014-02-07 16:34:20 +01:00
|
|
|
if (viewState == EditingState) option.state |= QStyle::State_Editing;
|
2011-06-25 18:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
itemDelegate()->paint(&painter, option, *it);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw headers
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const Header& header : headers_) {
|
2014-02-07 16:34:20 +01:00
|
|
|
const QRect header_rect =
|
|
|
|
QRect(header_indent_, header.y,
|
|
|
|
viewport()->width() - header_indent_ * 2, header_height());
|
2011-06-25 18:47:00 +02:00
|
|
|
|
|
|
|
// Is this header contained in the area we're drawing?
|
|
|
|
if (!header_rect.intersects(viewport_rect)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw the header
|
2011-10-30 17:51:43 +01:00
|
|
|
DrawHeader(&painter,
|
2011-06-25 18:47:00 +02:00
|
|
|
header_rect.translated(-horizontalOffset(), -verticalOffset()),
|
2014-02-07 16:34:20 +01:00
|
|
|
font(), palette(),
|
2011-10-30 17:51:43 +01:00
|
|
|
model()->index(header.first_row, 0).data(Role_Group).toString());
|
2011-06-25 18:47:00 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
void GroupedIconView::setSelection(
|
|
|
|
const QRect& rect, QItemSelectionModel::SelectionFlags command) {
|
|
|
|
QVector<QModelIndex> indexes(
|
|
|
|
IntersectingItems(rect.translated(horizontalOffset(), verticalOffset())));
|
2011-06-25 18:47:00 +02:00
|
|
|
QItemSelection selection;
|
|
|
|
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QModelIndex& index : indexes) {
|
2011-06-25 18:47:00 +02:00
|
|
|
selection << QItemSelectionRange(index);
|
|
|
|
}
|
|
|
|
|
|
|
|
selectionModel()->select(selection, command);
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
QVector<QModelIndex> GroupedIconView::IntersectingItems(const QRect& rect)
|
|
|
|
const {
|
2011-06-25 18:47:00 +02:00
|
|
|
QVector<QModelIndex> ret;
|
|
|
|
|
|
|
|
const int count = visual_rects_.count();
|
2014-02-07 16:34:20 +01:00
|
|
|
for (int i = 0; i < count; ++i) {
|
2011-06-25 18:47:00 +02:00
|
|
|
if (rect.intersects(visual_rects_[i])) {
|
|
|
|
ret.append(model()->index(i, 0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
QRegion GroupedIconView::visualRegionForSelection(
|
|
|
|
const QItemSelection& selection) const {
|
2011-06-25 18:47:00 +02:00
|
|
|
QRegion ret;
|
2014-02-10 14:29:07 +01:00
|
|
|
for (const QModelIndex& index : selection.indexes()) {
|
2011-06-25 18:47:00 +02:00
|
|
|
ret += visual_rects_[index.row()];
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2014-02-07 16:34:20 +01:00
|
|
|
QModelIndex GroupedIconView::moveCursor(CursorAction action,
|
|
|
|
Qt::KeyboardModifiers) {
|
2011-06-25 18:47:00 +02:00
|
|
|
if (model()->rowCount() == 0) {
|
|
|
|
return QModelIndex();
|
|
|
|
}
|
|
|
|
|
|
|
|
int ret = currentIndex().row();
|
|
|
|
if (ret == -1) {
|
|
|
|
ret = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (action) {
|
2014-02-07 16:34:20 +01:00
|
|
|
case MoveUp:
|
|
|
|
ret = IndexAboveOrBelow(ret, -1);
|
|
|
|
break;
|
2011-06-25 18:47:00 +02:00
|
|
|
case MovePrevious:
|
2014-02-07 16:34:20 +01:00
|
|
|
case MoveLeft:
|
|
|
|
ret--;
|
|
|
|
break;
|
|
|
|
case MoveDown:
|
|
|
|
ret = IndexAboveOrBelow(ret, +1);
|
|
|
|
break;
|
2011-06-25 18:47:00 +02:00
|
|
|
case MoveNext:
|
2014-02-07 16:34:20 +01:00
|
|
|
case MoveRight:
|
|
|
|
ret++;
|
|
|
|
break;
|
2011-06-25 18:47:00 +02:00
|
|
|
case MovePageUp:
|
2014-02-07 16:34:20 +01:00
|
|
|
case MoveHome:
|
|
|
|
ret = 0;
|
|
|
|
break;
|
2011-06-25 18:47:00 +02:00
|
|
|
case MovePageDown:
|
2014-02-07 16:34:20 +01:00
|
|
|
case MoveEnd:
|
|
|
|
ret = model()->rowCount() - 1;
|
|
|
|
break;
|
2011-06-25 18:47:00 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return model()->index(qBound(0, ret, model()->rowCount()), 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
int GroupedIconView::IndexAboveOrBelow(int index, int d) const {
|
|
|
|
const QRect orig_rect(visual_rects_[index]);
|
|
|
|
|
|
|
|
while (index >= 0 && index < visual_rects_.count()) {
|
|
|
|
const QRect rect(visual_rects_[index]);
|
|
|
|
const QPoint center(rect.center());
|
|
|
|
|
|
|
|
if ((center.y() <= orig_rect.top() || center.y() >= orig_rect.bottom()) &&
|
2014-02-07 16:34:20 +01:00
|
|
|
center.x() >= orig_rect.left() && center.x() <= orig_rect.right()) {
|
2011-06-25 18:47:00 +02:00
|
|
|
return index;
|
|
|
|
}
|
|
|
|
|
|
|
|
index += d;
|
|
|
|
}
|
|
|
|
|
|
|
|
return index;
|
|
|
|
}
|