#include "CategorizedView.h" #include #include #include #include #include CategorizedView::Category::Category(const QString &text, CategorizedView *view) : view(view), text(text), collapsed(false) { } CategorizedView::Category::Category(const CategorizedView::Category *other) : view(other->view), text(other->text), collapsed(other->collapsed), iconRect(other->iconRect), textRect(other->textRect) { } void CategorizedView::Category::drawHeader(QPainter *painter, const int y) { painter->save(); int height = headerHeight() - 4; int collapseSize = height; // the icon iconRect = QRect(view->m_rightMargin + 2, 2 + y, collapseSize, collapseSize); painter->setPen(QPen(Qt::black, 1)); painter->drawRect(iconRect); static const int margin = 2; QRect iconSubrect = iconRect.adjusted(margin, margin, -margin, -margin); int midX = iconSubrect.center().x(); int midY = iconSubrect.center().y(); if (collapsed) { painter->drawLine(midX, iconSubrect.top(), midX, iconSubrect.bottom()); } painter->drawLine(iconSubrect.left(), midY, iconSubrect.right(), midY); // the text int textWidth = painter->fontMetrics().width(text); textRect = QRect(iconRect.right() + 4, y, textWidth, headerHeight()); painter->drawText(textRect, text, QTextOption(Qt::AlignHCenter | Qt::AlignVCenter)); // the line painter->drawLine(textRect.right() + 4, y + headerHeight() / 2, view->contentWidth() - view->m_rightMargin, y + headerHeight() / 2); painter->restore(); } int CategorizedView::Category::totalHeight() const { return headerHeight() + 5 + contentHeight(); } int CategorizedView::Category::headerHeight() const { return qApp->fontMetrics().height() + 4; } int CategorizedView::Category::contentHeight() const { if (collapsed) { return 0; } const int rows = qMax(1, qCeil((qreal)view->numItemsForCategory(this) / (qreal)view->itemsPerRow())); return view->itemSize().height() * rows; } QSize CategorizedView::Category::categoryTotalSize() const { return QSize(view->contentWidth(), contentHeight()); } CategorizedView::CategorizedView(QWidget *parent) : QListView(parent), m_leftMargin(5), m_rightMargin(5), m_categoryMargin(5)//, m_updatesDisabled(false), m_categoryEditor(0), m_editedCategory(0) { setViewMode(IconMode); setMovement(Snap); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); setWordWrap(true); } CategorizedView::~CategorizedView() { qDeleteAll(m_categories); m_categories.clear(); } void CategorizedView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) { // if (m_updatesDisabled) // { // return; // } QListView::dataChanged(topLeft, bottomRight, roles); if (roles.contains(CategoryRole)) { updateGeometries(); update(); } } void CategorizedView::rowsInserted(const QModelIndex &parent, int start, int end) { // if (m_updatesDisabled) // { // return; // } QListView::rowsInserted(parent, start, end); updateGeometries(); update(); } void CategorizedView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) { // if (m_updatesDisabled) // { // return; // } QListView::rowsAboutToBeRemoved(parent, start, end); updateGeometries(); update(); } void CategorizedView::updateGeometries() { QListView::updateGeometries(); m_cachedItemSize = QSize(); QMap cats; for (int i = 0; i < model()->rowCount(); ++i) { const QString category = model()->index(i, 0).data(CategoryRole).toString(); if (!cats.contains(category)) { Category *old = this->category(category); if (old) { cats.insert(category, new Category(old)); } else { cats.insert(category, new Category(category, this)); } } } /*if (m_editedCategory) { m_editedCategory = cats[m_editedCategory->text]; }*/ qDeleteAll(m_categories); m_categories = cats.values(); update(); } bool CategorizedView::isIndexHidden(const QModelIndex &index) const { Category *cat = category(index); if (cat) { return cat->collapsed; } else { return false; } } CategorizedView::Category *CategorizedView::category(const QModelIndex &index) const { return category(index.data(CategoryRole).toString()); } CategorizedView::Category *CategorizedView::category(const QString &cat) const { for (int i = 0; i < m_categories.size(); ++i) { if (m_categories.at(i)->text == cat) { return m_categories.at(i); } } return 0; } int CategorizedView::numItemsForCategory(const CategorizedView::Category *category) const { return itemsForCategory(category).size(); } QList CategorizedView::itemsForCategory(const CategorizedView::Category *category) const { QList indices; for (int i = 0; i < model()->rowCount(); ++i) { if (model()->index(i, 0).data(CategoryRole).toString() == category->text) { indices += model()->index(i, 0); } } return indices; } int CategorizedView::categoryTop(const CategorizedView::Category *category) const { int res = 0; const QList cats = sortedCategories(); for (int i = 0; i < cats.size(); ++i) { if (cats.at(i) == category) { break; } res += cats.at(i)->totalHeight() + m_categoryMargin; } return res; } int CategorizedView::itemsPerRow() const { return qFloor((qreal)contentWidth() / (qreal)itemSize().width()); } int CategorizedView::contentWidth() const { return width() - m_leftMargin - m_rightMargin; } bool CategorizedView::lessThanCategoryPointer(const CategorizedView::Category *c1, const CategorizedView::Category *c2) { return c1->text < c2->text; } QList CategorizedView::sortedCategories() const { QList out = m_categories; qSort(out.begin(), out.end(), &CategorizedView::lessThanCategoryPointer); return out; } QSize CategorizedView::itemSize(const QStyleOptionViewItem &option) const { if (!m_cachedItemSize.isValid()) { QModelIndex sample = model()->index(model()->rowCount() -1, 0); const QAbstractItemDelegate *delegate = itemDelegate(); if (delegate) { m_cachedItemSize = delegate->sizeHint(option, sample); m_cachedItemSize.setWidth(m_cachedItemSize.width() + 20); m_cachedItemSize.setHeight(m_cachedItemSize.height() + 20); } else { m_cachedItemSize = QSize(); } } return m_cachedItemSize; } void CategorizedView::mousePressEvent(QMouseEvent *event) { //endCategoryEditor(); if (event->buttons() & Qt::LeftButton) { foreach (Category *category, m_categories) { if (category->iconRect.contains(event->pos())) { category->collapsed = !category->collapsed; updateGeometries(); viewport()->update(); event->accept(); return; } } for (int i = 0; i < model()->rowCount(); ++i) { QModelIndex index = model()->index(i, 0); if (visualRect(index).contains(event->pos())) { selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); event->accept(); return; } } } QListView::mousePressEvent(event); } void CategorizedView::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { for (int i = 0; i < model()->rowCount(); ++i) { QModelIndex index = model()->index(i, 0); if (visualRect(index).contains(event->pos())) { selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); event->accept(); return; } } } QListView::mouseMoveEvent(event); } void CategorizedView::mouseReleaseEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { for (int i = 0; i < model()->rowCount(); ++i) { QModelIndex index = model()->index(i, 0); if (visualRect(index).contains(event->pos())) { selectionModel()->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect); event->accept(); return; } } } QListView::mouseReleaseEvent(event); } void CategorizedView::mouseDoubleClickEvent(QMouseEvent *event) { /*endCategoryEditor(); foreach (Category *category, m_categories) { if (category->textRect.contains(event->pos()) && m_categoryEditor == 0) { startCategoryEditor(category); event->accept(); return; } }*/ QListView::mouseDoubleClickEvent(event); } void CategorizedView::paintEvent(QPaintEvent *event) { QPainter painter(this->viewport()); int y = 0; for (int i = 0; i < m_categories.size(); ++i) { Category *category = m_categories.at(i); category->drawHeader(&painter, y); y += category->totalHeight() + m_categoryMargin; } for (int i = 0; i < model()->rowCount(); ++i) { const QModelIndex index = model()->index(i, 0); if (isIndexHidden(index)) { continue; } Qt::ItemFlags flags = index.flags(); QStyleOptionViewItemV4 option(viewOptions()); option.rect = visualRect(index); option.widget = this; option.features |= wordWrap() ? QStyleOptionViewItemV2::WrapText : QStyleOptionViewItemV2::None; if (flags & Qt::ItemIsSelectable) { option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected : QStyle::State_None; } else { option.state &= ~QStyle::State_Selected; } option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None; if (!(flags & Qt::ItemIsEnabled)) { option.state &= ~QStyle::State_Enabled; } itemDelegate()->paint(&painter, option, index); } } void CategorizedView::resizeEvent(QResizeEvent *event) { QListView::resizeEvent(event); // if (m_categoryEditor) // { // m_categoryEditor->resize(qMax(contentWidth() / 2, m_editedCategory->textRect.width()), m_categoryEditor->height()); // } updateGeometries(); } void CategorizedView::dragEnterEvent(QDragEnterEvent *event) { // TODO } void CategorizedView::dragMoveEvent(QDragMoveEvent *event) { // TODO } void CategorizedView::dragLeaveEvent(QDragLeaveEvent *event) { // TODO } void CategorizedView::dropEvent(QDropEvent *event) { stopAutoScroll(); setState(NoState); if (event->source() != this || !(event->possibleActions() & Qt::MoveAction)) { return; } // check that we aren't on a category header and calculate which category we're in Category *category = 0; { int y = 0; foreach (Category *cat, m_categories) { if (event->pos().y() > y && event->pos().y() < (y + cat->headerHeight())) { viewport()->update(); return; } y += cat->totalHeight() + m_categoryMargin; if (event->pos().y() < y) { category = cat; break; } } } // calculate the internal column int internalColumn = -1; { const int itemWidth = itemSize().width(); for (int i = 0, c = 0; i < contentWidth(); i += itemWidth, ++c) { if (event->pos().x() > (i - itemWidth / 2) && event->pos().x() < (i + itemWidth / 2)) { internalColumn = c; break; } } if (internalColumn == -1) { viewport()->update(); return; } } // calculate the internal row int internalRow = -1; { const int itemHeight = itemSize().height(); const int top = categoryTop(category); for (int i = top + category->headerHeight(), r = 0; i < top + category->totalHeight(); i += itemHeight, ++r) { if (event->pos().y() > i && event->pos().y() < (i + itemHeight)) { internalRow = r; break; } } if (internalRow == -1) { viewport()->update(); return; } } QList indices = itemsForCategory(category); // flaten the internalColumn/internalRow to one row int categoryRow; { for (int i = 0; i < internalRow; ++i) { if (i == internalRow) { break; } categoryRow += itemsPerRow(); } categoryRow += internalColumn; } int row = indices.at(categoryRow).row(); if (model()->dropMimeData(event->mimeData(), Qt::MoveAction, row, 0, QModelIndex())) { event->setDropAction(Qt::MoveAction); event->accept(); } updateGeometries(); } bool lessThanQModelIndex(const QModelIndex &i1, const QModelIndex &i2) { return i1.data() < i2.data(); } QRect CategorizedView::visualRect(const QModelIndex &index) const { if (!index.isValid() || isIndexHidden(index) || index.column() > 0) { return QRect(); } const Category *cat = category(index); QList indices = itemsForCategory(cat); qSort(indices.begin(), indices.end(), &lessThanQModelIndex); int x = 0; int y = 0; const int perRow = itemsPerRow(); for (int i = 0; i < indices.size(); ++i) { if (indices.at(i) == index) { break; } ++x; if (x == perRow) { x = 0; ++y; } } QSize size = itemSize(); QRect out; out.setTop(categoryTop(cat) + cat->headerHeight() + 5 + y * size.height()); out.setLeft(x * size.width()); out.setSize(size); return out; } /* void CategorizedView::startCategoryEditor(Category *category) { if (m_categoryEditor != 0) { return; } m_editedCategory = category; m_categoryEditor = new QLineEdit(m_editedCategory->text, this); QRect rect = m_editedCategory->textRect; rect.setWidth(qMax(contentWidth() / 2, rect.width())); m_categoryEditor->setGeometry(rect); m_categoryEditor->show(); m_categoryEditor->setFocus(); connect(m_categoryEditor, &QLineEdit::returnPressed, this, &CategorizedView::endCategoryEditor); } void CategorizedView::endCategoryEditor() { if (m_categoryEditor == 0) { return; } m_editedCategory->text = m_categoryEditor->text(); m_updatesDisabled = true; foreach (const QModelIndex &index, itemsForCategory(m_editedCategory)) { const_cast(index.model())->setData(index, m_categoryEditor->text(), CategoryRole); } m_updatesDisabled = false; delete m_categoryEditor; m_categoryEditor = 0; m_editedCategory = 0; updateGeometries(); } */