From ccbf341dc8d8e515d9cf918bff7ff9435c477847 Mon Sep 17 00:00:00 2001 From: Jan Dalheimer Date: Tue, 24 Dec 2013 11:47:30 +0100 Subject: [PATCH] Initial commit. Basics work. Next: Drag and Drop --- .gitignore | 2 + CMakeLists.txt | 35 +++ CategorizedProxyModel.cpp | 12 + CategorizedProxyModel.h | 18 ++ CategorizedView.cpp | 587 ++++++++++++++++++++++++++++++++++++++ CategorizedView.h | 96 +++++++ main.cpp | 53 ++++ 7 files changed, 803 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 CategorizedProxyModel.cpp create mode 100644 CategorizedProxyModel.h create mode 100644 CategorizedView.cpp create mode 100644 CategorizedView.h create mode 100644 main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..a5d18fa3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build/ +*.user* diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..8a246bcf --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 2.8.9) + +project(GroupView) + +set(CMAKE_AUTOMOC ON) + +IF(APPLE) + message(STATUS "Using APPLE CMAKE_CXX_FLAGS") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall") +ELSEIF(UNIX) + # assume GCC, add C++0x/C++11 stuff + MESSAGE(STATUS "Using UNIX CMAKE_CXX_FLAGS") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall") +ELSEIF(MINGW) + MESSAGE(STATUS "Using MINGW CMAKE_CXX_FLAGS") + SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=gnu++11 -Wall") +ENDIF() + +find_package(Qt5Core REQUIRED) +find_package(Qt5Gui REQUIRED) +find_package(Qt5Widgets REQUIRED) + +include_directories(${Qt5Core_INCLUDE_DIRS} ${Qt5Gui_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS}) + +set(SOURCES + main.cpp + + CategorizedView.h + CategorizedView.cpp + CategorizedProxyModel.h + CategorizedProxyModel.cpp +) + +add_executable(GroupView ${SOURCES}) +qt5_use_modules(GroupView Core Gui Widgets) diff --git a/CategorizedProxyModel.cpp b/CategorizedProxyModel.cpp new file mode 100644 index 00000000..2b54ce1b --- /dev/null +++ b/CategorizedProxyModel.cpp @@ -0,0 +1,12 @@ +#include "CategorizedProxyModel.h" + +#include "CategorizedView.h" + +CategorizedProxyModel::CategorizedProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ +} +bool CategorizedProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + return left.data(CategorizedView::CategoryRole).toString() < right.data(CategorizedView::CategoryRole).toString(); +} diff --git a/CategorizedProxyModel.h b/CategorizedProxyModel.h new file mode 100644 index 00000000..6e4f3fdc --- /dev/null +++ b/CategorizedProxyModel.h @@ -0,0 +1,18 @@ +#ifndef CATEGORIZEDPROXYMODEL_H +#define CATEGORIZEDPROXYMODEL_H + +#include + +class CategorizedProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + CategorizedProxyModel(QObject *parent = 0); + +protected: + bool lessThan(const QModelIndex &left, const QModelIndex &right) const; +}; + + +#endif // CATEGORIZEDPROXYMODEL_H diff --git a/CategorizedView.cpp b/CategorizedView.cpp new file mode 100644 index 00000000..46b1e072 --- /dev/null +++ b/CategorizedView.cpp @@ -0,0 +1,587 @@ +#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(); +} +*/ diff --git a/CategorizedView.h b/CategorizedView.h new file mode 100644 index 00000000..1e918496 --- /dev/null +++ b/CategorizedView.h @@ -0,0 +1,96 @@ +#ifndef WIDGET_H +#define WIDGET_H + +#include +#include + +class CategorizedView : public QListView +{ + Q_OBJECT + +public: + CategorizedView(QWidget *parent = 0); + ~CategorizedView(); + + enum + { + CategoryRole = Qt::UserRole + }; + + virtual QRect visualRect(const QModelIndex &index) const; + +protected slots: + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + virtual void rowsInserted(const QModelIndex &parent, int start, int end); + virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + virtual void updateGeometries(); + +protected: + virtual bool isIndexHidden(const QModelIndex &index) const; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + +private: + struct Category + { + Category(const QString &text, CategorizedView *view); + Category(const Category *other); + CategorizedView *view; + QString text; + bool collapsed; + QRect iconRect; + QRect textRect; + + void drawHeader(QPainter *painter, const int y); + int totalHeight() const; + int headerHeight() const; + int contentHeight() const; + QSize categoryTotalSize() const; + }; + friend struct Category; + + QList m_categories; + + int m_leftMargin; + int m_rightMargin; + int m_categoryMargin; + int m_itemSpacing; + + //bool m_updatesDisabled; + + Category *category(const QModelIndex &index) const; + Category *category(const QString &cat) const; + int numItemsForCategory(const Category *category) const; + QList itemsForCategory(const Category *category) const; + + int categoryTop(const Category *category) const; + + int itemsPerRow() const; + int contentWidth() const; + + static bool lessThanCategoryPointer(const Category *c1, const Category *c2); + QList sortedCategories() const; + +private: + mutable QSize m_cachedItemSize; + QSize itemSize(const QStyleOptionViewItem &option) const; + QSize itemSize() const { return itemSize(viewOptions()); } + + /*QLineEdit *m_categoryEditor; + Category *m_editedCategory; + void startCategoryEditor(Category *category); + +private slots: + void endCategoryEditor();*/ +}; + +#endif // WIDGET_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 00000000..24d7075e --- /dev/null +++ b/main.cpp @@ -0,0 +1,53 @@ +#include "CategorizedView.h" +#include +#include + +#include "CategorizedProxyModel.h" + +QPixmap icon(const Qt::GlobalColor color) +{ + QPixmap p = QPixmap(32, 32); + p.fill(QColor(color)); + return p; +} +QStandardItem *createItem(const Qt::GlobalColor color, const QString &text, const QString &category) +{ + QStandardItem *item = new QStandardItem; + item->setText(text); + item->setData(icon(color), Qt::DecorationRole); + item->setData(category, CategorizedView::CategoryRole); + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + return item; +} + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + QStandardItemModel model; + model.setRowCount(10); + model.setColumnCount(1); + + model.setItem(0, createItem(Qt::red, "Red", "Colorful")); + model.setItem(1, createItem(Qt::blue, "Blue", "Colorful")); + model.setItem(2, createItem(Qt::yellow, "Yellow", "Colorful")); + + model.setItem(3, createItem(Qt::black, "Black", "Not Colorful")); + model.setItem(4, createItem(Qt::darkGray, "Dark Gray", "Not Colorful")); + model.setItem(5, createItem(Qt::gray, "Gray", "Not Colorful")); + model.setItem(6, createItem(Qt::lightGray, "Light Gray", "Not Colorful")); + model.setItem(7, createItem(Qt::white, "White", "Not Colorful")); + + model.setItem(8, createItem(Qt::darkGreen, "Dark Green", "")); + model.setItem(9, createItem(Qt::green, "Green", "")); + + CategorizedProxyModel pModel; + pModel.setSourceModel(&model); + + CategorizedView w; + w.setModel(&pModel); + w.resize(640, 480); + w.show(); + + return a.exec(); +}