From e88efff11587da0967431703474f406fdfe2b7ca 2017-12-20 09:30:35 From: Thibaud Rabillard Date: 2017-12-20 09:30:35 Subject: [PATCH] Refactoring of catalogue display using a QTreeView and a custom model based on QTreeWidgetItem --- diff --git a/gui/include/Catalogue/CatalogueTreeModel.h b/gui/include/Catalogue/CatalogueTreeModel.h new file mode 100644 index 0000000..3d2447e --- /dev/null +++ b/gui/include/Catalogue/CatalogueTreeModel.h @@ -0,0 +1,47 @@ +#ifndef SCIQLOP_CATALOGUETREEMODEL_H +#define SCIQLOP_CATALOGUETREEMODEL_H + +#include +#include +#include + +/** + * @brief Model to display catalogue items based on QTreeWidgetItem + * @warning Do not use the method QTreeWidgetItem::treeWidget for an item added to this model or the application will crash + */ +class CatalogueTreeModel : public QAbstractItemModel { + Q_OBJECT + +signals: + void itemRenamed(const QModelIndex &index); + +public: + CatalogueTreeModel(QObject *parent = nullptr); + + enum class Column { Name, Validation, Count }; + + QModelIndex addTopLevelItem(QTreeWidgetItem *item); + int topLevelItemCount() const; + QTreeWidgetItem *topLevelItem(int i) const; + + void addChildItem(QTreeWidgetItem *child, const QModelIndex &parentIndex); + + QTreeWidgetItem *item(const QModelIndex &index) const; + QModelIndex indexOf(QTreeWidgetItem *item, int column = 0) const; + + // model + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + +private: + class CatalogueTreeModelPrivate; + spimpl::unique_impl_ptr impl; +}; + +#endif // CATALOGUETREEMODEL_H diff --git a/gui/include/Catalogue/CatalogueTreeWidgetItem.h b/gui/include/Catalogue/CatalogueTreeWidgetItem.h index f9b0d65..8480c7c 100644 --- a/gui/include/Catalogue/CatalogueTreeWidgetItem.h +++ b/gui/include/Catalogue/CatalogueTreeWidgetItem.h @@ -18,8 +18,6 @@ public: /// Returns the catalogue represented by the item std::shared_ptr catalogue() const; - /// Displays or hides the save and cancel buttons indicating that the catalogue has unsaved - /// changes void setHasChanges(bool value); /// Returns true if the widget indicating the event has unsaved changes is displayed diff --git a/gui/meson.build b/gui/meson.build index e37bc1c..135206a 100644 --- a/gui/meson.build +++ b/gui/meson.build @@ -27,7 +27,8 @@ gui_moc_headers = [ 'include/Catalogue/CatalogueSideBarWidget.h', 'include/Catalogue/CatalogueInspectorWidget.h', 'include/Catalogue/CatalogueEventsModel.h', - 'include/Catalogue/CreateEventDialog.h' + 'include/Catalogue/CreateEventDialog.h', + 'include/Catalogue/CatalogueTreeModel.h' ] gui_ui_files = [ @@ -122,7 +123,8 @@ gui_sources = [ 'src/Catalogue/CatalogueEventsModel.cpp', 'src/Catalogue/CatalogueExplorerHelper.cpp', 'src/Catalogue/CatalogueActionManager.cpp', - 'src/Catalogue/CreateEventDialog.cpp' + 'src/Catalogue/CreateEventDialog.cpp', + 'src/Catalogue/CatalogueTreeModel.cpp' ] gui_inc = include_directories(['include']) diff --git a/gui/src/Catalogue/CatalogueSideBarWidget.cpp b/gui/src/Catalogue/CatalogueSideBarWidget.cpp index e9f5d4e..6cf2bcb 100644 --- a/gui/src/Catalogue/CatalogueSideBarWidget.cpp +++ b/gui/src/Catalogue/CatalogueSideBarWidget.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include #include @@ -21,14 +23,17 @@ constexpr auto DATABASE_ITEM_TYPE = QTreeWidgetItem::UserType + 3; struct CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate { - void configureTreeWidget(QTreeWidget *treeWidget); - QTreeWidgetItem *addDatabaseItem(const QString &name, QTreeWidget *treeWidget); - QTreeWidgetItem *getDatabaseItem(const QString &name, QTreeWidget *treeWidget); + CatalogueTreeModel *m_TreeModel = nullptr; + + void configureTreeWidget(QTreeView *treeView); + QModelIndex addDatabaseItem(const QString &name); + QTreeWidgetItem *getDatabaseItem(const QString &name); void addCatalogueItem(const std::shared_ptr &catalogue, - QTreeWidgetItem *parentDatabaseItem); + const QModelIndex &databaseIndex); - CatalogueTreeWidgetItem *getCatalogueItem(const std::shared_ptr &catalogue, - QTreeWidget *treeWidget) const; + CatalogueTreeWidgetItem *getCatalogueItem(const std::shared_ptr &catalogue) const; + void setHasChanges(bool value, const QModelIndex &index, QTreeView *treeView); + bool hasChanges(const QModelIndex &index, QTreeView *treeView); }; CatalogueSideBarWidget::CatalogueSideBarWidget(QWidget *parent) @@ -37,26 +42,36 @@ CatalogueSideBarWidget::CatalogueSideBarWidget(QWidget *parent) impl{spimpl::make_unique_impl()} { ui->setupUi(this); - impl->configureTreeWidget(ui->treeWidget); - ui->treeWidget->setColumnCount(2); - ui->treeWidget->header()->setStretchLastSection(false); - ui->treeWidget->header()->setSectionResizeMode(QHeaderView::ResizeToContents); - ui->treeWidget->header()->setSectionResizeMode(0, QHeaderView::Stretch); + impl->m_TreeModel = new CatalogueTreeModel(this); + ui->treeView->setModel(impl->m_TreeModel); + + impl->configureTreeWidget(ui->treeView); + + ui->treeView->header()->setStretchLastSection(false); + ui->treeView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch); auto emitSelection = [this]() { - auto selectedItems = ui->treeWidget->selectedItems(); + auto selectedItems = ui->treeView->selectionModel()->selectedRows(); if (selectedItems.isEmpty()) { emit this->selectionCleared(); } else { QVector > catalogues; QStringList databases; - int selectionType = selectedItems.first()->type(); + auto firstIndex = selectedItems.first(); + auto firstItem = impl->m_TreeModel->item(firstIndex); + if (!firstItem) { + Q_ASSERT(false); + return; + } + auto selectionType = firstItem->type(); - for (auto item : ui->treeWidget->selectedItems()) { - if (item->type() == selectionType) { + for (auto itemIndex : selectedItems) { + auto item = impl->m_TreeModel->item(itemIndex); + if (item && item->type() == selectionType) { switch (selectionType) { case CATALOGUE_ITEM_TYPE: catalogues.append( @@ -100,19 +115,20 @@ CatalogueSideBarWidget::CatalogueSideBarWidget(QWidget *parent) }; - connect(ui->treeWidget, &QTreeWidget::itemClicked, emitSelection); - connect(ui->treeWidget, &QTreeWidget::currentItemChanged, emitSelection); - connect(ui->treeWidget, &QTreeWidget::itemChanged, - [emitSelection, this](auto item, auto column) { - auto selectedItems = ui->treeWidget->selectedItems(); - qDebug() << "ITEM CHANGED" << column; - if (selectedItems.contains(item) && column == 0) { - emitSelection(); - } - }); + connect(ui->treeView, &QTreeView::clicked, emitSelection); + connect(ui->treeView->selectionModel(), &QItemSelectionModel::currentChanged, emitSelection); + connect(impl->m_TreeModel, &CatalogueTreeModel::itemRenamed, [emitSelection, this](auto index) { + auto selectedIndexes = ui->treeView->selectionModel()->selectedRows(); + if (selectedIndexes.contains(index)) { + emitSelection(); + } + + auto item = impl->m_TreeModel->item(index); + impl->setHasChanges(true, index, ui->treeView); + }); - ui->treeWidget->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->treeWidget, &QTreeWidget::customContextMenuRequested, this, + ui->treeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->treeView, &QTreeWidget::customContextMenuRequested, this, &CatalogueSideBarWidget::onContextMenuRequested); } @@ -124,8 +140,9 @@ CatalogueSideBarWidget::~CatalogueSideBarWidget() void CatalogueSideBarWidget::setCatalogueChanges(const std::shared_ptr &catalogue, bool hasChanges) { - if (auto catalogueItem = impl->getCatalogueItem(catalogue, ui->treeWidget)) { - catalogueItem->setHasChanges(hasChanges); + if (auto catalogueItem = impl->getCatalogueItem(catalogue)) { + auto index = impl->m_TreeModel->indexOf(catalogueItem); + impl->setHasChanges(hasChanges, index, ui->treeView); catalogueItem->refresh(); } } @@ -134,11 +151,15 @@ void CatalogueSideBarWidget::onContextMenuRequested(const QPoint &pos) { QMenu menu{this}; - auto currentItem = ui->treeWidget->currentItem(); + auto currentIndex = ui->treeView->currentIndex(); + auto currentItem = impl->m_TreeModel->item(currentIndex); + if (!currentItem) { + return; + } + switch (currentItem->type()) { case CATALOGUE_ITEM_TYPE: - menu.addAction("Rename", - [this, currentItem]() { ui->treeWidget->editItem(currentItem); }); + menu.addAction("Rename", [this, currentIndex]() { ui->treeView->edit(currentIndex); }); break; case DATABASE_ITEM_TYPE: break; @@ -154,58 +175,54 @@ void CatalogueSideBarWidget::onContextMenuRequested(const QPoint &pos) } if (!menu.isEmpty()) { - menu.exec(ui->treeWidget->mapToGlobal(pos)); + menu.exec(ui->treeView->mapToGlobal(pos)); } } -void CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::configureTreeWidget( - QTreeWidget *treeWidget) +void CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::configureTreeWidget(QTreeView *treeView) { auto allEventsItem = new QTreeWidgetItem{{"All Events"}, ALL_EVENT_ITEM_TYPE}; allEventsItem->setIcon(0, QIcon(":/icones/allEvents.png")); - treeWidget->addTopLevelItem(allEventsItem); + m_TreeModel->addTopLevelItem(allEventsItem); auto trashItem = new QTreeWidgetItem{{"Trash"}, TRASH_ITEM_TYPE}; trashItem->setIcon(0, QIcon(":/icones/trash.png")); - treeWidget->addTopLevelItem(trashItem); + m_TreeModel->addTopLevelItem(trashItem); - auto separator = new QFrame{treeWidget}; + auto separator = new QFrame{treeView}; separator->setFrameShape(QFrame::HLine); auto separatorItem = new QTreeWidgetItem{}; separatorItem->setFlags(Qt::NoItemFlags); - treeWidget->addTopLevelItem(separatorItem); - treeWidget->setItemWidget(separatorItem, 0, separator); + auto separatorIndex = m_TreeModel->addTopLevelItem(separatorItem); + treeView->setIndexWidget(separatorIndex, separator); auto repositories = sqpApp->catalogueController().getRepositories(); for (auto dbname : repositories) { - auto db = addDatabaseItem(dbname, treeWidget); - + auto dbIndex = addDatabaseItem(dbname); auto catalogues = sqpApp->catalogueController().retrieveCatalogues(dbname); for (auto catalogue : catalogues) { - addCatalogueItem(catalogue, db); + addCatalogueItem(catalogue, dbIndex); } } - treeWidget->expandAll(); + treeView->expandAll(); } -QTreeWidgetItem * -CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::addDatabaseItem(const QString &name, - QTreeWidget *treeWidget) +QModelIndex +CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::addDatabaseItem(const QString &name) { auto databaseItem = new QTreeWidgetItem{{name}, DATABASE_ITEM_TYPE}; databaseItem->setIcon(0, QIcon{":/icones/database.png"}); - treeWidget->addTopLevelItem(databaseItem); + auto databaseIndex = m_TreeModel->addTopLevelItem(databaseItem); - return databaseItem; + return databaseIndex; } QTreeWidgetItem * -CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::getDatabaseItem(const QString &name, - QTreeWidget *treeWidget) +CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::getDatabaseItem(const QString &name) { - for (auto i = 0; i < treeWidget->topLevelItemCount(); ++i) { - auto item = treeWidget->topLevelItem(i); + for (auto i = 0; i < m_TreeModel->topLevelItemCount(); ++i) { + auto item = m_TreeModel->topLevelItem(i); if (item->type() == DATABASE_ITEM_TYPE && item->text(0) == name) { return item; } @@ -215,18 +232,18 @@ CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::getDatabaseItem(const QSt } void CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::addCatalogueItem( - const std::shared_ptr &catalogue, QTreeWidgetItem *parentDatabaseItem) + const std::shared_ptr &catalogue, const QModelIndex &databaseIndex) { auto catalogueItem = new CatalogueTreeWidgetItem{catalogue, CATALOGUE_ITEM_TYPE}; catalogueItem->setIcon(0, QIcon{":/icones/catalogue.png"}); - parentDatabaseItem->addChild(catalogueItem); + m_TreeModel->addChildItem(catalogueItem, databaseIndex); } CatalogueTreeWidgetItem *CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::getCatalogueItem( - const std::shared_ptr &catalogue, QTreeWidget *treeWidget) const + const std::shared_ptr &catalogue) const { - for (auto i = 0; i < treeWidget->topLevelItemCount(); ++i) { - auto item = treeWidget->topLevelItem(i); + for (auto i = 0; i < m_TreeModel->topLevelItemCount(); ++i) { + auto item = m_TreeModel->topLevelItem(i); if (item->type() == DATABASE_ITEM_TYPE) { for (auto j = 0; j < item->childCount(); ++j) { auto childItem = item->child(j); @@ -248,3 +265,34 @@ CatalogueTreeWidgetItem *CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate:: return nullptr; } + +void CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::setHasChanges(bool value, + const QModelIndex &index, + QTreeView *treeView) +{ + auto validationIndex = index.sibling(index.row(), (int)CatalogueTreeModel::Column::Validation); + if (value) { + if (!hasChanges(validationIndex, treeView)) { + auto widget = CatalogueExplorerHelper::buildValidationWidget( + treeView, + [this, validationIndex, treeView]() { + setHasChanges(false, validationIndex, treeView); + }, + [this, validationIndex, treeView]() { + setHasChanges(false, validationIndex, treeView); + }); + treeView->setIndexWidget(validationIndex, widget); + } + } + else { + // Note: the widget is destroyed + treeView->setIndexWidget(validationIndex, nullptr); + } +} + +bool CatalogueSideBarWidget::CatalogueSideBarWidgetPrivate::hasChanges(const QModelIndex &index, + QTreeView *treeView) +{ + auto validationIndex = index.sibling(index.row(), (int)CatalogueTreeModel::Column::Validation); + return treeView->indexWidget(validationIndex) != nullptr; +} diff --git a/gui/src/Catalogue/CatalogueTreeModel.cpp b/gui/src/Catalogue/CatalogueTreeModel.cpp new file mode 100644 index 0000000..da7a33d --- /dev/null +++ b/gui/src/Catalogue/CatalogueTreeModel.cpp @@ -0,0 +1,169 @@ +#include "Catalogue/CatalogueTreeModel.h" + +#include +#include + +struct CatalogueTreeModel::CatalogueTreeModelPrivate { + std::unique_ptr m_RootItem = nullptr; + + CatalogueTreeModelPrivate() : m_RootItem{std::make_unique()} {} +}; + +CatalogueTreeModel::CatalogueTreeModel(QObject *parent) + : QAbstractItemModel(parent), impl{spimpl::make_unique_impl()} +{ +} + +QModelIndex CatalogueTreeModel::addTopLevelItem(QTreeWidgetItem *item) +{ + beginInsertRows(QModelIndex(), impl->m_RootItem->childCount(), impl->m_RootItem->childCount()); + impl->m_RootItem->addChild(item); + endInsertRows(); + + return index(impl->m_RootItem->childCount() - 1, 0); +} + +int CatalogueTreeModel::topLevelItemCount() const +{ + return impl->m_RootItem->childCount(); +} + +QTreeWidgetItem *CatalogueTreeModel::topLevelItem(int i) const +{ + return impl->m_RootItem->child(i); +} + +void CatalogueTreeModel::addChildItem(QTreeWidgetItem *child, const QModelIndex &parentIndex) +{ + auto parentItem = item(parentIndex); + beginInsertRows(parentIndex, parentItem->childCount(), parentItem->childCount()); + parentItem->addChild(child); + endInsertRows(); +} + +QTreeWidgetItem *CatalogueTreeModel::item(const QModelIndex &index) const +{ + return static_cast(index.internalPointer()); +} + +QModelIndex CatalogueTreeModel::indexOf(QTreeWidgetItem *item, int column) const +{ + auto parentItem = item->parent(); + if (!parentItem) { + return QModelIndex(); + } + + auto row = parentItem->indexOfChild(item); + return createIndex(row, column, item); +} + +QModelIndex CatalogueTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (!hasIndex(row, column, parent)) { + return QModelIndex(); + } + + QTreeWidgetItem *parentItem = nullptr; + + if (!parent.isValid()) { + parentItem = impl->m_RootItem.get(); + } + else { + parentItem = static_cast(parent.internalPointer()); + } + + QTreeWidgetItem *childItem = parentItem->child(row); + if (childItem) { + return createIndex(row, column, childItem); + } + + return QModelIndex(); +} + + +QModelIndex CatalogueTreeModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) { + return QModelIndex(); + } + + auto childItem = static_cast(index.internalPointer()); + auto parentItem = childItem->parent(); + + if (parentItem == nullptr || parentItem->parent() == nullptr) { + return QModelIndex(); + } + + auto row = parentItem->parent()->indexOfChild(parentItem); + return createIndex(row, 0, parentItem); +} + +int CatalogueTreeModel::rowCount(const QModelIndex &parent) const +{ + QTreeWidgetItem *parentItem = nullptr; + if (parent.column() > 0) { + return 0; + } + + if (!parent.isValid()) { + parentItem = impl->m_RootItem.get(); + } + else { + parentItem = static_cast(parent.internalPointer()); + } + + return parentItem->childCount(); +} + +int CatalogueTreeModel::columnCount(const QModelIndex &parent) const +{ + return (int)Column::Count; +} + +Qt::ItemFlags CatalogueTreeModel::flags(const QModelIndex &index) const +{ + if (index.column() == (int)Column::Validation) { + return Qt::NoItemFlags; + } + + auto item = static_cast(index.internalPointer()); + if (item) { + return item->flags(); + } + + return Qt::NoItemFlags; +} + +QVariant CatalogueTreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QModelIndex(); + } + + auto item = static_cast(index.internalPointer()); + if (item) { + return item->data(index.column(), role); + } + + return QModelIndex(); +} + +bool CatalogueTreeModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid()) { + return false; + } + + auto item = static_cast(index.internalPointer()); + if (item) { + item->setData(index.column(), role, value); + + if (index.column() == (int)Column::Name) { + emit itemRenamed(index); + } + + return true; + } + + return false; +} diff --git a/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp b/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp index 92de0ff..6726cbc 100644 --- a/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp +++ b/gui/src/Catalogue/CatalogueTreeWidgetItem.cpp @@ -25,7 +25,7 @@ CatalogueTreeWidgetItem::CatalogueTreeWidgetItem(std::shared_ptr ca : QTreeWidgetItem(type), impl{spimpl::make_unique_impl(catalogue)} { - setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsDropEnabled); } QVariant CatalogueTreeWidgetItem::data(int column, int role) const @@ -51,7 +51,6 @@ void CatalogueTreeWidgetItem::setData(int column, int role, const QVariant &valu setText(0, newName); impl->m_Catalogue->setName(newName); sqpApp->catalogueController().updateCatalogue(impl->m_Catalogue); - setHasChanges(true); } } else { @@ -64,27 +63,6 @@ std::shared_ptr CatalogueTreeWidgetItem::catalogue() const return impl->m_Catalogue; } -void CatalogueTreeWidgetItem::setHasChanges(bool value) -{ - if (value) { - if (!hasChanges()) { - auto widget = CatalogueExplorerHelper::buildValidationWidget( - treeWidget(), [this]() { setHasChanges(false); }, - [this]() { setHasChanges(false); }); - treeWidget()->setItemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN, widget); - } - } - else { - // Note: the widget is destroyed - treeWidget()->setItemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN, nullptr); - } -} - -bool CatalogueTreeWidgetItem::hasChanges() -{ - return treeWidget()->itemWidget(this, APPLY_CANCEL_BUTTONS_COLUMN) != nullptr; -} - void CatalogueTreeWidgetItem::refresh() { emitDataChanged(); diff --git a/gui/ui/Catalogue/CatalogueSideBarWidget.ui b/gui/ui/Catalogue/CatalogueSideBarWidget.ui index 34e03a1..06a2e35 100644 --- a/gui/ui/Catalogue/CatalogueSideBarWidget.ui +++ b/gui/ui/Catalogue/CatalogueSideBarWidget.ui @@ -72,18 +72,25 @@ - + + + true + + + QAbstractItemView::DropOnly + + + Qt::CopyAction + QAbstractItemView::ExtendedSelection - - false + + true + + + 0 - - - 1 - -