diff --git a/CMakeLists.txt b/CMakeLists.txt index 91d2d86cc..f62a081fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ set(CPP_SOURCE_FILES src/FlowScene.cpp src/FlowView.cpp src/FlowViewStyle.cpp + src/ModelSelectionWidget.cpp src/Node.cpp src/NodeConnectionInteraction.cpp src/NodeDataModel.cpp diff --git a/include/nodes/internal/DataModelRegistry.hpp b/include/nodes/internal/DataModelRegistry.hpp index 35fdbddc2..b786fa992 100644 --- a/include/nodes/internal/DataModelRegistry.hpp +++ b/include/nodes/internal/DataModelRegistry.hpp @@ -36,7 +36,9 @@ class NODE_EDITOR_PUBLIC DataModelRegistry using RegistryItemCreator = std::function; using RegisteredModelCreatorsMap = std::unordered_map; using RegisteredModelsCategoryMap = std::unordered_map; + using RegisteredModelsOrder = std::vector; using CategoriesSet = std::set; + using CategoriesOrder = std::vector; using RegisteredTypeConvertersMap = std::map; @@ -82,10 +84,16 @@ class NODE_EDITOR_PUBLIC DataModelRegistry RegisteredModelCreatorsMap const ®isteredModelCreators() const; + RegisteredModelsOrder const& registeredModelsOrder() const; + RegisteredModelsCategoryMap const ®isteredModelsCategoryAssociation() const; CategoriesSet const &categories() const; + CategoriesOrder const &categoriesOrder() const; + + void sortCategories(); + TypeConverter getTypeConverter(NodeDataType const & d1, NodeDataType const & d2) const; @@ -97,6 +105,10 @@ class NODE_EDITOR_PUBLIC DataModelRegistry RegisteredModelCreatorsMap _registeredItemCreators; + CategoriesOrder _categoriesOrder; + + RegisteredModelsOrder _registeredModelsOrder; + RegisteredTypeConvertersMap _registeredTypeConverters; private: @@ -125,11 +137,16 @@ class NODE_EDITOR_PUBLIC DataModelRegistry registerModelImpl(RegistryItemCreator creator, QString const &category ) { const QString name = ModelType::Name(); - if (_registeredItemCreators.count(name) == 0) + if (!_registeredItemCreators.count(name)) { _registeredItemCreators[name] = std::move(creator); - _categories.insert(category); + _registeredModelsOrder.push_back(name); _registeredModelsCategory[name] = category; + + if (!_categories.count(category)) { + _categories.insert(category); + _categoriesOrder.push_back(category); + } } } @@ -138,11 +155,16 @@ class NODE_EDITOR_PUBLIC DataModelRegistry registerModelImpl(RegistryItemCreator creator, QString const &category ) { const QString name = creator()->name(); - if (_registeredItemCreators.count(name) == 0) + if (!_registeredItemCreators.count(name)) { _registeredItemCreators[name] = std::move(creator); - _categories.insert(category); + _registeredModelsOrder.push_back(name); _registeredModelsCategory[name] = category; + + if (!_categories.count(category)) { + _categories.insert(category); + _categoriesOrder.push_back(category); + } } } diff --git a/include/nodes/internal/FlowView.hpp b/include/nodes/internal/FlowView.hpp index 378ff2e6d..44959eaef 100644 --- a/include/nodes/internal/FlowView.hpp +++ b/include/nodes/internal/FlowView.hpp @@ -1,6 +1,9 @@ #pragma once +#include + #include +#include #include "Export.hpp" diff --git a/src/DataModelRegistry.cpp b/src/DataModelRegistry.cpp index 4a8b202d8..c7c271fb2 100644 --- a/src/DataModelRegistry.cpp +++ b/src/DataModelRegistry.cpp @@ -1,5 +1,7 @@ #include "DataModelRegistry.hpp" +#include + #include #include @@ -30,6 +32,12 @@ registeredModelCreators() const return _registeredItemCreators; } +DataModelRegistry::RegisteredModelsOrder const & +DataModelRegistry:: +registeredModelsOrder() const +{ + return _registeredModelsOrder; +} DataModelRegistry::RegisteredModelsCategoryMap const & DataModelRegistry:: @@ -47,6 +55,21 @@ categories() const } +DataModelRegistry::CategoriesOrder const & +DataModelRegistry:: +categoriesOrder() const +{ + return _categoriesOrder; +} + +void +DataModelRegistry:: +sortCategories() +{ + std::sort(_categoriesOrder.begin(), _categoriesOrder.end()); +} + + TypeConverter DataModelRegistry:: getTypeConverter(NodeDataType const & d1, diff --git a/src/FlowView.cpp b/src/FlowView.cpp index 2e94d8875..5f34d6a05 100644 --- a/src/FlowView.cpp +++ b/src/FlowView.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -22,9 +23,15 @@ #include "NodeGraphicsObject.hpp" #include "ConnectionGraphicsObject.hpp" #include "StyleCollection.hpp" +#include "ModelSelectionWidget.hpp" using QtNodes::FlowView; using QtNodes::FlowScene; +using QtNodes::ModelSelectionWidget; + + +static auto const SkipText = QStringLiteral("skip me"); + FlowView:: FlowView(QWidget *parent) @@ -100,7 +107,7 @@ FlowView::setScene(FlowScene *scene) void FlowView:: -contextMenuEvent(QContextMenuEvent *event) +contextMenuEvent(QContextMenuEvent* event) { if (itemAt(event->pos())) { @@ -110,56 +117,14 @@ contextMenuEvent(QContextMenuEvent *event) QMenu modelMenu; - auto skipText = QStringLiteral("skip me"); - - //Add filterbox to the context menu - auto *txtBox = new QLineEdit(&modelMenu); - - txtBox->setPlaceholderText(QStringLiteral("Filter")); - txtBox->setClearButtonEnabled(true); - - auto *txtBoxAction = new QWidgetAction(&modelMenu); - txtBoxAction->setDefaultWidget(txtBox); - - modelMenu.addAction(txtBoxAction); + auto* modelSelectionWidget = new ModelSelectionWidget(_scene->registry(), &modelMenu); - //Add result treeview to the context menu - auto *treeView = new QTreeWidget(&modelMenu); - treeView->header()->close(); - - auto *treeViewAction = new QWidgetAction(&modelMenu); - treeViewAction->setDefaultWidget(treeView); - - modelMenu.addAction(treeViewAction); - - QMap topLevelItems; - for (auto const &cat : _scene->registry().categories()) + for (QAction* action : modelSelectionWidget->actions()) { - auto item = new QTreeWidgetItem(treeView); - item->setText(0, cat); - item->setData(0, Qt::UserRole, skipText); - topLevelItems[cat] = item; + modelMenu.addAction(action); } - for (auto const &assoc : _scene->registry().registeredModelsCategoryAssociation()) - { - auto parent = topLevelItems[assoc.second]; - auto item = new QTreeWidgetItem(parent); - item->setText(0, assoc.first); - item->setData(0, Qt::UserRole, assoc.first); - } - - treeView->expandAll(); - - connect(treeView, &QTreeWidget::itemClicked, [&](QTreeWidgetItem *item, int) - { - QString modelName = item->data(0, Qt::UserRole).toString(); - - if (modelName == skipText) - { - return; - } - + connect(modelSelectionWidget, &ModelSelectionWidget::modelSelected, [&](QString modelName) { auto type = _scene->registry().create(modelName); if (type) @@ -180,30 +145,6 @@ contextMenuEvent(QContextMenuEvent *event) modelMenu.close(); }); - //Setup filtering - connect(txtBox, &QLineEdit::textChanged, [&](const QString &text) - { - for (auto& topLvlItem : topLevelItems) - { - for (int i = 0; i < topLvlItem->childCount(); ++i) - { - auto child = topLvlItem->child(i); - auto modelName = child->data(0, Qt::UserRole).toString(); - if (modelName.contains(text, Qt::CaseInsensitive)) - { - child->setHidden(false); - } - else - { - child->setHidden(true); - } - } - } - }); - - // make sure the text box gets focus so the user doesn't have to click on it - txtBox->setFocus(); - modelMenu.exec(event->globalPos()); } diff --git a/src/ModelSelectionWidget.cpp b/src/ModelSelectionWidget.cpp new file mode 100644 index 000000000..d7bf30ad8 --- /dev/null +++ b/src/ModelSelectionWidget.cpp @@ -0,0 +1,102 @@ +#include "ModelSelectionWidget.hpp" + +#include + +#include +#include +#include +#include +#include + +#include + +using QtNodes::DataModelRegistry; +using QtNodes::ModelSelectionWidget; + + +static auto const SkipText = QStringLiteral("skip me"); + + +ModelSelectionWidget:: +ModelSelectionWidget(DataModelRegistry& registry, QWidget* parent) + : QWidget(parent) +{ + auto* layout = new QVBoxLayout(this); + setLayout(layout); + + // Add filterbox to the context menu + auto* filter = new QLineEdit(this); + + filter->setPlaceholderText(QStringLiteral("Filter")); + filter->setClearButtonEnabled(true); + + auto* filterAction = new QWidgetAction(this); + filterAction->setDefaultWidget(filter); + + addAction(filterAction); + layout->addWidget(filter); + + // Add result treeview to the context menu + auto* treeView = new QTreeWidget(this); + treeView->header()->close(); + + auto* treeViewAction = new QWidgetAction(this); + treeViewAction->setDefaultWidget(treeView); + + addAction(treeViewAction); + layout->addWidget(treeView); + + QMap topLevelItems; + for (auto const& cat : registry.categoriesOrder()) + { + auto item = new QTreeWidgetItem(treeView); + item->setText(0, cat); + item->setData(0, Qt::UserRole, SkipText); + topLevelItems[cat] = item; + } + + auto const& assocCategory = registry.registeredModelsCategoryAssociation(); + for (auto const& name : registry.registeredModelsOrder()) + { + auto topLevelParent = topLevelItems[assocCategory.at(name)]; + auto item = new QTreeWidgetItem(topLevelParent); + item->setText(0, name); + item->setData(0, Qt::UserRole, name); + } + + treeView->expandAll(); + + connect(treeView, &QTreeWidget::itemClicked, this, [this](QTreeWidgetItem* item) { + QString modelName = item->data(0, Qt::UserRole).toString(); + + if (modelName == SkipText) + { + return; + } + + emit modelSelected(modelName); + }); + + // Setup filtering + connect(filter, &QLineEdit::textChanged, [topLevelItems = std::move(topLevelItems)](const QString& text) { + for (auto& topLvlItem : topLevelItems) + { + for (int i = 0; i < topLvlItem->childCount(); ++i) + { + auto child = topLvlItem->child(i); + auto modelName = child->data(0, Qt::UserRole).toString(); + if (modelName.contains(text, Qt::CaseInsensitive)) + { + child->setHidden(false); + } + else + { + child->setHidden(true); + } + } + } + }); + + // make sure the text box gets focus so the user doesn't have to click on it + filter->setFocus(); +} diff --git a/src/ModelSelectionWidget.hpp b/src/ModelSelectionWidget.hpp new file mode 100644 index 000000000..cc37600be --- /dev/null +++ b/src/ModelSelectionWidget.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#include "Export.hpp" + +namespace QtNodes +{ + +class DataModelRegistry; + +class NODE_EDITOR_PUBLIC ModelSelectionWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ModelSelectionWidget(DataModelRegistry& registry, QWidget* parent = Q_NULLPTR); + +signals: + void + modelSelected(QString modelName); +}; + +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d93b391f2..368a1fd58 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -5,14 +5,14 @@ add_executable(test_nodes test_main.cpp src/test_dragging.cpp src/TestDataModelRegistry.cpp + src/TestModelSelectionWidget.cpp src/TestNodeGraphicsObject.cpp ) target_include_directories(test_nodes PRIVATE - ../src - ../include/internal include + $ ) target_link_libraries(test_nodes diff --git a/test/include/Stringify.hpp b/test/include/Stringify.hpp index f6f349524..dee051e30 100644 --- a/test/include/Stringify.hpp +++ b/test/include/Stringify.hpp @@ -2,6 +2,7 @@ #include #include +#include #include @@ -28,4 +29,14 @@ struct StringMaker return std::string(QTest::toString(p)); } }; + +template <> +struct StringMaker +{ + static std::string + convert(QString const& str) + { + return str.toStdString(); + } +}; } diff --git a/test/src/TestModelSelectionWidget.cpp b/test/src/TestModelSelectionWidget.cpp new file mode 100644 index 000000000..7f0bb7fe9 --- /dev/null +++ b/test/src/TestModelSelectionWidget.cpp @@ -0,0 +1,141 @@ +#include "ModelSelectionWidget.hpp" + +#include + +#include +#include +#include + +#include + +#include "ApplicationSetup.hpp" +#include "Stringify.hpp" +#include "StubNodeDataModel.hpp" + +using QtNodes::DataModelRegistry; +using QtNodes::ModelSelectionWidget; + +#include +#include + +TEST_CASE("ModelSelectionWidget models order (within a category)", "[gui]") +{ + class StubModel : public StubNodeDataModel + { + public: + StubModel(QString modelName) + : modelName(modelName) + { + } + + QString + name() const override + { + return modelName; + } + + QString modelName; + }; + + auto setup = applicationSetup(); + + DataModelRegistry registry; + registry.registerModel([] { + return std::make_unique("a"); + }); + registry.registerModel([] { + return std::make_unique("c"); + }); + registry.registerModel([] { + return std::make_unique("b"); + }); + + auto widget = std::make_unique(registry); + + auto tree = widget->findChild(); + + { + INFO("Test out of date. ModelSelectionWidget doesn't use QTreeWidget"); + REQUIRE(tree != nullptr); + } + + REQUIRE(tree->topLevelItemCount() == 1); + + QTreeWidgetItem* mainCategory = tree->topLevelItem(0); + + REQUIRE(mainCategory->childCount() == 3); + CHECK(mainCategory->child(0)->data(0, Qt::UserRole).toString().toStdString() == "a"); + CHECK(mainCategory->child(1)->data(0, Qt::UserRole).toString().toStdString() == "c"); + CHECK(mainCategory->child(2)->data(0, Qt::UserRole).toString().toStdString() == "b"); +} + +#include +#include + +TEST_CASE("ModelSelectionWidget category order", "[gui]") +{ + auto setup = applicationSetup(); + + DataModelRegistry registry; + + registry.registerModel("Category A", [] { + auto result = std::make_unique(); + + result->name("A"); + + return result; + }); + + registry.registerModel("Category C", [] { + auto result = std::make_unique(); + + result->name("C"); + + return result; + }); + + registry.registerModel("Category B", [] { + auto result = std::make_unique(); + + result->name("B"); + + return result; + }); + + SECTION("unsorted categories") + { + auto widget = std::make_unique(registry); + + auto tree = widget->findChild(); + + { + INFO("Test out of date. ModelSelectionWidget doesn't use QTreeWidget"); + REQUIRE(tree != nullptr); + } + + REQUIRE(tree->topLevelItemCount() == 3); + + CHECK(tree->topLevelItem(0)->text(0).toStdString() == "Category A"); + CHECK(tree->topLevelItem(1)->text(0).toStdString() == "Category C"); + CHECK(tree->topLevelItem(2)->text(0).toStdString() == "Category B"); + } + SECTION("sorted categories") + { + registry.sortCategories(); + + auto widget = std::make_unique(registry); + + auto tree = widget->findChild(); + + { + INFO("Test out of date. ModelSelectionWidget doesn't use QTreeWidget"); + REQUIRE(tree != nullptr); + } + + REQUIRE(tree->topLevelItemCount() == 3); + + CHECK(tree->topLevelItem(0)->text(0).toStdString() == "Category A"); + CHECK(tree->topLevelItem(1)->text(0).toStdString() == "Category B"); + CHECK(tree->topLevelItem(2)->text(0).toStdString() == "Category C"); + } +}