From 8136ca707641d4167709b9672013c0431c58f0ee Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Tue, 12 Mar 2024 16:42:22 -0400 Subject: [PATCH 01/42] ENH: Added the ability to override context menus for qtTaskNodes Added a virtual method to qtBaseTaskNode to setup a context menu for a qtTaskNode. --- doc/release/notes/qtBaseTaskNodeChanges.rst | 7 ++ smtk/extension/qt/diagram/qtBaseTaskNode.h | 5 + .../qt/diagram/qtDefaultTaskNode.cxx | 105 ++++++++++-------- smtk/extension/qt/diagram/qtDefaultTaskNode.h | 6 + 4 files changed, 74 insertions(+), 49 deletions(-) create mode 100644 doc/release/notes/qtBaseTaskNodeChanges.rst diff --git a/doc/release/notes/qtBaseTaskNodeChanges.rst b/doc/release/notes/qtBaseTaskNodeChanges.rst new file mode 100644 index 000000000..6c45c1c8e --- /dev/null +++ b/doc/release/notes/qtBaseTaskNodeChanges.rst @@ -0,0 +1,7 @@ +QT UI Changes +============= + +Custom TaskNode Context Menus +----------------------------- + +Added a virtual method to qtBaseTaskNode to setup a context menu for a qtTaskNode. diff --git a/smtk/extension/qt/diagram/qtBaseTaskNode.h b/smtk/extension/qt/diagram/qtBaseTaskNode.h index 96e3e3a8a..a8d115843 100644 --- a/smtk/extension/qt/diagram/qtBaseTaskNode.h +++ b/smtk/extension/qt/diagram/qtBaseTaskNode.h @@ -78,6 +78,11 @@ class SMTKQTEXT_EXPORT qtBaseTaskNode : public qtBaseObjectNode /// Return true if the task is currently active (i.e., being worked on by the user). bool isActive() const; + ///Setup a context menu to be displayed by the node's underlying widget + /// + /// Returns true if setting up the menu was successful. + virtual bool setupContextMenu(QMenu*) { return false; } + /// Deals with state updates virtual void updateTaskState(smtk::task::State prev, smtk::task::State next, bool active) = 0; diff --git a/smtk/extension/qt/diagram/qtDefaultTaskNode.cxx b/smtk/extension/qt/diagram/qtDefaultTaskNode.cxx index 5f4efcf1e..1d56da48b 100644 --- a/smtk/extension/qt/diagram/qtDefaultTaskNode.cxx +++ b/smtk/extension/qt/diagram/qtDefaultTaskNode.cxx @@ -121,7 +121,7 @@ class DefaultTaskNodeWidget // need to do this QObject::connect( m_title, &QPushButton::customContextMenuRequested, this, [self, this](const QPoint& pt) { - if (!self) + if (!(self && self->m_node)) { return; } @@ -131,11 +131,10 @@ class DefaultTaskNodeWidget QMenu* menu = new QMenu(); menu->setObjectName(menuName); - auto* renameAction = new QAction("Rename task…"); - renameAction->setObjectName("RenameTaskAction"); - QObject::connect( - renameAction, &QAction::triggered, self.data(), &DefaultTaskNodeWidget::renameTask); - menu->addAction(renameAction); + if (!self->m_node->setupContextMenu(menu)) + { + return; + } // Figure out where to place the context menu. // Simply calling self->mapToGlobal(pt) doesn't work; instead we must // translate from the proxy-widget's coordinates to the parent node's coordinates, @@ -150,49 +149,6 @@ class DefaultTaskNodeWidget this->updateTaskState(m_node->m_task->state(), m_node->m_task->state(), m_node->isActive()); } - // This method runs the rename task operation - bool renameTask() - { - bool ok; - auto* task = m_node->task(); - if (!task) - { - return false; - } - // Request the new name from the user - QString origName(task->name().c_str()); - QString newName = QInputDialog::getText( - nullptr, tr("Renaming Task"), tr("New Task Name: "), QLineEdit::Normal, origName, &ok); - // If the name is the same or the user canceled the dialog just return - if (!ok || (origName == newName)) - { - return false; - } - - // Prep and run the operation (if possible) - auto opMgr = task->manager()->managers()->get(); - auto op = opMgr->create("smtk::task::RenameTask"); - if (!op) - { - return false; - } - if (!op->parameters()->associate(task->shared_from_this())) - { - return false; - } - if (!op->parameters()->findAs("name")->setValue( - newName.toStdString())) - { - return false; - } - if (!op->ableToOperate()) - { - return false; - } - opMgr->launchers()(op); - return true; - } - void flashWarning() { // TODO: pulse the widget background. @@ -439,5 +395,56 @@ void qtDefaultTaskNode::dataUpdated() m_container->m_title->setText(QString::fromStdString(m_task->name())); } +// This method runs the rename task operation +bool qtDefaultTaskNode::renameTask() +{ + bool ok; + if (!m_task) + { + return false; + } + // Request the new name from the user + QString origName(m_task->name().c_str()); + QString newName = QInputDialog::getText( + nullptr, tr("Renaming Task"), tr("New Task Name: "), QLineEdit::Normal, origName, &ok); + // If the name is the same or the user canceled the dialog just return + if (!ok || (origName == newName)) + { + return false; + } + + // Prep and run the operation (if possible) + auto opMgr = m_task->manager()->managers()->get(); + auto op = opMgr->create("smtk::task::RenameTask"); + if (!op) + { + return false; + } + if (!op->parameters()->associate(m_task->shared_from_this())) + { + return false; + } + if (!op->parameters()->findAs("name")->setValue( + newName.toStdString())) + { + return false; + } + if (!op->ableToOperate()) + { + return false; + } + opMgr->launchers()(op); + return true; +} + +bool qtDefaultTaskNode::setupContextMenu(QMenu* menu) +{ + auto* renameAction = new QAction("Rename task…"); + renameAction->setObjectName("RenameTaskAction"); + QObject::connect(renameAction, &QAction::triggered, this, &qtDefaultTaskNode::renameTask); + menu->addAction(renameAction); + return menu; +} + } // namespace extension } // namespace smtk diff --git a/smtk/extension/qt/diagram/qtDefaultTaskNode.h b/smtk/extension/qt/diagram/qtDefaultTaskNode.h index dadcaa94e..f97d94fcd 100644 --- a/smtk/extension/qt/diagram/qtDefaultTaskNode.h +++ b/smtk/extension/qt/diagram/qtDefaultTaskNode.h @@ -70,6 +70,9 @@ class SMTKQTEXT_EXPORT qtDefaultTaskNode : public qtBaseTaskNode /// Handle renames, etc. void dataUpdated() override; + /// Setup a context menu for this type of node + bool setupContextMenu(QMenu*) override; + protected: friend class DefaultTaskNodeWidget; void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) override; @@ -77,6 +80,9 @@ class SMTKQTEXT_EXPORT qtDefaultTaskNode : public qtBaseTaskNode /// Update the node bounds to fit its content. int updateSize() override; + /// Change the name of the task + bool renameTask(); + DefaultTaskNodeWidget* m_container{ nullptr }; }; From 767b5a916df068b0ccd05652c70fb9f3f695b1a1 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Mon, 25 Mar 2024 17:09:43 -0400 Subject: [PATCH 02/42] BUG: Fixing Iterator API for ValueItems Also added an iterator section to an existing test. Closes #530 --- smtk/attribute/ValueItemTemplate.h | 6 ++-- smtk/attribute/testing/cxx/unitDoubleItem.cxx | 33 ++++++++++++++++++- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/smtk/attribute/ValueItemTemplate.h b/smtk/attribute/ValueItemTemplate.h index 9ce7d4f10..cd82e6eff 100644 --- a/smtk/attribute/ValueItemTemplate.h +++ b/smtk/attribute/ValueItemTemplate.h @@ -36,12 +36,12 @@ class SMTK_ALWAYS_EXPORT ValueItemTemplate : public ValueItem public: typedef DataT DataType; typedef typename std::vector value_type; - typedef value_type const_iterator; + typedef typename value_type::const_iterator const_iterator; typedef ValueItemDefinitionTemplate DefType; ~ValueItemTemplate() override = default; - typename std::vector::const_iterator begin() const { return m_values.begin(); } - typename std::vector::const_iterator end() const { return m_values.end(); } + const_iterator begin() const { return m_values.begin(); } + const_iterator end() const { return m_values.end(); } bool setNumberOfValues(std::size_t newSize) override; ///@{ diff --git a/smtk/attribute/testing/cxx/unitDoubleItem.cxx b/smtk/attribute/testing/cxx/unitDoubleItem.cxx index 82a5a821a..eac74929f 100644 --- a/smtk/attribute/testing/cxx/unitDoubleItem.cxx +++ b/smtk/attribute/testing/cxx/unitDoubleItem.cxx @@ -109,6 +109,10 @@ class DoubleItemTest smtkTest( !mp_doubleItemDef1->setDefaultValueAsString("10.0 m/s"), "Was able to set default to 10 m/s"); + mp_doubleVecItemDef = DoubleItemDefinition::New("testDoubleVecItem"); + mp_doubleVecItemDef->setNumberOfRequiredValues(3); + mp_def->addItemDefinition(mp_doubleVecItemDef); + // Simple test with no units system auto ddef = DoubleItemDefinition::New("NoUnitsDef"); smtkTest(ddef->setUnits("cm"), "Could not set units to cm"); @@ -146,6 +150,23 @@ class DoubleItemTest return mp_att->findDouble("testDoubleItem1"); } + DoubleItemPtr getDoubleVecItem() + { + if (!mp_att) + { + mp_att = mp_attRes->createAttribute(mp_def); + } + auto item = mp_att->findDouble("testDoubleVecItem"); + // Is the vector initialized? + if (!item->isValid()) + { + item->setValue(0, 0); + item->setValue(1, 1); + item->setValue(2, 2); + } + return item; + } + void SetUpDoubleItemTestExpressions() { mp_evaluatorDef = mp_attRes->createDefinition("doubleItemTestExpression"); @@ -166,6 +187,7 @@ class DoubleItemTest DefinitionPtr mp_def; DoubleItemDefinitionPtr mp_doubleItemDef; DoubleItemDefinitionPtr mp_doubleItemDef1; + DoubleItemDefinitionPtr mp_doubleVecItemDef; DefinitionPtr mp_evaluatorDef; }; @@ -196,6 +218,15 @@ void testBasicGettingValue() smtkTest(item1->value() == 15.0, "Expected value to be 15.0 cm."); smtkTest(item1->valueAsString() == "150 mm", "Expected value to be 150 mm."); smtkTest(!item1->setValueFromString(0, "150 cm/s"), "Could set value to 150 cm/s"); + + // Test Iterator Interface + item = doubleItemTest.getDoubleVecItem(); + double answer = 0; + for (auto it = item->begin(); it != item->end(); ++it) + { + smtkTest(*it == answer, "Expected iterator value: " << answer << " but found: " << *it); + ++answer; + } } void testGettingValueWithExpression() @@ -277,7 +308,7 @@ void testValueAsString() } } -// Test XML reading and writing for evalutors by writing a Attribute Resource, +// Test XML reading and writing for evaluators by writing a Attribute Resource, // reading it, then writing it again to get the same result. void testEvaluatorXMLIO() { From 837e48a82a4e445112d77154aed2d4ea288dd600 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Tue, 9 Apr 2024 15:43:57 -0400 Subject: [PATCH 03/42] ENH: Added Template Support When Defining Analyses SMTK XML Attribute File now supports instantiation of templates inside the Analyses XML Element Block. --- doc/release/notes/templateSupportChanges.rst | 8 ++ smtk/io/XmlDocV1Parser.cxx | 136 ++++++++++++------- smtk/io/XmlDocV1Parser.h | 3 + 3 files changed, 95 insertions(+), 52 deletions(-) create mode 100644 doc/release/notes/templateSupportChanges.rst diff --git a/doc/release/notes/templateSupportChanges.rst b/doc/release/notes/templateSupportChanges.rst new file mode 100644 index 000000000..9e6ff1868 --- /dev/null +++ b/doc/release/notes/templateSupportChanges.rst @@ -0,0 +1,8 @@ +Changes in SMTK IO +================== + +Added Template Support When Defining Analyses +--------------------------------------------- + +SMTK XML Attribute File now supports instantiation of templates inside the Analyses +XML Element Block. diff --git a/smtk/io/XmlDocV1Parser.cxx b/smtk/io/XmlDocV1Parser.cxx index 0cf302514..59f81c04f 100644 --- a/smtk/io/XmlDocV1Parser.cxx +++ b/smtk/io/XmlDocV1Parser.cxx @@ -635,66 +635,21 @@ void XmlDocV1Parser::process( // Add the new Categories seccategories.insert(newCats.begin(), newCats.end()); + // Since Analyses Block can support template instantiation + // We need to process Template Definitions prior to Analyses + this->processTemplatesDefinitions(amnode, globalTemplateMap); + // Process Analysis Info - std::set categories; node = amnode.child("Analyses"); if (node) { attribute::Analyses& analyses = m_resource->analyses(); - xml_node anode; auto xatt = node.attribute("Exclusive"); if (xatt && xatt.as_bool()) { analyses.setTopLevelExclusive(true); } - for (anode = node.first_child(); anode; anode = anode.next_sibling()) - { - s = anode.attribute("Type").value(); - categories.clear(); - for (cnode = anode.first_child(); cnode; cnode = cnode.next_sibling()) - { - if (cnode.text().empty()) - { - continue; - } - categories.insert(cnode.text().get()); - } - auto* analysis = analyses.create(s); - if (analysis == nullptr) - { - smtkErrorMacro(m_logger, "Failed to create Analysis: " << s); - continue; - } - analysis->setLocalCategories(categories); - // Does this analysis have a base type? - xatt = anode.attribute("BaseType"); - if (xatt) - { - const auto* bt = xatt.value(); - auto* parent = analyses.find(bt); - if (parent == nullptr) - { - smtkErrorMacro(m_logger, "Failed to set Analysis: " << s << " parent to " << bt); - } - analysis->setParent(parent); - } - xatt = anode.attribute("Exclusive"); - if (xatt && xatt.as_bool()) - { - analysis->setExclusive(true); - } - xatt = anode.attribute("Required"); - if (xatt && xatt.as_bool()) - { - analysis->setRequired(true); - } - // Does the analysis have a label? - xatt = anode.attribute("Label"); - if (xatt) - { - analysis->setLabel(xatt.value()); - } - } + this->processAnalysisInformationChildren(node); } // Process AdvanceLevel Info @@ -728,7 +683,6 @@ void XmlDocV1Parser::process( } this->processConfigurations(amnode); this->processItemDefinitionBlocks(amnode, globalTemplateMap); - this->processTemplatesDefinitions(amnode, globalTemplateMap); this->processAssociationRules(amnode); this->processAttributeInformation(amnode); this->processViews(amnode); @@ -774,6 +728,84 @@ void XmlDocV1Parser::process( this->processHints(amnode); } +void XmlDocV1Parser::processAnalysisInformationChildren(xml_node& node) +{ + for (xml_node child = node.first_child(); child; child = child.next_sibling()) + { + std::string nodeName = child.name(); + if ((nodeName == "Block") || (nodeName == "Template")) + { + pugi::xml_node instancedTemplateNode; + if (this->createXmlFromTemplate(child, instancedTemplateNode)) + { + this->processAnalysisInformationChildren(instancedTemplateNode); + this->releaseXmlTemplate(instancedTemplateNode); + } + continue; + } + if (nodeName == "Analysis") + { + this->createAnalysis(child); + continue; + } + smtkWarningMacro(m_logger, "Skipping unsupported Analysis child element type: " << nodeName); + } +} + +void XmlDocV1Parser::createAnalysis(xml_node& anode) +{ + attribute::Analyses& analyses = m_resource->analyses(); + std::set categories; + std::string s; + xml_node cnode; + xml_attribute xatt; + + s = anode.attribute("Type").value(); + for (cnode = anode.first_child(); cnode; cnode = cnode.next_sibling()) + { + if (cnode.text().empty()) + { + continue; + } + categories.insert(cnode.text().get()); + } + auto* analysis = analyses.create(s); + if (analysis == nullptr) + { + smtkErrorMacro(m_logger, "Failed to create Analysis: " << s); + return; + } + analysis->setLocalCategories(categories); + // Does this analysis have a base type? + xatt = anode.attribute("BaseType"); + if (xatt) + { + const auto* bt = xatt.value(); + auto* parent = analyses.find(bt); + if (parent == nullptr) + { + smtkErrorMacro(m_logger, "Failed to set Analysis: " << s << " parent to " << bt); + } + analysis->setParent(parent); + } + xatt = anode.attribute("Exclusive"); + if (xatt && xatt.as_bool()) + { + analysis->setExclusive(true); + } + xatt = anode.attribute("Required"); + if (xatt && xatt.as_bool()) + { + analysis->setRequired(true); + } + // Does the analysis have a label? + xatt = anode.attribute("Label"); + if (xatt) + { + analysis->setLabel(xatt.value()); + } +} + void XmlDocV1Parser::processItemDefinitionBlocks( xml_node& root, std::map>& globalTemplateMap) @@ -809,7 +841,7 @@ void XmlDocV1Parser::processTemplatesDefinitions( xml_node child, node = root.child("Templates"); if (!node) { - return; // There are no ItemBlocks + return; // There are no Templates } xml_attribute xnsatt = node.attribute("Namespace"); diff --git a/smtk/io/XmlDocV1Parser.h b/smtk/io/XmlDocV1Parser.h index b1bca8c4a..575ac83de 100644 --- a/smtk/io/XmlDocV1Parser.h +++ b/smtk/io/XmlDocV1Parser.h @@ -119,6 +119,9 @@ class SMTKCORE_EXPORT XmlDocV1Parser void processDefinitionInformationChildren(pugi::xml_node& node); void createDefinition(pugi::xml_node& defNode); + void processAnalysisInformationChildren(pugi::xml_node& node); + void createAnalysis(pugi::xml_node& analysisNode); + virtual void processDefinition(pugi::xml_node& defNode, smtk::attribute::DefinitionPtr& def); virtual void processDefinitionAtts(pugi::xml_node& defNode, smtk::attribute::DefinitionPtr& def); virtual void processDefinitionContents( From 014d8f4294ce0550c38fa8d28841f1fec68e439d Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 17 Apr 2024 16:08:24 -0400 Subject: [PATCH 04/42] ENH: Replaced GridLayout in qtDiagram On some systems the layout was not allowing the contents to fully utilize the space available. The widget now uses 2 layouts - a VBox for the toolbar (if needed) and a HBox for the drawer and diagram itself. --- doc/release/notes/qt-diagram-issues.rst | 1 + smtk/extension/qt/diagram/qtDiagram.cxx | 34 +++++++++++++++---------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/doc/release/notes/qt-diagram-issues.rst b/doc/release/notes/qt-diagram-issues.rst index 231c62b30..5bc9a93f3 100644 --- a/doc/release/notes/qt-diagram-issues.rst +++ b/doc/release/notes/qt-diagram-issues.rst @@ -11,3 +11,4 @@ Diagram view issues fixed keys (previously, only backspace worked). + Make shift in "select" mode temporarily enter "pan" mode to mirror the behavior of the same key in "pan" mode (which enters "select"). ++ Replaced GridLayout which was causing the widget not to expand to consume all of the available space. diff --git a/smtk/extension/qt/diagram/qtDiagram.cxx b/smtk/extension/qt/diagram/qtDiagram.cxx index eed238c41..eae1ab5c4 100644 --- a/smtk/extension/qt/diagram/qtDiagram.cxx +++ b/smtk/extension/qt/diagram/qtDiagram.cxx @@ -75,8 +75,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -85,6 +85,7 @@ #include #include #include +#include #include #include @@ -106,11 +107,12 @@ class qtDiagram::Internal , m_self(self) , m_scene(new qtDiagramScene(m_self)) , m_widget(new QWidget) - // m_widget contains a QGridLayout with two overlapping widgets: - , m_sidebarOuterLayout(new QGridLayout(m_widget)) + // Layout for possibly the toolbar and the rest of the widget's contents: + , m_mainLayout(new QVBoxLayout) + // Layout for the actual diagram and drawer + , m_contentsLayout(new QHBoxLayout) , m_sidebarOuter(new QFrame) , m_view(new qtDiagramView(m_scene, m_self)) - // m_sidebarOuter contains a QGridLayout with two overlapping widgets: , m_sidebarMiddleLayout(new QVBoxLayout(m_sidebarOuter)) , m_sidebarSizer(new QSizeGrip(m_sidebarOuter)) , m_sidebarMiddle(new QScrollArea) @@ -121,7 +123,7 @@ class qtDiagram::Internal , m_legend(new qtDiagramLegend("Legend", m_self)) { // If this view is inside a dock-widget, take over the dock's title-bar. - // Otherwise, put the dock inside m_sidebarOuterLayout above m_view. + // Otherwise, put the dock inside m_mainLayout above m_view. m_dock = nullptr; QObject* dp = info.get(); while (dp && !m_dock) @@ -129,28 +131,31 @@ class qtDiagram::Internal m_dock = qobject_cast(dp); dp = dp->parent(); } - const int gridRow = m_dock ? 0 : 1; m_toolbar = new QToolBar(m_dock ? static_cast>(m_dock) : m_widget); m_toolbar->setObjectName("DiagramToolbar"); m_toolbar->setIconSize(QSize(16, 16)); + m_mainLayout->setObjectName("MainLayout"); + m_mainLayout->setSpacing(0); + m_scene->setObjectName("DiagramScene"); - m_widget->setObjectName("OuterDiagramGrid"); - m_widget->setLayout(m_sidebarOuterLayout); + m_widget->setObjectName("OuterDiagramWdiget"); + m_widget->setLayout(m_mainLayout); m_self->Widget = m_widget; - m_sidebarOuterLayout->setObjectName("SidebarOuterLayout"); - m_sidebarOuterLayout->setSpacing(0); + m_contentsLayout->setObjectName("ContentsLayout"); + m_contentsLayout->setSpacing(0); if (m_dock) { m_dock->setTitleBarWidget(m_toolbar); } else { - m_sidebarOuterLayout->addWidget(m_toolbar, 0, 0, Qt::AlignLeft | Qt::AlignTop); + m_mainLayout->addWidget(m_toolbar, Qt::AlignLeft | Qt::AlignTop); } - m_sidebarOuterLayout->addWidget(m_view, gridRow, 0, Qt::AlignLeft | Qt::AlignTop); - m_sidebarOuterLayout->addWidget(m_sidebarOuter, gridRow, 0, Qt::AlignLeft | Qt::AlignTop); + m_mainLayout->addLayout(m_contentsLayout); + m_contentsLayout->addWidget(m_sidebarOuter, Qt::AlignLeft | Qt::AlignTop); + m_contentsLayout->addWidget(m_view, Qt::AlignLeft | Qt::AlignTop); m_sidebarOuter->setObjectName("SidebarOuter"); // Mark the sidebar "outer" widget as a subwindow so it will // be resized by the QSizeGrip contained inside in the "middle" layout. @@ -576,7 +581,8 @@ class qtDiagram::Internal qtDiagramScene* m_scene{ nullptr }; QPointer m_widget; - QPointer m_sidebarOuterLayout; + QPointer m_mainLayout; + QPointer m_contentsLayout; QPointer m_sidebarOuter; qtDiagramView* m_view{ nullptr }; From fe03befad49b65051f2802fa6225c5a0abe7c248 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Sat, 20 Apr 2024 11:17:30 -0400 Subject: [PATCH 05/42] Fix a connect mode crash; allow modes to observe operations. This fixes #531. --- smtk/extension/qt/diagram/qtConnectMode.cxx | 36 +++++++++++++++++++ smtk/extension/qt/diagram/qtConnectMode.h | 11 ++++++ smtk/extension/qt/diagram/qtDiagram.cxx | 6 +++- smtk/extension/qt/diagram/qtDiagramViewMode.h | 19 ++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/smtk/extension/qt/diagram/qtConnectMode.cxx b/smtk/extension/qt/diagram/qtConnectMode.cxx index f3283c0b0..c19c0a054 100644 --- a/smtk/extension/qt/diagram/qtConnectMode.cxx +++ b/smtk/extension/qt/diagram/qtConnectMode.cxx @@ -324,5 +324,41 @@ void qtConnectMode::abandonConnection() } } +qtPreviewArc* qtConnectMode::previewArc() const +{ + return m_previewArc; +} + +void qtConnectMode::updateFromOperation( + std::unordered_set& created, + std::unordered_set& modified, + std::unordered_set& expunged, + const smtk::operation::Operation& operation, + const smtk::operation::Operation::Result& result) +{ + (void)created; + (void)modified; + (void)operation; + (void)result; + + bool resetPreview = false; + auto* predecessor = m_previewArc->predecessor(); + if (predecessor && expunged.find(predecessor->object()) != expunged.end()) + { + resetPreview = true; + } + auto* successor = m_previewArc->successor(); + if (successor && expunged.find(successor->object()) != expunged.end()) + { + resetPreview = true; + } + if (resetPreview) + { + // NB: We do not call abandonConnection() here because that might + // cause us to exit this mode; we only want to reset the preview. + m_previewArc->setPredecessor(nullptr); + } +} + } // namespace extension } // namespace smtk diff --git a/smtk/extension/qt/diagram/qtConnectMode.h b/smtk/extension/qt/diagram/qtConnectMode.h index c5f51510f..1d9642719 100644 --- a/smtk/extension/qt/diagram/qtConnectMode.h +++ b/smtk/extension/qt/diagram/qtConnectMode.h @@ -50,6 +50,17 @@ class SMTKQTEXT_EXPORT qtConnectMode : public qtDiagramViewMode ~qtConnectMode() override; + /// Return the preview arc this mode uses to indicate potential connections. + qtPreviewArc* previewArc() const; + + /// Ensure the preview arc does not become invalid when objects are expunged. + void updateFromOperation( + std::unordered_set& created, + std::unordered_set& modified, + std::unordered_set& expunged, + const smtk::operation::Operation& operation, + const smtk::operation::Operation::Result& result) override; + public Q_SLOTS: void hoverConnectNode(qtBaseObjectNode* node); void clickConnectNode(qtBaseObjectNode* node); diff --git a/smtk/extension/qt/diagram/qtDiagram.cxx b/smtk/extension/qt/diagram/qtDiagram.cxx index eed238c41..c95dd3a16 100644 --- a/smtk/extension/qt/diagram/qtDiagram.cxx +++ b/smtk/extension/qt/diagram/qtDiagram.cxx @@ -538,8 +538,12 @@ class qtDiagram::Internal vv(std::static_pointer_cast(value)); } - // Now iterate over diagram generators to process the objects. + // Now iterate over diagram modes and generators to process the objects. this->resetViewHints(); + for (const auto& entry : m_modeMap) + { + entry.second->updateFromOperation(created, modified, expunged, op, result); + } for (const auto& entry : m_generators) { entry.second->updateScene(created, modified, expunged, op, result); diff --git a/smtk/extension/qt/diagram/qtDiagramViewMode.h b/smtk/extension/qt/diagram/qtDiagramViewMode.h index 6e0dda5b7..fe06d4796 100644 --- a/smtk/extension/qt/diagram/qtDiagramViewMode.h +++ b/smtk/extension/qt/diagram/qtDiagramViewMode.h @@ -14,6 +14,7 @@ #include "smtk/extension/qt/qtBaseView.h" #include "smtk/common/TypeContainer.h" +#include "smtk/operation/Operation.h" // for Operation::Result #include "smtk/PublicPointerDefs.h" @@ -86,6 +87,24 @@ class SMTKQTEXT_EXPORT qtDiagramViewMode : public QObject /// drawn while the mode is active, etc.) virtual void enterMode() {} + /// This method is called by the diagram when an operation is observed. + /// + /// Subclasses may override this if they have state related to persistent + /// objects in order to adapt to those objects being modified or expunged. + virtual void updateFromOperation( + std::unordered_set& created, + std::unordered_set& modified, + std::unordered_set& expunged, + const smtk::operation::Operation& operation, + const smtk::operation::Operation::Result& result) + { + (void)created; + (void)modified; + (void)expunged; + (void)operation; + (void)result; + } + /// A method subclasses may call to invoke deleters on the view's selection. bool removeSelectedObjects(); From 65017049befeb7838f7a697bd3ce52d8dffe7193 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Sat, 20 Apr 2024 11:44:00 -0400 Subject: [PATCH 06/42] Fix a reference-to-temporary issue. --- smtk/task/FillOutAttributes.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smtk/task/FillOutAttributes.cxx b/smtk/task/FillOutAttributes.cxx index f5edf7b49..58a884f65 100644 --- a/smtk/task/FillOutAttributes.cxx +++ b/smtk/task/FillOutAttributes.cxx @@ -218,7 +218,7 @@ bool FillOutAttributes::initializeResources() auto resources = resourceManager->find(); for (const auto& resource : resources) { - const std::string& role = smtk::project::detail::role(resource); + std::string role = smtk::project::detail::role(resource); for (auto& attributeSet : m_attributeSets) { if ( @@ -369,7 +369,7 @@ int FillOutAttributes::update( auto resource = std::dynamic_pointer_cast(weakResource.lock()); if (resource) { - const std::string& role = smtk::project::detail::role(resource); + std::string role = smtk::project::detail::role(resource); // Do we care about this resource? for (auto& predicate : m_attributeSets) { From 71d276f7157dc4151e0088bebe65674d63bad37e Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Fri, 19 Apr 2024 17:11:49 -0400 Subject: [PATCH 07/42] ENH: Supporting Category Expressions You can now specify a category constraint as a category expression instead of defining it as sets of included and excluded category names. This not only provided greater flexibility but is also easier to define. For example in an SBT file this would like the following: (a & !b) & (d | 'category with spaces') Note that in XML & represents & In this example the expression will match if the test set of categories contains **a** and either **d** or **category with spaces** but not **b** Also bumped the file versions of both the XML (to version 8) and JSON (version 7) for Attribute Resources in order to support these changes. --- doc/release/notes/categoryExpressions.rst | 19 ++ doc/userguide/attribute/file-syntax.rst | 91 ++++- smtk/attribute/CMakeLists.txt | 3 + smtk/attribute/Categories.cxx | 318 +++++++++++++++++- smtk/attribute/Categories.h | 110 +++++- smtk/attribute/Definition.h | 15 +- smtk/attribute/ItemDefinition.h | 15 +- smtk/attribute/Resource.cxx | 3 +- smtk/attribute/Resource.h | 2 +- smtk/attribute/ValueItemDefinition.cxx | 21 +- smtk/attribute/ValueItemDefinition.h | 6 +- smtk/attribute/categories/Actions.h | 123 +++++++ smtk/attribute/categories/Evaluators.h | 178 ++++++++++ smtk/attribute/categories/Grammar.h | 101 ++++++ smtk/attribute/json/jsonDefinition.cxx | 86 +++-- smtk/attribute/json/jsonItemDefinition.cxx | 90 +++-- smtk/attribute/json/jsonResource.cxx | 4 +- smtk/attribute/json/jsonValueItemDefinition.h | 74 ++-- smtk/attribute/operators/Read.cxx | 4 +- smtk/attribute/pybind11/PybindCategories.h | 13 +- smtk/attribute/pybind11/PybindDefinition.h | 4 +- .../attribute/pybind11/PybindItemDefinition.h | 4 +- smtk/attribute/pybind11/PybindResource.h | 2 +- smtk/attribute/testing/cxx/categoryTest.cxx | 1 + smtk/attribute/testing/cxx/unitCategories.cxx | 2 +- .../testing/cxx/unitExclusionCategories.cxx | 2 +- .../testing/cxx/unitPassCategories.cxx | 5 +- smtk/io/AttributeReader.cxx | 15 +- smtk/io/AttributeWriter.cxx | 9 +- smtk/io/CMakeLists.txt | 4 + smtk/io/XmlDocV1Parser.cxx | 54 ++- smtk/io/XmlDocV1Parser.h | 8 +- smtk/io/XmlDocV6Parser.cxx | 16 +- smtk/io/XmlDocV6Parser.h | 4 +- smtk/io/XmlDocV8Parser.cxx | 152 +++++++++ smtk/io/XmlDocV8Parser.h | 51 +++ smtk/io/XmlV2StringWriter.cxx | 80 ++--- smtk/io/XmlV3StringWriter.cxx | 37 +- smtk/io/XmlV8StringWriter.cxx | 46 +++ smtk/io/XmlV8StringWriter.h | 45 +++ 40 files changed, 1573 insertions(+), 244 deletions(-) create mode 100644 doc/release/notes/categoryExpressions.rst create mode 100644 smtk/attribute/categories/Actions.h create mode 100644 smtk/attribute/categories/Evaluators.h create mode 100644 smtk/attribute/categories/Grammar.h create mode 100644 smtk/io/XmlDocV8Parser.cxx create mode 100644 smtk/io/XmlDocV8Parser.h create mode 100644 smtk/io/XmlV8StringWriter.cxx create mode 100644 smtk/io/XmlV8StringWriter.h diff --git a/doc/release/notes/categoryExpressions.rst b/doc/release/notes/categoryExpressions.rst new file mode 100644 index 000000000..a4d611fb0 --- /dev/null +++ b/doc/release/notes/categoryExpressions.rst @@ -0,0 +1,19 @@ +Changes in Attribute Resource +============================= + +Supporting Category Expressions +------------------------------- + +You can now specify a category constraint as a category expression instead of defining it as sets of included and excluded category names. +This not only provided greater flexibility but is also easier to define. For example in an SBT file this would like the following: + +.. code-block:: xml + + (a & !b) & (d | 'category with spaces') + +Note that in XML ``&`` represents ``&``. + +In this example the expression will match if the test set of categories contains **a** and either **d** or **category with spaces** but not **b**. + + +Also bumped the file versions of both the XML (to version 8) and JSON (version 7) for Attribute Resources in order to support these changes. diff --git a/doc/userguide/attribute/file-syntax.rst b/doc/userguide/attribute/file-syntax.rst index 316a56ec3..e8e803ca2 100644 --- a/doc/userguide/attribute/file-syntax.rst +++ b/doc/userguide/attribute/file-syntax.rst @@ -1,4 +1,4 @@ -Template File Syntax (Reference) for Version 7 XML Attribute Files +Template File Syntax (Reference) for Version 8 XML Attribute Files ================================================================== File Layout @@ -17,7 +17,7 @@ Attributes that can be included in this XML Element. * - Version - Integer value that indicates the SMTK attribute format (Required) - Current value is 7 (latest version) + Current value is 8 (latest version) * - DisplayHint - Boolean value that indicates if the Attribute Resource should be automatically @@ -731,11 +731,19 @@ This element can contain the following children XML Elements: * - XML Child Element - Description + * - + - Specifies the local category expression specified on the + Definition + + See `CategoryExpression Format`_. + * - - Specifies the local category constraints specified on the Definition See `CategoryInfo Format`_. + **Note** - This is an old style format - please use CategoryExpression + elements instead * - - Defines the items contained within the attributes generated @@ -916,6 +924,62 @@ Attributes that can be included in this XML Element. and 1 inclusive. If not specified its value is 0, 0, 0. + +CategoryExpression Format +~~~~~~~~~~~~~~~~~~~~~~~~~ +This subsection of an AttDef Element describes the local category constants imposed on the Definition. All Item Definitions belonging to +this Definition, as well as all Definitions derived from this, will inherit these constraints unless explicitly told not to. + +The category expression can be specified using one of the following methods: + +* Specifying the PassMode attribute - this is used to create trivial pass all / reject all expressions +* Specifying an expression string which is the contents of the CategoryExpression element + +A category expression string is composed of series of category names separated by either a '|' (representing Or) or a '&' (representing And). +You can used '(' and ')' to model sub-expressions. You can also place a '!' before a category name or sub-expression to mean "not". In terms of categories names, +if the name could be consider a valid C++ variable name, you can simply use the name in the expression. If the name does not conform to this format you can enclose it in single quotes. + +Here is an example of a CategoryExpression Element. + +.. code-block:: xml + + + (a & !b) & (d | 'category with spaces') + + + +Note that in XML ``&`` represents ``&``. + +In this example the expression will match if the test set of categories contains **a** and either **d** or **category with spaces** but not **b**. + +The CategotyInfo Element itself can have the following XML Attributes: + +.. list-table:: XML Attributes for Element + :widths: 10 40 + :header-rows: 1 + :class: smtk-xml-att-table + + * - XML Attribute + - Description + + * - PassMode + - String value representing that the expression will either pass or reject any set of categories being tested + + If set to *All*, then the expression will pass any set of categories provided to it. + + If set to *None*, then the expression will not pass any set of categories provided to it. + + (Optional) + + * - InheritanceMode + - String value that indicates how the Definition should combine its local category information with the category information + inherited from its Base Definition. + + If set to *And*, then its local category information will be and'd with its Base Definition's. If it is set to *Or*, then its local category information will be or'd with its Base Definition's. If it is set to *LocalOnly*, then the Base Definition's category information will be ignored. + + (Optional - the default is *And*) + + CategoryInfo Format ~~~~~~~~~~~~~~~~~~~ This subsection of an AttDef Element describes the local category constants imposed on the Definition. All Item Definitions belonging to @@ -929,7 +993,7 @@ The category information is divided into two elements: The children of these elements include elements whose values are category names. These elements can optionally include an XML attribute called **Combination** which can be set to *Or* or *And*. If set to *And*, all of the categories listed must be active in the owning attribute resource in order for the set to be considered *satisfied*. If *Or* is specified, then only one of the categories need to be active in order for the set to be *satisfied*. The default is *Or*. -The CategotyInfo Element itself can have the following XML Attributes: +The CategoryInfo Element itself can have the following XML Attributes: .. list-table:: XML Attributes for Element :widths: 10 40 @@ -1111,6 +1175,15 @@ This element can contain the following children XML Elements: * - XML Child Element - Description + * - + - Specifies the local category expression specified on the + Item Definition. The format is identical to the one used for Attribute Definitions except + that the category inheritance mode controls how the Item Definition's local category information is + combined with the category information it inherits from either its owning Attribute or Item Definition. + (Optional) + + See `CategoryExpression Format`_. + * - - Specifies the local category constraints specified on the Item Definition. The format is identical to the one used for Attribute Definitions except @@ -1119,6 +1192,8 @@ This element can contain the following children XML Elements: (Optional) See `CategoryInfo Format`_. + **Note** - This is an old style format - please use CategoryExpression + elements instead * - - Provides a brief description of the item (Optional). @@ -1481,6 +1556,14 @@ The Structure Element contains a Value element along with optional category and - Simple form when defining only discrete values without category information or associated Item Definitions. See `Simple Discrete Value Form`_. + * - + - Specifies the additional category constraints associated with this discrete value. + The format is identical to the one used for Attribute Definitions except + that the category inheritance mode is not used. + (Optional) + + See `CategoryExpression Format`_. + * - - Specifies the additional category constraints associated with this discrete value. The format is identical to the one used for Attribute Definitions except @@ -1488,6 +1571,8 @@ The Structure Element contains a Value element along with optional category and (Optional) See `CategoryInfo Format`_. + **Note** - This is an old style format - please use CategoryExpression + elements instead * - - Represents the Children Items that are associated with this discrete value. diff --git a/smtk/attribute/CMakeLists.txt b/smtk/attribute/CMakeLists.txt index 0633ebab2..2f8d47c7b 100644 --- a/smtk/attribute/CMakeLists.txt +++ b/smtk/attribute/CMakeLists.txt @@ -141,6 +141,9 @@ set(attributeHeaders ValueItemTemplate.h VoidItem.h VoidItemDefinition.h + categories/Actions.h + categories/Evaluators.h + categories/Syntax.h filter/Attribute.h filter/Grammar.h update/AttributeUpdateFactory.h diff --git a/smtk/attribute/Categories.cxx b/smtk/attribute/Categories.cxx index 11073fbb7..0af5458b0 100644 --- a/smtk/attribute/Categories.cxx +++ b/smtk/attribute/Categories.cxx @@ -10,22 +10,295 @@ #include "smtk/attribute/Categories.h" +#include "smtk/attribute/categories/Actions.h" +#include "smtk/attribute/categories/Evaluators.h" +#include "smtk/attribute/categories/Grammar.h" + +#include "smtk/io/Logger.h" #include #include #include +namespace +{ +std::string assembleSubExpression(const std::set& cats, const std::string& op) +{ + std::string exp; + if (!cats.empty()) + { + bool first = true; + exp = "("; + for (const std::string& cat : cats) + { + if (first) + { + exp.append("'").append(cat).append("'"); + first = false; + } + else + { + exp.append(" ").append(op).append(" '").append(cat).append("'"); + } + } + exp.append(")"); + } + return exp; +} +} // namespace using namespace smtk::attribute; +Categories::Expression::Expression() +{ + // By default set the expression to reject all + this->setAllReject(); + m_isSet = false; +} + +const std::string& Categories::Expression::expression() const +{ + return m_expression; +} + +bool Categories::Expression::buildEvaluator() +{ + // If we don't have an expression string can we build one? + if (m_expression.empty()) + { + std::string includeSubExpression = assembleSubExpression( + m_includedCategories, Categories::combinationModeAsSymbol(m_includeMode)); + + std::string excludeSubExpression = assembleSubExpression( + m_excludedCategories, Categories::combinationModeAsSymbol(m_excludeMode)); + + if (includeSubExpression.empty()) + { + if (m_combinationMode == Set::CombinationMode::And) + { + // Since there are no include categories and the combination mode is And + // the result is that we reject all + this->setAllReject(); + return true; + } + if (excludeSubExpression.empty()) + { + // In this case we have an empty exclude set and the combination mode is Or + // the result is that we pass all + this->setAllPass(); + return true; + } + // In this case the expression is just the complement of the exclusion sub-expression + m_expression = "!"; + m_expression.append(excludeSubExpression); + } + else if (excludeSubExpression.empty()) + { + if (m_combinationMode == Set::CombinationMode::Or) + { + // In this case we have an empty exclude set and the combination mode is Or + // the result is that we pass all + this->setAllPass(); + return true; + } + // In this case the expression is just the inclusion sub-expression + m_expression = includeSubExpression; + } + else + { + // Combine the 2 sub expressions + m_expression = includeSubExpression; + m_expression.append(" ") + .append(Categories::combinationModeAsSymbol(m_combinationMode)) + .append(" !") + .append(excludeSubExpression); + } + } + // Ok we have a non-empty expression string to evaluate + tao::pegtl::string_input<> in(m_expression, "(expression grammar)"); + smtk::attribute::categories::Evaluators evals; + try + { + tao::pegtl:: + parse( + in, evals); + } + catch (tao::pegtl::parse_error& err) + { + const auto p = err.positions.front(); +#if TAO_PEGTL_VERSION_MAJOR <= 2 && TAO_PEGTL_VERSION_MINOR <= 7 + smtkErrorMacro( + smtk::io::Logger::instance(), + "smtk::attribute::Categories::Expression: " << err.what() << "\n" + << in.line_as_string(p) << "\n" + << std::string(p.byte_in_line, ' ') << "^\n"); +#else + smtkErrorMacro( + smtk::io::Logger::instance(), + "smtk::attribute::Categories::Expression: " << err.what() << "\n" + << in.line_at(p) << "\n" + << std::string(p.byte_in_line, ' ') << "^\n"); +#endif + return false; + } + + if (evals.isValid()) + { + m_evaluator = evals.top(); + m_categoryNames = evals.categoryNames(); + m_isSet = true; + return true; + } + return false; +} + +bool Categories::Expression::setExpression(const std::string& expString) +{ + // Save the old expression in case it needs to be restored. + std::string originalExpression = m_expression; + m_expression = expString; + + if (!this->buildEvaluator()) + { + // There was a problem with the new expression - restore the original + m_expression = originalExpression; + return false; + } + // Clear the sets + m_includedCategories = {}; + m_excludedCategories = {}; + return true; +} + +void Categories::Expression::setAllPass() +{ + m_expression = ""; + + // Clear the sets + m_includedCategories = {}; + m_excludedCategories = {}; + + m_allPass = true; + m_isSet = true; + m_evaluator = [this](const std::set&) { return m_allPass; }; +} + +void Categories::Expression::setAllReject() +{ + m_expression = ""; + + // Clear the sets + m_includedCategories = {}; + m_excludedCategories = {}; + + m_allPass = false; + m_isSet = true; + m_evaluator = [this](const std::set&) { return m_allPass; }; +} + +bool Categories::Expression::passes(const std::set& cats) const +{ + return m_evaluator(cats); +} + +bool Categories::Expression::passes(const std::string& cat) const +{ + std::set categories; + categories.insert(cat); + return this->passes(categories); +} + +///\brief Compares with other set - returns -1 if this < rhs, 0 if they are equal, and 1 if this > rhs +int Categories::Expression::compare(const Expression& rhs) const +{ + if (m_expression != rhs.m_expression) + { + return (m_expression < rhs.m_expression) ? -1 : 1; + } + if (m_allPass != rhs.m_allPass) + { + return (m_allPass < rhs.m_allPass) ? -1 : 1; + } + return Categories::Set::compare(rhs); +} + +void Categories::Expression::updatedSetInfo() +{ + // If both sets are empty there is nothing to update yet + if (m_includedCategories.empty() && m_excludedCategories.empty()) + { + return; + } + + // Save the old expression in case it needs to be restored. + std::string originalExpression = m_expression; + m_expression = ""; + + if (!this->buildEvaluator()) + { + m_expression = originalExpression; + } +} + +std::string Categories::Expression::convertToString(const std::string& prefix) const +{ + std::stringstream ss; + ss << prefix; + if (m_isSet) + { + if (m_expression.empty()) + { + if (m_allPass) + { + ss << "All_Pass"; + } + else + { + ss << "All_Reject"; + } + } + else + { + ss << m_expression; + } + } + else + { + ss << "Not_Set"; + } + ss << std::endl; + return ss.str(); +} + bool Categories::Set::setCombinationMode(const Set::CombinationMode& newMode) { if (newMode != Set::CombinationMode::LocalOnly) { m_combinationMode = newMode; + this->updatedSetInfo(); return true; } return false; } +void Categories::Set::setInclusionMode(const Set::CombinationMode& newMode) +{ + if (m_includeMode == newMode) + { + return; + } + m_includeMode = newMode; + this->updatedSetInfo(); +} + +void Categories::Set::setExclusionMode(const Set::CombinationMode& newMode) +{ + if (m_excludeMode == newMode) + { + return; + } + m_excludeMode = newMode; + this->updatedSetInfo(); +} + bool Categories::Set::passes(const std::string& category) const { std::set categories; @@ -121,7 +394,7 @@ std::string Categories::Set::convertToString(const std::string& prefix) const return ss.str(); } -bool Categories::Stack::append(CombinationMode mode, const Set& categorySet) +bool Categories::Stack::append(CombinationMode mode, const Expression& exp) { // if the mode is LocalOnly - clear the stack if (mode == CombinationMode::LocalOnly) @@ -129,8 +402,8 @@ bool Categories::Stack::append(CombinationMode mode, const Set& categorySet) m_stack.clear(); } - // check to see if the category set represents nothing - if it is don't add it - if (categorySet.empty()) + // check to see if the category expression has not been set - if so ignore it + if (!exp.isSet()) { // If the mode was not LocalOnly then return false since this result in a nop return (mode == CombinationMode::LocalOnly); @@ -140,12 +413,12 @@ bool Categories::Stack::append(CombinationMode mode, const Set& categorySet) // of the stack for testing categories if (m_stack.empty()) { - std::pair p(CombinationMode::LocalOnly, categorySet); + std::pair p(CombinationMode::LocalOnly, exp); m_stack.push_back(p); return true; } // Else append the mode/categorySet - std::pair newPair(mode, categorySet); + std::pair newPair(mode, exp); m_stack.push_back(newPair); return true; } @@ -184,7 +457,7 @@ std::string Categories::Stack::convertToString(const std::string& prefix) const for (auto it = m_stack.cbegin(); it != m_stack.cend(); it++) { ss << prefix << Categories::combinationModeAsString(it->first) << "\n"; - ss << prefix << it->second.convertToString(); + ss << prefix << it->second.expression(); } return ss.str(); } @@ -194,10 +467,7 @@ std::set Categories::Stack::categoryNames() const std::set result; for (auto it = m_stack.cbegin(); it != m_stack.cend(); it++) { - result.insert( - it->second.includedCategoryNames().begin(), it->second.includedCategoryNames().end()); - result.insert( - it->second.excludedCategoryNames().begin(), it->second.excludedCategoryNames().end()); + result.insert(it->second.categoryNames().begin(), it->second.categoryNames().end()); } return result; } @@ -237,6 +507,19 @@ std::string Categories::combinationModeAsString(const CombinationMode mode) return "LocalOnly"; } +std::string Categories::combinationModeAsSymbol(const CombinationMode mode) +{ + if (mode == CombinationMode::And) + { + return "&"; + } + if (mode == CombinationMode::Or) + { + return "|"; + } + return ""; +} + bool Categories::combinationModeFromString(const std::string& val, CombinationMode& mode) { if ((val == "And") || (val == "All")) @@ -257,6 +540,21 @@ bool Categories::combinationModeFromString(const std::string& val, CombinationMo return false; } +bool Categories::combinationModeStringToSymbol(const std::string& val, std::string& symbol) +{ + if ((val == "And") || (val == "All")) + { + symbol = "&"; + return true; + } + if ((val == "Or") || (val == "Any")) + { + symbol = "|"; + return true; + } + return false; +} + bool Categories::insert(const Stack& stack) { // if the stack is not empty, add it diff --git a/smtk/attribute/Categories.h b/smtk/attribute/Categories.h index 201ba54db..9c164ee80 100644 --- a/smtk/attribute/Categories.h +++ b/smtk/attribute/Categories.h @@ -14,6 +14,7 @@ #include "smtk/CoreExports.h" #include "smtk/SystemConfig.h" // quiet dll-interface warnings on windows +#include "smtk/attribute/categories/Evaluators.h" #include #include #include @@ -70,12 +71,12 @@ class SMTKCORE_EXPORT Categories ///@{ ///\brief Set/Get the CombinationMode associated with the included categories. Set::CombinationMode inclusionMode() const { return m_includeMode; } - void setInclusionMode(const Set::CombinationMode& newMode) { m_includeMode = newMode; } + void setInclusionMode(const Set::CombinationMode& newMode); ///@} ///@{ ///\brief Set/Get the CombinationMode associated with the excluded categories. Set::CombinationMode exclusionMode() const { return m_excludeMode; } - void setExclusionMode(const Set::CombinationMode& newMode) { m_excludeMode = newMode; } + void setExclusionMode(const Set::CombinationMode& newMode); ///@} ///\brief Return the set of category names associated with the inclusion set. const std::set& includedCategoryNames() const { return m_includedCategories; } @@ -84,6 +85,7 @@ class SMTKCORE_EXPORT Categories { m_includeMode = mode; m_includedCategories = values; + this->updatedSetInfo(); } ///\brief Return the set of category names associated with the exclusion set. const std::set& excludedCategoryNames() const { return m_excludedCategories; } @@ -92,17 +94,34 @@ class SMTKCORE_EXPORT Categories { m_excludeMode = mode; m_excludedCategories = values; + this->updatedSetInfo(); } ///\brief Add a category name to the inclusion set. - void insertInclusion(const std::string& val) { m_includedCategories.insert(val); } + void insertInclusion(const std::string& val) + { + m_includedCategories.insert(val); + this->updatedSetInfo(); + } ///\brief Add a category name to the exclusion set. - void insertExclusion(const std::string& val) { m_excludedCategories.insert(val); } + void insertExclusion(const std::string& val) + { + m_excludedCategories.insert(val); + this->updatedSetInfo(); + } ///\brief Remove a category name from the inclusion set. - void eraseInclusion(const std::string& val) { m_includedCategories.erase(val); } + void eraseInclusion(const std::string& val) + { + m_includedCategories.erase(val); + this->updatedSetInfo(); + } ///\brief Remove a category name from the exclusion set. - void eraseExclusion(const std::string& val) { m_excludedCategories.erase(val); } + void eraseExclusion(const std::string& val) + { + m_excludedCategories.erase(val); + this->updatedSetInfo(); + } ///\brief Remove all names from both inclusion and exclusion sets. - void reset() + virtual void reset() { m_includedCategories.clear(); m_excludedCategories.clear(); @@ -125,8 +144,8 @@ class SMTKCORE_EXPORT Categories /// If the input set is empty then the method will return true. Else if /// the instance's mode is Or then at least one of its category names must be in the /// input set. If the mode is And then all of the instance's names must be in the input set. - bool passes(const std::set& cats) const; - bool passes(const std::string& cat) const; + virtual bool passes(const std::set& cats) const; + virtual bool passes(const std::string& cat) const; ///@} static bool passesCheck( const std::set& cats, @@ -134,19 +153,79 @@ class SMTKCORE_EXPORT Categories Set::CombinationMode comboMode); ///\brief Compares with other set - returns -1 if this < rhs, 0 if they are equal, and 1 if this > rhs - int compare(const Set& rhs) const; - std::string convertToString(const std::string& prefix = "") const; + virtual int compare(const Set& rhs) const; + virtual std::string convertToString(const std::string& prefix = "") const; - private: + protected: + virtual void updatedSetInfo(){}; Set::CombinationMode m_includeMode{ Set::CombinationMode::Or }, m_excludeMode{ Set::CombinationMode::Or }, m_combinationMode{ Set::CombinationMode::And }; std::set m_includedCategories, m_excludedCategories; }; + class SMTKCORE_EXPORT Expression : public Set + { + public: + Expression(); + ///@{ + ///\brief Set/Get the expression string. + /// + /// Setting the expression will return true if the string represented a valid expression and will construct an + /// appropriate evaluator. If not, no changes are made and false is returned. + bool setExpression(const std::string& expString); + const std::string& expression() const; + ///@} + + ///\brief Set the expression to pass all sets of categories + void setAllPass(); + ///\brief Set the expression to reject any set of categories + void setAllReject(); + ///\brief Indicates that the expression is set to pass all sets of categories + bool allPass() const { return (m_expression.empty() ? m_allPass : false); } + ///\brief Indicates that the expression is set to reject all sets of categories + bool allReject() const { return (m_expression.empty() ? !m_allPass : false); } + + ///\brief Resets the expression to its unset condition and sets the default + /// evaluator to reject all. + void reset() override + { + this->setAllReject(); + m_isSet = false; + } + + ///\brief Indicates that the expression has been set + bool isSet() const { return m_isSet; } + ///@{ + ///\brief Return true if the input set of categories satisfies the Set's + /// constraints. + /// + /// If the input set is empty then the method will return true. Else if + /// the instance's mode is Or then at least one of its category names must be in the + /// input set. If the mode is And then all of the instance's names must be in the input set. + bool passes(const std::set& cats) const override; + bool passes(const std::string& cat) const override; + ///@} + ///\brief Compares with other set - returns -1 if this < rhs, 0 if they are equal, and 1 if this > rhs + int compare(const Expression& rhs) const; + + std::string convertToString(const std::string& prefix = "") const override; + + const std::set& categoryNames() const { return m_categoryNames; } + + protected: + void updatedSetInfo() override; + bool buildEvaluator(); + std::string m_expression; + categories::Evaluators::Eval m_evaluator; + bool m_allPass = false; + bool m_isSet = false; + std::set m_categoryNames; + }; + class SMTKCORE_EXPORT Stack { public: - bool append(CombinationMode mode, const Set& categorySet); + bool append(CombinationMode mode, const Expression& exp); void clear() { m_stack.clear(); } bool passes(const std::set& cats) const; bool passes(const std::string& cat) const; @@ -157,7 +236,7 @@ class SMTKCORE_EXPORT Categories bool operator<(const Stack& rhs) const; protected: - std::vector> m_stack; + std::vector> m_stack; }; Categories() = default; @@ -176,7 +255,6 @@ class SMTKCORE_EXPORT Categories void reset() { m_stacks.clear(); } ///\brief Return the number of stacks in this instance. std::size_t size() const { return m_stacks.size(); } - ///\brief Return the sets contained in this instance. ///\brief Return the stacks contained in this instance. const std::set& stacks() const { return m_stacks; } ///\brief Return a set of all category names referenced in all of the instance's stacks. @@ -186,7 +264,9 @@ class SMTKCORE_EXPORT Categories ///\brief Print to cerr the current contents of the instance. void print() const; static std::string combinationModeAsString(Set::CombinationMode mode); + static std::string combinationModeAsSymbol(Set::CombinationMode mode); static bool combinationModeFromString(const std::string& val, Set::CombinationMode& mode); + static bool combinationModeStringToSymbol(const std::string& val, std::string& symbol); private: std::set m_stacks; diff --git a/smtk/attribute/Definition.h b/smtk/attribute/Definition.h index bb6668da9..7983fa778 100644 --- a/smtk/attribute/Definition.h +++ b/smtk/attribute/Definition.h @@ -143,17 +143,20 @@ class SMTKCORE_EXPORT Definition : public smtk::enable_shared_from_thisapplyCategories(initialCats); def->applyAdvanceLevels(0, 0); } + // Now all of the definitions have been processed we need to combine all // of their categories to form the Resource's categories m_categories.clear(); @@ -1434,7 +1435,7 @@ void Resource::setActiveCategories(const std::set& cats) m_activeCategories = cats; } -bool Resource::passActiveCategoryCheck(const smtk::attribute::Categories::Set& cats) const +bool Resource::passActiveCategoryCheck(const smtk::attribute::Categories::Expression& cats) const { if (!m_activeCategoriesEnabled) { diff --git a/smtk/attribute/Resource.h b/smtk/attribute/Resource.h index 6fb16b092..456cd82b8 100644 --- a/smtk/attribute/Resource.h +++ b/smtk/attribute/Resource.h @@ -241,7 +241,7 @@ class SMTKCORE_EXPORT Resource const std::set& activeCategories() const { return m_activeCategories; } ///@} - bool passActiveCategoryCheck(const smtk::attribute::Categories::Set& cats) const; + bool passActiveCategoryCheck(const smtk::attribute::Categories::Expression& cats) const; bool passActiveCategoryCheck(const smtk::attribute::Categories& cats) const; void addView(smtk::view::ConfigurationPtr); diff --git a/smtk/attribute/ValueItemDefinition.cxx b/smtk/attribute/ValueItemDefinition.cxx index eb3a41087..9653b36f5 100644 --- a/smtk/attribute/ValueItemDefinition.cxx +++ b/smtk/attribute/ValueItemDefinition.cxx @@ -399,7 +399,7 @@ bool ValueItemDefinition::getEnumIndex(const std::string& enumVal, std::size_t& void ValueItemDefinition::setEnumCategories( const std::string& enumValue, - const smtk::attribute::Categories::Set& cats) + const smtk::attribute::Categories::Expression& exp) { if ( std::find(m_discreteValueEnums.begin(), m_discreteValueEnums.end(), enumValue) == @@ -407,7 +407,7 @@ void ValueItemDefinition::setEnumCategories( { return; // enum not defined } - m_valueToCategoryAssociations[enumValue] = cats; + m_valueToCategoryAssociations[enumValue] = exp; } void ValueItemDefinition::addEnumCategory(const std::string& enumValue, const std::string& cat) @@ -418,13 +418,16 @@ void ValueItemDefinition::addEnumCategory(const std::string& enumValue, const st { return; // enum not defined } - m_valueToCategoryAssociations[enumValue].insertInclusion(cat); + std::string expString("'"); + expString.append(cat).append("'"); + m_valueToCategoryAssociations[enumValue].setExpression(expString); } -const smtk::attribute::Categories::Set& ValueItemDefinition::enumCategories( +const smtk::attribute::Categories::Expression& ValueItemDefinition::enumCategories( const std::string& enumValue) const { - static smtk::attribute::Categories::Set dummy; + static smtk::attribute::Categories::Expression dummy; + dummy.setAllPass(); auto result = m_valueToCategoryAssociations.find(enumValue); if (result == m_valueToCategoryAssociations.end()) { @@ -498,8 +501,8 @@ std::vector ValueItemDefinition::relevantEnums( { for (std::size_t i = 0; i < m_discreteValueEnums.size(); i++) { - const auto& cats = this->enumCategories(m_discreteValueEnums[i]); - if (cats.empty() || cats.passes(testCategories)) + const auto& catExp = this->enumCategories(m_discreteValueEnums[i]); + if (catExp.allPass() || catExp.passes(testCategories)) { if ( (!includeReadAccess) || @@ -542,8 +545,8 @@ bool ValueItemDefinition::isDiscreteIndexValid(int index, const std::setenumCategories(m_discreteValueEnums[index]); - return (cats.empty() || cats.passes(categories)); + const auto& catExp = this->enumCategories(m_discreteValueEnums[index]); + return (catExp.allPass() || catExp.passes(categories)); } bool ValueItemDefinition::isDiscreteIndexValid(int index) const diff --git a/smtk/attribute/ValueItemDefinition.h b/smtk/attribute/ValueItemDefinition.h index 91f5fda14..0dd19352d 100644 --- a/smtk/attribute/ValueItemDefinition.h +++ b/smtk/attribute/ValueItemDefinition.h @@ -139,9 +139,9 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti ///\brief Set/Get the set of categories associated with a value Enum void setEnumCategories( const std::string& enumValue, - const smtk::attribute::Categories::Set& cats); + const smtk::attribute::Categories::Expression& cats); void addEnumCategory(const std::string& enumValue, const std::string& cat); - const smtk::attribute::Categories::Set& enumCategories(const std::string& enumValue) const; + const smtk::attribute::Categories::Expression& enumCategories(const std::string& enumValue) const; /// @} /// @{ @@ -217,7 +217,7 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti std::map m_itemDefs; std::map> m_itemToValueAssociations; std::map> m_valueToItemAssociations; - std::map m_valueToCategoryAssociations; + std::map m_valueToCategoryAssociations; std::map m_valueToAdvanceLevelAssociations; private: diff --git a/smtk/attribute/categories/Actions.h b/smtk/attribute/categories/Actions.h new file mode 100644 index 000000000..238a841ce --- /dev/null +++ b/smtk/attribute/categories/Actions.h @@ -0,0 +1,123 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_categories_Actions_h +#define smtk_attribute_categories_Actions_h + +#include "smtk/attribute/categories/Evaluators.h" +#include "smtk/attribute/categories/Grammar.h" + +#include "smtk/common/TypeName.h" + +// clang-format off + +namespace smtk +{ +namespace attribute +{ +namespace categories +{ + +template +struct Action : nothing +{ +}; + +///\brief Processing the start of a new sub-expression +template<> +struct Action> +{ + template + static void apply(const Input&, smtk::attribute::categories::Evaluators& evals) + { + evals.startSubExpression(); + } +}; + +///\brief Processing the end of the current sub-expression +template<> +struct Action> +{ + template + static void apply(const Input&, smtk::attribute::categories::Evaluators& evals) + { + evals.endSubExpression(); + } +}; + +///\brief Generate lambda for matching a category name defined within single quotes +template<> +struct Action +{ + template + static void apply(const Input& input, smtk::attribute::categories::Evaluators& evals) + { + std::string catName = input.string(); + evals.addCategoryName(catName); + evals.pushEval([catName](const std::set& catNames){ + return (catNames.find(catName) != catNames.end()); + }); + } +}; + +///\brief Generate lambda for matching a category name defined without using quotes +template<> +struct Action +{ + template + static void apply(const Input& input, smtk::attribute::categories::Evaluators& evals) + { + std::string catName = input.string(); + evals.addCategoryName(catName); + evals.pushEval([catName](const std::set& catNames){ + return (catNames.find(catName) != catNames.end()); + }); + } +}; + +///\brief Push a complement symbol onto the operation stack +template<> +struct Action +{ + template + static void apply(const Input&, smtk::attribute::categories::Evaluators& evals) + { + evals.pushOp('!'); + } +}; + +///\brief Push an and symbol onto the operation stack +template<> +struct Action +{ + template + static void apply(const Input&, smtk::attribute::categories::Evaluators& evals) + { + evals.pushOp('&'); + } +}; + +///\brief Push an or symbol onto the operation stack +template<> +struct Action +{ + template + static void apply(const Input&, smtk::attribute::categories::Evaluators& evals) + { + evals.pushOp('|'); + } +}; + +// clang-format on + +} // namespace categories +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/categories/Evaluators.h b/smtk/attribute/categories/Evaluators.h new file mode 100644 index 000000000..68172f7ab --- /dev/null +++ b/smtk/attribute/categories/Evaluators.h @@ -0,0 +1,178 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_categories_Evaluators_h +#define smtk_attribute_categories_Evaluators_h + +#include "smtk/io/Logger.h" + +#include +#include +#include +#include + +namespace smtk +{ +namespace attribute +{ +namespace categories +{ + +class SMTKCORE_EXPORT Evaluators +{ +public: + typedef std::function& catNames)> Eval; + + void pushEval(Eval e) + { + m_evalStack.push(e); + this->processOps(); + } + + void pushOp(const char& op) + { + // Simple optimization - if we are trying to push a complement + // operation onto the stack and there is a complement operation + // on the top - then instead of pushing the second we will remove + // in the stack + if ((op == '!') && (!m_currentOps.empty()) && (m_currentOps.top() == '!')) + { + m_currentOps.pop(); + } + else + { + m_currentOps.push(op); + } + } + + void startSubExpression() + { + m_subExpressionOps.push(m_currentOps); + m_currentOps = {}; + } + + void endSubExpression() + { + if (!m_currentOps.empty()) + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "Category Expression: Ending Sub Expression and Op stack is not empty!\n"); + } + else if (m_subExpressionOps.empty()) + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "Category Expression: Ending Sub Expression and sub Ops stack is empty!\n"); + } + else + { + m_currentOps = m_subExpressionOps.top(); + m_subExpressionOps.pop(); + this->processOps(); + } + } + + void processOps() + { + char op; + while (!m_currentOps.empty()) + { + op = m_currentOps.top(); + m_currentOps.pop(); + switch (op) + { + case '!': + if (m_evalStack.empty()) + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "Category Expression: Can not perform Complement - Eval Stack is empty!\n"); + } + else + { + auto e = m_evalStack.top(); + m_evalStack.pop(); + m_evalStack.push([e](const std::set& catNames) { return !e(catNames); }); + } + break; + case '&': + if (m_evalStack.size() < 2) + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "Category Expression: Can not perform And - Eval Stack contains only " + << m_evalStack.size() << " entries!\n"); + } + else + { + auto a = m_evalStack.top(); + m_evalStack.pop(); + auto b = m_evalStack.top(); + m_evalStack.pop(); + m_evalStack.push([a, b](const std::set& catNames) { + return (a(catNames) && b(catNames)); + }); + } + break; + case '|': + if (m_evalStack.size() < 2) + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "Category Expression: Can not perform Or - Eval Stack contains only " + << m_evalStack.size() << " entries!\n"); + } + else + { + auto a = m_evalStack.top(); + m_evalStack.pop(); + auto b = m_evalStack.top(); + m_evalStack.pop(); + m_evalStack.push([a, b](const std::set& catNames) { + return (a(catNames) || b(catNames)); + }); + } + break; + default: + smtkErrorMacro( + smtk::io::Logger::instance(), "Category Expression: Unsupported Operation: " << op); + } + } + } + + bool isValid() const + { + return (m_evalStack.size() == 1) && m_currentOps.empty() && m_subExpressionOps.empty(); + } + + Eval top() const + { + if (m_evalStack.empty()) + { + return ([](const std::set&) { return false; }); + } + return m_evalStack.top(); + } + + void addCategoryName(const std::string& name) { m_categoryNames.insert(name); } + + const std::set& categoryNames() const { return m_categoryNames; } + +private: + std::stack> m_subExpressionOps; + std::stack m_currentOps; + std::stack m_evalStack; + std::set m_categoryNames; +}; +} // namespace categories +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/categories/Grammar.h b/smtk/attribute/categories/Grammar.h new file mode 100644 index 000000000..70dcc7e88 --- /dev/null +++ b/smtk/attribute/categories/Grammar.h @@ -0,0 +1,101 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_categories_Grammar_h +#define smtk_attribute_categories_Grammar_h + +#include "tao/pegtl.hpp" + +using namespace tao::pegtl; + +// clang-format off + +namespace smtk +{ +namespace attribute +{ +namespace categories +{ + +/// Define a syntax for expressions that occur inside a pair of symbols, and +/// construct named specializations that fit our grammar. +template +struct enclosed : if_must, until, arguments...> > {}; + +template +struct parenthesized : enclosed<'(', ')', arguments...> {}; + +template +struct quoted : enclosed<'\'', '\'', arguments...> {}; + +struct ExpressionSyntax; + +struct BracketSyntax + : tao::pegtl::if_must< one< '(' >, ExpressionSyntax, tao::pegtl::one< ')' > > {}; + +struct NameSyntax : plus> +{ +}; + +struct BareNameSyntax : identifier +{ +}; + +struct SMTKCORE_EXPORT CategoryNameSyntax + : sor< + quoted, BareNameSyntax> +{ +}; + +struct SMTKCORE_EXPORT ComplementOperator + : TAO_PEGTL_ISTRING("!") +{ +}; +struct SMTKCORE_EXPORT ComplementSyntax + : pad, space> +{ +}; + +struct SMTKCORE_EXPORT OperandSyntax + : sor +{ +}; + +struct SMTKCORE_EXPORT AndOperator + : TAO_PEGTL_ISTRING("&") +{ +}; +struct SMTKCORE_EXPORT OrOperator + : TAO_PEGTL_ISTRING("|") +{ +}; +struct SMTKCORE_EXPORT BinaryOperator + : sor +{ +}; + +struct SMTKCORE_EXPORT ExpressionSyntax + : list +{ +}; + +struct SMTKCORE_EXPORT ExpressionGrammar + : must< + ExpressionSyntax, eof> +{ +}; + +// clang-format on + +} // namespace categories +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/json/jsonDefinition.cxx b/smtk/attribute/json/jsonDefinition.cxx index ca39e28df..adba7f13a 100644 --- a/smtk/attribute/json/jsonDefinition.cxx +++ b/smtk/attribute/json/jsonDefinition.cxx @@ -78,30 +78,27 @@ SMTKCORE_EXPORT void to_json(nlohmann::json& j, const smtk::attribute::Definitio { j["Nodal"] = true; } - // Process Category Info - j["CategoryInfo"]["Combination"] = smtk::attribute::Categories::combinationModeAsString( - defPtr->localCategories().combinationMode()); - - // Inclusion Info - if (!defPtr->localCategories().includedCategoryNames().empty()) + // Process Local Category Expression + if (defPtr->localCategories().isSet()) { - j["CategoryInfo"]["InclusionCombination"] = - smtk::attribute::Categories::combinationModeAsString( - defPtr->localCategories().inclusionMode()); - j["CategoryInfo"]["IncludeCategories"] = defPtr->localCategories().includedCategoryNames(); - } - - // Exclusion Info - if (!defPtr->localCategories().excludedCategoryNames().empty()) - { - j["CategoryInfo"]["ExclusionCombination"] = - smtk::attribute::Categories::combinationModeAsString( - defPtr->localCategories().exclusionMode()); - j["CategoryInfo"]["ExcludeCategories"] = defPtr->localCategories().excludedCategoryNames(); + // If the expression string is not empty save it, else we are dealing with + // a trivial All Pass/Reject constraint + if (!defPtr->localCategories().expression().empty()) + { + j["CategoryExpression"]["Expression"] = defPtr->localCategories().expression(); + } + else if (defPtr->localCategories().allPass()) + { + j["CategoryExpression"]["PassMode"] = "All"; + } + else + { + j["CategoryExpression"]["PassMode"] = "None"; + } } - // Inheritance Option - j["CategoryInfo"]["InheritanceMode"] = + // Inheritance Option - This is always needed + j["CategoryExpression"]["InheritanceMode"] = smtk::attribute::Categories::combinationModeAsString(defPtr->categoryInheritanceMode()); // Save Color Information @@ -250,10 +247,51 @@ SMTKCORE_EXPORT void from_json( // Process Category Info () smtk::attribute::Categories::CombinationMode cmode; - auto catInfo = j.find("CategoryInfo"); // Current Form - auto categories = j.find("Categories"); // Old Deprecated Form + auto catExp = j.find("CategoryExpression"); // Current Form + auto catInfo = j.find("CategoryInfo"); // Old Form since 05/24 + auto categories = j.find("Categories"); // Old Deprecated Form - if (catInfo != j.end()) + if (catExp != j.end()) + { + auto inheritanceMode = catExp->find("InheritanceMode"); + if (inheritanceMode != catExp->end()) + { + if (smtk::attribute::Categories::combinationModeFromString(*inheritanceMode, cmode)) + { + defPtr->setCategoryInheritanceMode(cmode); + } + else + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "When converting json, definition " + << defPtr->label() + << " has an invalid category inheritance mode = " << *inheritanceMode); + } + } + // Is this a trivial reject / pass constraint + auto passMode = catExp->find("PassMode"); + if (passMode != catExp->end()) + { + if (*passMode == "All") + { + defPtr->localCategories().setAllPass(); + } + else if (*passMode == "None") + { + defPtr->localCategories().setAllReject(); + } + } + else + { + auto catExpression = catExp->find("Expression"); + if (catExpression != catExp->end()) + { + defPtr->localCategories().setExpression(*catExpression); + } + } + } + else if (catInfo != j.end()) { auto inheritanceMode = catInfo->find("InheritanceMode"); if (inheritanceMode != catInfo->end()) diff --git a/smtk/attribute/json/jsonItemDefinition.cxx b/smtk/attribute/json/jsonItemDefinition.cxx index 0864fca83..f059aa2fc 100644 --- a/smtk/attribute/json/jsonItemDefinition.cxx +++ b/smtk/attribute/json/jsonItemDefinition.cxx @@ -51,30 +51,28 @@ SMTKCORE_EXPORT void to_json( { j["AdvanceWriteLevel"] = itemDefPtr->localAdvanceLevel(1); } - // Process Category Info - auto& cats = itemDefPtr->localCategories(); - j["CategoryInfo"]["Combination"] = - smtk::attribute::Categories::combinationModeAsString(cats.combinationMode()); - - // Inheritance Option - j["CategoryInfo"]["InheritanceMode"] = - smtk::attribute::Categories::combinationModeAsString(itemDefPtr->categoryInheritanceMode()); - - // Inclusion Info - if (!cats.includedCategoryNames().empty()) + // Process Local Category Expression + if (itemDefPtr->localCategories().isSet()) { - j["CategoryInfo"]["InclusionCombination"] = - smtk::attribute::Categories::combinationModeAsString(cats.inclusionMode()); - j["CategoryInfo"]["IncludeCategories"] = cats.includedCategoryNames(); + // If the expression string is not empty save it, else we are dealing with + // a trivial All Pass/Reject constraint + if (!itemDefPtr->localCategories().expression().empty()) + { + j["CategoryExpression"]["Expression"] = itemDefPtr->localCategories().expression(); + } + else if (itemDefPtr->localCategories().allPass()) + { + j["CategoryExpression"]["PassMode"] = "All"; + } + else + { + j["CategoryExpression"]["PassMode"] = "None"; + } } - // Exclusion Info - if (!cats.excludedCategoryNames().empty()) - { - j["CategoryInfo"]["ExclusionCombination"] = - smtk::attribute::Categories::combinationModeAsString(cats.exclusionMode()); - j["CategoryInfo"]["ExcludeCategories"] = cats.excludedCategoryNames(); - } + // Inheritance Option - this is always needed + j["CategoryExpression"]["InheritanceMode"] = + smtk::attribute::Categories::combinationModeAsString(itemDefPtr->categoryInheritanceMode()); if (!itemDefPtr->briefDescription().empty()) { @@ -159,11 +157,53 @@ SMTKCORE_EXPORT void from_json( itemDefPtr->setDetailedDescription(*result); } - // Process Category Info - auto catInfo = j.find("CategoryInfo"); // Current Form - auto categories = j.find("Categories"); // Old Deprecated Form + // Process Category Information + auto catExp = j.find("CategoryExpression"); // Current Form + auto catInfo = j.find("CategoryInfo"); // Old Form since 05/24 + auto categories = j.find("Categories"); // Old Deprecated Form - if (catInfo != j.end()) + if (catExp != j.end()) + { + smtk::attribute::Categories::CombinationMode cmode; + auto inheritanceMode = catExp->find("InheritanceMode"); + if (inheritanceMode != catExp->end()) + { + if (smtk::attribute::Categories::combinationModeFromString(*inheritanceMode, cmode)) + { + itemDefPtr->setCategoryInheritanceMode(cmode); + } + else + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "When converting json, definition " + << itemDefPtr->label() + << " has an invalid category inheritance mode = " << *inheritanceMode); + } + } + // Is this a trivial reject / pass constraint + auto passMode = catExp->find("PassMode"); + if (passMode != catExp->end()) + { + if (*passMode == "All") + { + itemDefPtr->localCategories().setAllPass(); + } + else if (*passMode == "None") + { + itemDefPtr->localCategories().setAllReject(); + } + } + else + { + auto catExpression = catExp->find("Expression"); + if (catExpression != catExp->end()) + { + itemDefPtr->localCategories().setExpression(*catExpression); + } + } + } + else if (catInfo != j.end()) { smtk::attribute::Categories::CombinationMode cmode; auto inheritanceMode = catInfo->find("InheritanceMode"); diff --git a/smtk/attribute/json/jsonResource.cxx b/smtk/attribute/json/jsonResource.cxx index a1ddf97db..d569f2c52 100644 --- a/smtk/attribute/json/jsonResource.cxx +++ b/smtk/attribute/json/jsonResource.cxx @@ -36,12 +36,12 @@ namespace attribute { using json = nlohmann::json; -/// \brief Provide a way to serialize an attribute::Resource. The current version is 6.0 but +/// \brief Provide a way to serialize an attribute::Resource. The current version is 7.0 but /// but can also read in a resource from version 3.0 format or later. SMTKCORE_EXPORT void to_json(json& j, const smtk::attribute::ResourcePtr& res) { smtk::resource::to_json(j, smtk::static_pointer_cast(res)); - j["version"] = "6.0"; + j["version"] = "7.0"; j["IsPrivate"] = res->isPrivate(); j["NameSeparator"] = res->defaultNameSeparator(); if (res->templateType().id() && !res->templateType().data().empty()) diff --git a/smtk/attribute/json/jsonValueItemDefinition.h b/smtk/attribute/json/jsonValueItemDefinition.h index 49bb2ff69..0991f695c 100644 --- a/smtk/attribute/json/jsonValueItemDefinition.h +++ b/smtk/attribute/json/jsonValueItemDefinition.h @@ -57,9 +57,10 @@ static void processDerivedValueDefToJson(json& j, ItemDefType defPtr) enumName = defPtr->discreteEnum(i); // Check conditional items conditionalItems = defPtr->conditionalItems(enumName); - const smtk::attribute::Categories::Set& categoryValues = defPtr->enumCategories(enumName); + const smtk::attribute::Categories::Expression& categoryValues = + defPtr->enumCategories(enumName); json valueJson, structureJson, resultJson; - if (!conditionalItems.empty() || !categoryValues.empty()) + if (!conditionalItems.empty() || categoryValues.isSet()) { // Structure enums // TODO: Simplifiy the logic here @@ -70,28 +71,21 @@ static void processDerivedValueDefToJson(json& j, ItemDefType defPtr) { structureJson["Items"] = conditionalItems; } - if (!categoryValues.empty()) + if (categoryValues.isSet()) { // Process Category Info - structureJson["CategoryInfo"]["Combination"] = - smtk::attribute::Categories::combinationModeAsString(categoryValues.combinationMode()); - - // Inclusion Info - if (!categoryValues.includedCategoryNames().empty()) + if (!categoryValues.expression().empty()) { - structureJson["CategoryInfo"]["InclusionCombination"] = - smtk::attribute::Categories::combinationModeAsString(categoryValues.inclusionMode()); - structureJson["CategoryInfo"]["IncludeCategories"] = - categoryValues.includedCategoryNames(); + // Process Category Expression + structureJson["CategoryExpression"]["Expression"] = categoryValues.expression(); } - - // Exclusion Info - if (!categoryValues.excludedCategoryNames().empty()) + else if (categoryValues.allPass()) { - structureJson["CategoryInfo"]["ExclusionCombination"] = - smtk::attribute::Categories::combinationModeAsString(categoryValues.exclusionMode()); - structureJson["CategoryInfo"]["ExcludeCategories"] = - categoryValues.excludedCategoryNames(); + structureJson["CategoryExpression"]["PassMode"] = "All"; + } + else + { + structureJson["CategoryExpression"]["PassMode"] = "None"; } } if (defPtr->hasEnumAdvanceLevel(enumName)) @@ -186,8 +180,9 @@ static void processDerivedValueDefFromJson( std::cerr << "Item Definition: " << defPtr->name() << " is missing Enum!\n"; continue; } - auto itemsValue = structure->find("Items"); // list of conditional Items - auto catInfo = structure->find("CategoryInfo"); // Current Form + auto itemsValue = structure->find("Items"); // list of conditional Items + auto catExp = structure->find("CategoryExpression"); // Current Form + auto catInfo = structure->find("CategoryInfo"); // Old Form since 05/24 auto catsValue = structure->find("Categories"); // list of categories for enum - Deprecated // Should just iterate once for (auto currentEnum = enumValue->begin(); currentEnum != enumValue->end(); currentEnum++) @@ -203,9 +198,40 @@ static void processDerivedValueDefFromJson( defPtr->addConditionalItem(discreteEnum, currentItem); } } - if (catInfo != structure->end()) + if (catExp != structure->end()) + { + attribute::Categories::Expression localExp; + auto catExpression = catExp->find("Expression"); + auto catPassMode = catExp->find("PassMode"); + if (catExpression != catExp->end()) + { + localExp.setExpression(*catExpression); + defPtr->setEnumCategories(discreteEnum, localExp); + } + else if (catPassMode != catExp->end()) + { + if (*catPassMode == "None") + { + localExp.setAllReject(); + defPtr->setEnumCategories(discreteEnum, localExp); + } + else if (*catPassMode == "All") + { + localExp.setAllPass(); + defPtr->setEnumCategories(discreteEnum, localExp); + } + else + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "When converting json, Category Expression PassMode value: " + << *catPassMode << " is unsupported."); + } + } + } + else if (catInfo != structure->end()) { - attribute::Categories::Set localCats; + attribute::Categories::Expression localCats; auto combineMode = catInfo->find("Combination"); smtk::attribute::Categories::CombinationMode cmode; // If Combination is not specified - assume the default value; @@ -278,7 +304,7 @@ static void processDerivedValueDefFromJson( } else if (catsValue != structure->end()) // Old Deprecated Format { - smtk::attribute::Categories::Set localCats; + smtk::attribute::Categories::Expression localCats; auto ccm = structure->find("categoryCheckMode"); smtk::attribute::Categories::CombinationMode mode = smtk::attribute::Categories::CombinationMode::Or; diff --git a/smtk/attribute/operators/Read.cxx b/smtk/attribute/operators/Read.cxx index 06bb087bb..075cc1c26 100644 --- a/smtk/attribute/operators/Read.cxx +++ b/smtk/attribute/operators/Read.cxx @@ -80,9 +80,9 @@ Read::Result Read::operateInternal() VersionNumber version; try { - // Is this a supported version? Should be 3.0, 4.0, 5.0, or 6.0. + // Is this a supported version? Should be 3.0, 4.0, 5.0, 6.0, or 7.0. version = j.at("version"); - if (version >= VersionNumber(3) && version < VersionNumber(7)) + if (version >= VersionNumber(3) && version < VersionNumber(8)) { supportedVersion = true; } diff --git a/smtk/attribute/pybind11/PybindCategories.h b/smtk/attribute/pybind11/PybindCategories.h index b136d05fe..8341fb87d 100644 --- a/smtk/attribute/pybind11/PybindCategories.h +++ b/smtk/attribute/pybind11/PybindCategories.h @@ -61,7 +61,18 @@ inline py::class_< smtk::attribute::Categories > pybind11_init_smtk_attribute_Ca .def("passes", (bool (smtk::attribute::Categories::Set::*)(const ::std::set<::std::string>&) const) &smtk::attribute::Categories::Set::passes, py::arg("categories")) .def("passes", (bool (smtk::attribute::Categories::Set::*)(const ::std::string&) const) &smtk::attribute::Categories::Set::passes, py::arg("category")) ; - py::class_< smtk::attribute::Categories::Stack >(instance, "Stack") + py::class_< smtk::attribute::Categories::Expression, smtk::attribute::Categories::Set >(instance, "Expression") + .def(py::init<>()) + .def("deepcopy", (smtk::attribute::Categories::Expression & (smtk::attribute::Categories::Expression::*)(::smtk::attribute::Categories::Expression const &)) &smtk::attribute::Categories::Expression::operator=) + + .def("setExpression", &smtk::attribute::Categories::Expression::setExpression) + .def("expression", &smtk::attribute::Categories::Expression::expression) + .def("setAllPass", &smtk::attribute::Categories::Expression::setAllPass) + .def("allPass", &smtk::attribute::Categories::Expression::allPass) + .def("setAllReject", &smtk::attribute::Categories::Expression::setAllReject) + .def("allReject", &smtk::attribute::Categories::Expression::allReject) + ; + py::class_< smtk::attribute::Categories::Stack >(instance, "Stack") .def(py::init<>()) .def("deepcopy", (smtk::attribute::Categories::Stack & (smtk::attribute::Categories::Stack::*)(::smtk::attribute::Categories::Stack const &)) &smtk::attribute::Categories::Stack::operator=) diff --git a/smtk/attribute/pybind11/PybindDefinition.h b/smtk/attribute/pybind11/PybindDefinition.h index b864d7fac..890c08e91 100644 --- a/smtk/attribute/pybind11/PybindDefinition.h +++ b/smtk/attribute/pybind11/PybindDefinition.h @@ -46,8 +46,8 @@ inline PySharedPtrClass< smtk::attribute::Definition > pybind11_init_smtk_attrib .def("categoryInheritanceMode", &smtk::attribute::Definition::categoryInheritanceMode) .def("setCategoryInheritanceMode", &smtk::attribute::Definition::setCategoryInheritanceMode, py::arg("categoryInheritanceModeValue")) .def("categories", &smtk::attribute::Definition::categories) - .def("localCategories", (smtk::attribute::Categories::Set& (smtk::attribute::Definition::*)()) &smtk::attribute::Definition::localCategories) - .def("setLocalCategories", &smtk::attribute::Definition::setLocalCategories, py::arg("catSet")) + .def("localCategories", (smtk::attribute::Categories::Expression& (smtk::attribute::Definition::*)()) &smtk::attribute::Definition::localCategories) + .def("setLocalCategories", &smtk::attribute::Definition::setLocalCategories, py::arg("catExpression")) .def("advanceLevel", &smtk::attribute::Definition::advanceLevel, py::arg("mode") = 0) .def("setLocalAdvanceLevel", (void (smtk::attribute::Definition::*)(int, unsigned int)) &smtk::attribute::Definition::setLocalAdvanceLevel, py::arg("mode"), py::arg("level")) .def("setLocalAdvanceLevel", (void (smtk::attribute::Definition::*)(unsigned int)) &smtk::attribute::Definition::setLocalAdvanceLevel, py::arg("level")) diff --git a/smtk/attribute/pybind11/PybindItemDefinition.h b/smtk/attribute/pybind11/PybindItemDefinition.h index a4e8cbe7f..2d664d759 100644 --- a/smtk/attribute/pybind11/PybindItemDefinition.h +++ b/smtk/attribute/pybind11/PybindItemDefinition.h @@ -33,8 +33,8 @@ inline PySharedPtrClass< smtk::attribute::ItemDefinition > pybind11_init_smtk_at // NOTE that the Python form of this method is returning a copy since Python // doesn't support const references - oly non-const method of localCategories supported .def("categories", &smtk::attribute::ItemDefinition::categories) - .def("localCategories", (smtk::attribute::Categories::Set& (smtk::attribute::ItemDefinition::*)()) &smtk::attribute::ItemDefinition::localCategories, py::return_value_policy::reference) - .def("setLocalCategories", &smtk::attribute::ItemDefinition::setLocalCategories, py::arg("catSet")) + .def("localCategories", (smtk::attribute::Categories::Expression& (smtk::attribute::ItemDefinition::*)()) &smtk::attribute::ItemDefinition::localCategories, py::return_value_policy::reference) + .def("setLocalCategories", &smtk::attribute::ItemDefinition::setLocalCategories, py::arg("catExpression")) .def("createCopy", &smtk::attribute::ItemDefinition::createCopy, py::arg("info")) .def("detailedDescription", &smtk::attribute::ItemDefinition::detailedDescription) .def("isEnabledByDefault", &smtk::attribute::ItemDefinition::isEnabledByDefault) diff --git a/smtk/attribute/pybind11/PybindResource.h b/smtk/attribute/pybind11/PybindResource.h index cd64f4a2c..0d968a37d 100644 --- a/smtk/attribute/pybind11/PybindResource.h +++ b/smtk/attribute/pybind11/PybindResource.h @@ -82,7 +82,7 @@ inline PySharedPtrClass< smtk::attribute::Resource> pybind11_init_smtk_attribute .def("isRoleUnique", &smtk::attribute::Resource::isRoleUnique) .def("numberOfAdvanceLevels", &smtk::attribute::Resource::numberOfAdvanceLevels) .def("numberOfCategories", &smtk::attribute::Resource::numberOfCategories) - .def("passActiveCategoryCheck", (bool (smtk::attribute::Resource::*) (const smtk::attribute::Categories::Set& cats) const) &smtk::attribute::Resource::passActiveCategoryCheck, py::arg("categorySet")) + .def("passActiveCategoryCheck", (bool (smtk::attribute::Resource::*) (const smtk::attribute::Categories::Expression& cats) const) &smtk::attribute::Resource::passActiveCategoryCheck, py::arg("categoryExpression")) .def("passActiveCategoryCheck", (bool (smtk::attribute::Resource::*) (const smtk::attribute::Categories& cats) const) &smtk::attribute::Resource::passActiveCategoryCheck, py::arg("categories")) .def("removeAttribute", &smtk::attribute::Resource::removeAttribute, py::arg("att")) .def("rename", &smtk::attribute::Resource::rename, py::arg("att"), py::arg("newName")) diff --git a/smtk/attribute/testing/cxx/categoryTest.cxx b/smtk/attribute/testing/cxx/categoryTest.cxx index 8a46320c7..958a1df63 100644 --- a/smtk/attribute/testing/cxx/categoryTest.cxx +++ b/smtk/attribute/testing/cxx/categoryTest.cxx @@ -27,6 +27,7 @@ int main() { int status = 0; { + smtk::attribute::ResourcePtr resptr = smtk::attribute::Resource::create(); smtk::attribute::Resource& resource(*resptr.get()); std::cout << "Resource Created\n"; diff --git a/smtk/attribute/testing/cxx/unitCategories.cxx b/smtk/attribute/testing/cxx/unitCategories.cxx index 8cc89e413..4c40ca526 100644 --- a/smtk/attribute/testing/cxx/unitCategories.cxx +++ b/smtk/attribute/testing/cxx/unitCategories.cxx @@ -116,7 +116,7 @@ bool testCategories( std::string e = videf->discreteEnum(i); std::cerr << prefix << " Testing Item Definition:" << idef->name() << " enum = " << e << "'s Categories: "; - if (!compareSets(videf->enumCategories(e).includedCategoryNames(), enumCats[i])) + if (!compareSets(videf->enumCategories(e).categoryNames(), enumCats[i])) { status = false; } diff --git a/smtk/attribute/testing/cxx/unitExclusionCategories.cxx b/smtk/attribute/testing/cxx/unitExclusionCategories.cxx index cf0986123..37e718807 100644 --- a/smtk/attribute/testing/cxx/unitExclusionCategories.cxx +++ b/smtk/attribute/testing/cxx/unitExclusionCategories.cxx @@ -81,7 +81,7 @@ void setupAttributeResource(attribute::ResourcePtr& attRes) // 7: Or Or Or DefinitionPtr A = attRes->createDefinition("A"); - Categories::Set testCats; + Categories::Expression testCats; testCats.insertInclusion("a"); testCats.insertInclusion("b"); testCats.insertExclusion("c"); diff --git a/smtk/attribute/testing/cxx/unitPassCategories.cxx b/smtk/attribute/testing/cxx/unitPassCategories.cxx index 84ad9fc9a..5399e408b 100644 --- a/smtk/attribute/testing/cxx/unitPassCategories.cxx +++ b/smtk/attribute/testing/cxx/unitPassCategories.cxx @@ -172,7 +172,7 @@ bool testResource(const attribute::ResourcePtr& attRes, const std::string& prefi std::cerr << "TestAtt Categories: "; att->categories().print(); - for (int i = 0; i < 4; i++) + for (int i = 0; i < 6; i++) { smtkTest(s[i] != nullptr, "Could not find s" << i << "!"); std::cerr << "s" << i << " Categories: "; @@ -419,6 +419,7 @@ void setupAttributeResource(attribute::ResourcePtr& attRes) sItemDef0->setCategoryInheritanceMode(Categories::CombinationMode::Or); sItemDef0->localCategories().insertInclusion("a"); sItemDef0->localCategories().insertInclusion("b"); + std::cerr << "S0 = " << sItemDef0->localCategories().expression() << std::endl; sItemDef0->setDefaultValue("foo"); StringItemDefinitionPtr sItemDef1 = A->addItemDefinition("s1"); sItemDef1->localCategories().insertInclusion("b"); @@ -432,7 +433,7 @@ void setupAttributeResource(attribute::ResourcePtr& attRes) StringItemDefinitionPtr sItemDef3 = A->addItemDefinition("s3"); sItemDef3->addDiscreteValue("a", "e1"); sItemDef3->addDiscreteValue("b", "e2"); - smtk::attribute::Categories::Set es; + smtk::attribute::Categories::Expression es; es.insertInclusion("a"); es.insertInclusion("b"); es.setInclusionMode(Categories::Set::CombinationMode::And); diff --git a/smtk/io/AttributeReader.cxx b/smtk/io/AttributeReader.cxx index 837096067..57057613b 100644 --- a/smtk/io/AttributeReader.cxx +++ b/smtk/io/AttributeReader.cxx @@ -18,6 +18,7 @@ #include "smtk/io/XmlDocV5Parser.h" #include "smtk/io/XmlDocV6Parser.h" #include "smtk/io/XmlDocV7Parser.h" +#include "smtk/io/XmlDocV8Parser.h" #include "smtk/attribute/Attribute.h" #include "smtk/attribute/Definition.h" @@ -121,6 +122,11 @@ void AttributeReaderInternals::print(smtk::attribute::ResourcePtr resource) // Returns the attribute resource root node in a pugi doc pugi::xml_node AttributeReaderInternals::getRootNode(pugi::xml_document& doc) { + if (XmlDocV8Parser::canParse(doc)) + { + return XmlDocV8Parser::getRootNode(doc); + } + if (XmlDocV7Parser::canParse(doc)) { return XmlDocV7Parser::getRootNode(doc); @@ -287,7 +293,14 @@ void AttributeReaderInternals::parseXml( } // Lets see if any of the parsers can process the node - if (XmlDocV7Parser::canParse(root)) + if (XmlDocV8Parser::canParse(root)) + { + XmlDocV8Parser theReader(resource, logger); + theReader.setIncludeFileIndex(m_currentFileIndex); + theReader.setReportDuplicateDefinitionsAsErrors(reportAsError); + theReader.process(root, m_globalTemplateMap); + } + else if (XmlDocV7Parser::canParse(root)) { XmlDocV7Parser theReader(resource, logger); theReader.setIncludeFileIndex(m_currentFileIndex); diff --git a/smtk/io/AttributeWriter.cxx b/smtk/io/AttributeWriter.cxx index 773e4e006..0704647f3 100644 --- a/smtk/io/AttributeWriter.cxx +++ b/smtk/io/AttributeWriter.cxx @@ -11,12 +11,12 @@ #include "smtk/io/AttributeWriter.h" #include "smtk/io/Logger.h" #include "smtk/io/XmlStringWriter.h" -#include "smtk/io/XmlV7StringWriter.h" +#include "smtk/io/XmlV8StringWriter.h" #include #include -#define DEFAULT_FILE_VERSION 7 -#define MAX_FILE_VERSION 7 +#define DEFAULT_FILE_VERSION 8 +#define MAX_FILE_VERSION 8 //force to use filesystem version 3 #define BOOST_FILESYSTEM_VERSION 3 #include @@ -169,7 +169,8 @@ XmlStringWriter* AttributeWriter::newXmlStringWriter( case 5: case 6: case 7: - writer = new XmlV7StringWriter(resource, logger); + case 8: + writer = new XmlV8StringWriter(resource, logger); break; default: diff --git a/smtk/io/CMakeLists.txt b/smtk/io/CMakeLists.txt index c919c1fdf..32fe42647 100644 --- a/smtk/io/CMakeLists.txt +++ b/smtk/io/CMakeLists.txt @@ -16,12 +16,14 @@ set(ioSrcs XmlDocV5Parser.cxx XmlDocV6Parser.cxx XmlDocV7Parser.cxx + XmlDocV8Parser.cxx XmlV2StringWriter.cxx XmlV3StringWriter.cxx XmlV4StringWriter.cxx XmlV5StringWriter.cxx XmlV6StringWriter.cxx XmlV7StringWriter.cxx + XmlV8StringWriter.cxx ) set(ioHeaders @@ -41,6 +43,7 @@ set(ioHeaders XmlDocV5Parser.h XmlDocV6Parser.h XmlDocV7Parser.h + XmlDocV8Parser.h XmlStringWriter.h XmlV2StringWriter.h XmlV3StringWriter.h @@ -48,6 +51,7 @@ set(ioHeaders XmlV5StringWriter.h XmlV6StringWriter.h XmlV7StringWriter.h + Xml87StringWriter.h ) # mesh-related I/O diff --git a/smtk/io/XmlDocV1Parser.cxx b/smtk/io/XmlDocV1Parser.cxx index 0cf302514..80a18cccc 100644 --- a/smtk/io/XmlDocV1Parser.cxx +++ b/smtk/io/XmlDocV1Parser.cxx @@ -248,11 +248,37 @@ bool processDerivedValueDefChildNode(pugi::xml_node& node, const ItemDefType& id // Does the enum have explicit categories // This is the old format for categories xml_node catNodes = child.child("Categories"); - // This is the current format + // This is the style used prior to 5/2024 xml_node catInfoNode = child.child("CategoryInfo"); - if (catInfoNode) + // This is the latest style + xml_node catExpNode = child.child("CategoryExpression"); + if (catExpNode) + { + Categories::Expression catExp; + // Is this a trivial expression - all pass or none pass? + xatt = catExpNode.attribute("PassMode"); + if (xatt) + { + std::string pmode = xatt.value(); + if (pmode == "All") + { + catExp.setAllPass(); + } + else if (pmode == "None") + { + catExp.setAllReject(); + } + } + else + { + std::string exp = catExpNode.text().get(); + catExp.setExpression(exp); + } + idef->setEnumCategories(v, catExp); + } + else if (catInfoNode) { - Categories::Set cats; + Categories::Expression cats; // Lets get the overall combination mode xatt = catInfoNode.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) @@ -293,7 +319,7 @@ bool processDerivedValueDefChildNode(pugi::xml_node& node, const ItemDefType& id } else if (catNodes) { - Categories::Set cats; + Categories::Expression cats; xatt = catNodes.attribute("CategoryCheckMode"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { @@ -1133,7 +1159,7 @@ void XmlDocV1Parser::processAssociationDef(xml_node& node, DefinitionPtr def) // This is to support specifying category inheritance via XML Attributes void XmlDocV1Parser::processCategoryAtts( xml_node& node, - Categories::Set& catSet, + Categories::Expression& catExp, Categories::CombinationMode& inheritanceMode) { attribute::Categories::Set::CombinationMode catMode; @@ -1149,15 +1175,15 @@ void XmlDocV1Parser::processCategoryAtts( xatt = node.attribute("CategoryCheckMode"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setInclusionMode(catMode); + catExp.setInclusionMode(catMode); } } -void XmlDocV1Parser::processOldStyleCategoryNode(xml_node& node, Categories::Set& catSet) +void XmlDocV1Parser::processOldStyleCategoryNode(xml_node& node, Categories::Expression& catExp) { for (xml_node child = node.first_child(); child; child = child.next_sibling()) { - catSet.insertInclusion(child.text().get()); + catExp.insertInclusion(child.text().get()); } } @@ -1170,7 +1196,7 @@ void XmlDocV1Parser::processItemDefCategoryInfoNode(xml_node& node, ItemDefiniti void XmlDocV1Parser::processCategoryInfoNode( xml_node& node, - Categories::Set& catSet, + Categories::Expression& catExp, Categories::CombinationMode& inheritanceMode) { attribute::Categories::Set::CombinationMode catMode; @@ -1188,7 +1214,7 @@ void XmlDocV1Parser::processCategoryInfoNode( xatt = node.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setCombinationMode(catMode); + catExp.setCombinationMode(catMode); } // Get the Include set (if one exists) xml_node catGroup; @@ -1199,11 +1225,11 @@ void XmlDocV1Parser::processCategoryInfoNode( xatt = catGroup.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setInclusionMode(catMode); + catExp.setInclusionMode(catMode); } for (child = catGroup.first_child(); child; child = child.next_sibling()) { - catSet.insertInclusion(child.text().get()); + catExp.insertInclusion(child.text().get()); } } catGroup = node.child("Exclude"); @@ -1213,11 +1239,11 @@ void XmlDocV1Parser::processCategoryInfoNode( xatt = catGroup.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setExclusionMode(catMode); + catExp.setExclusionMode(catMode); } for (child = catGroup.first_child(); child; child = child.next_sibling()) { - catSet.insertExclusion(child.text().get()); + catExp.insertExclusion(child.text().get()); } } } diff --git a/smtk/io/XmlDocV1Parser.h b/smtk/io/XmlDocV1Parser.h index b1bca8c4a..7b8072868 100644 --- a/smtk/io/XmlDocV1Parser.h +++ b/smtk/io/XmlDocV1Parser.h @@ -104,15 +104,17 @@ class SMTKCORE_EXPORT XmlDocV1Parser virtual void processCategoryAtts( pugi::xml_node& node, - attribute::Categories::Set& catSet, + attribute::Categories::Expression& catExp, attribute::Categories::CombinationMode& inheritanceMode); - void processOldStyleCategoryNode(pugi::xml_node& node, smtk::attribute::Categories::Set& catSet); + void processOldStyleCategoryNode( + pugi::xml_node& node, + smtk::attribute::Categories::Expression& catExp); void processItemDefCategoryInfoNode( pugi::xml_node& node, smtk::attribute::ItemDefinitionPtr idef); virtual void processCategoryInfoNode( pugi::xml_node& node, - attribute::Categories::Set& catSet, + attribute::Categories::Expression& catExp, attribute::Categories::CombinationMode& inheritanceMode); virtual void processDefinitionInformation(pugi::xml_node& defNode); diff --git a/smtk/io/XmlDocV6Parser.cxx b/smtk/io/XmlDocV6Parser.cxx index b826d8a9a..0b0247aa4 100644 --- a/smtk/io/XmlDocV6Parser.cxx +++ b/smtk/io/XmlDocV6Parser.cxx @@ -94,7 +94,7 @@ void XmlDocV6Parser::process( // This is to support specifying category inheritance via XML Attributes void XmlDocV6Parser::processCategoryAtts( xml_node& node, - Categories::Set& catSet, + Categories::Expression& catExp, Categories::CombinationMode& inheritanceMode) { attribute::Categories::Set::CombinationMode catMode; @@ -111,13 +111,13 @@ void XmlDocV6Parser::processCategoryAtts( xatt = node.attribute("CategoryCheckMode"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setInclusionMode(catMode); + catExp.setInclusionMode(catMode); } } void XmlDocV6Parser::processCategoryInfoNode( xml_node& node, - Categories::Set& catSet, + Categories::Expression& catExp, Categories::CombinationMode& inheritanceMode) { attribute::Categories::Set::CombinationMode catMode; @@ -145,7 +145,7 @@ void XmlDocV6Parser::processCategoryInfoNode( xatt = node.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setCombinationMode(catMode); + catExp.setCombinationMode(catMode); } // Get the Include set (if one exists) xml_node catGroup; @@ -156,11 +156,11 @@ void XmlDocV6Parser::processCategoryInfoNode( xatt = catGroup.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setInclusionMode(catMode); + catExp.setInclusionMode(catMode); } for (child = catGroup.first_child(); child; child = child.next_sibling()) { - catSet.insertInclusion(child.text().get()); + catExp.insertInclusion(child.text().get()); } } catGroup = node.child("Exclude"); @@ -170,11 +170,11 @@ void XmlDocV6Parser::processCategoryInfoNode( xatt = catGroup.attribute("Combination"); if (XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) { - catSet.setExclusionMode(catMode); + catExp.setExclusionMode(catMode); } for (child = catGroup.first_child(); child; child = child.next_sibling()) { - catSet.insertExclusion(child.text().get()); + catExp.insertExclusion(child.text().get()); } } } diff --git a/smtk/io/XmlDocV6Parser.h b/smtk/io/XmlDocV6Parser.h index 3f695ee2d..eb9e26c4c 100644 --- a/smtk/io/XmlDocV6Parser.h +++ b/smtk/io/XmlDocV6Parser.h @@ -40,11 +40,11 @@ class SMTKCORE_EXPORT XmlDocV6Parser : public XmlDocV5Parser protected: void processCategoryAtts( pugi::xml_node& node, - attribute::Categories::Set& catSet, + attribute::Categories::Expression& catExp, attribute::Categories::CombinationMode& inheritanceMode) override; void processCategoryInfoNode( pugi::xml_node& node, - attribute::Categories::Set& catSet, + attribute::Categories::Expression& catExp, attribute::Categories::CombinationMode& inheritanceMode) override; }; } // namespace io diff --git a/smtk/io/XmlDocV8Parser.cxx b/smtk/io/XmlDocV8Parser.cxx new file mode 100644 index 000000000..dabc3fb89 --- /dev/null +++ b/smtk/io/XmlDocV8Parser.cxx @@ -0,0 +1,152 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#include "smtk/io/XmlDocV8Parser.h" + +#include "smtk/common/StringUtil.h" + +#define PUGIXML_HEADER_ONLY +// NOLINTNEXTLINE(bugprone-suspicious-include) +#include "pugixml/src/pugixml.cpp" + +using namespace pugi; +using namespace smtk::attribute; +using namespace smtk::io; +using namespace smtk; + +XmlDocV8Parser::XmlDocV8Parser(smtk::attribute::ResourcePtr myResource, smtk::io::Logger& logger) + : XmlDocV7Parser(myResource, logger) +{ +} + +XmlDocV8Parser::~XmlDocV8Parser() = default; + +bool XmlDocV8Parser::canParse(pugi::xml_document& doc) +{ + // Get the attribute resource node + xml_node amnode = doc.child("SMTK_AttributeResource"); + if (amnode.empty()) + { + return false; + } + + pugi::xml_attribute xatt = amnode.attribute("Version"); + if (!xatt) + { + return false; + } + + int versionNum = xatt.as_int(); + return versionNum == 8; +} + +bool XmlDocV8Parser::canParse(pugi::xml_node& node) +{ + // Check the name of the node + std::string name = node.name(); + if (name != "SMTK_AttributeResource") + { + return false; + } + + pugi::xml_attribute xatt = node.attribute("Version"); + if (!xatt) + { + return false; + } + + int versionNum = xatt.as_int(); + return versionNum == 8; +} + +void XmlDocV8Parser::processCategoryExpressionNode( + xml_node& node, + Categories::Expression& catExp, + Categories::CombinationMode& inheritanceMode) +{ + xml_attribute xatt; + Categories::CombinationMode catMode; + // How are we inheriting categories + xatt = node.attribute("InheritanceMode"); + if (xatt && XmlDocV1Parser::getCategoryComboMode(xatt, catMode)) + { + inheritanceMode = catMode; + } + + // Are we dealing with a Pass/Reject All Expression? + xatt = node.attribute("PassMode"); + if (xatt) + { + std::string pval = xatt.value(); + if (pval == "None") + { + catExp.setAllReject(); + } + else if (pval == "All") + { + catExp.setAllPass(); + } + else + { + smtkErrorMacro(m_logger, "Unsupported Category PassMode: " << xatt.value()); + } + } + else + { + std::string exp = node.text().get(); + if (!exp.empty()) + { + if (!catExp.setExpression(exp)) + { + smtkErrorMacro(m_logger, "Invalid Category Expression: " << exp); + } + } + } +} + +void XmlDocV8Parser::processItemDefChildNode(xml_node& node, const ItemDefinitionPtr& idef) +{ + std::string nodeName = node.name(); + + // Are we dealing with Category Expressions + if (nodeName == "CategoryExpression") + { + this->processItemDefCategoryExpressionNode(node, idef); + return; + } + this->XmlDocV7Parser::processItemDefChildNode(node, idef); +} + +void XmlDocV8Parser::processItemDefCategoryExpressionNode(xml_node& node, ItemDefinitionPtr idef) +{ + Categories::CombinationMode inheritanceMode = idef->categoryInheritanceMode(); + this->processCategoryExpressionNode(node, idef->localCategories(), inheritanceMode); + idef->setCategoryInheritanceMode(inheritanceMode); +} + +void XmlDocV8Parser::processDefinitionChildNode(xml_node& node, DefinitionPtr& def) +{ + std::string nodeName = node.name(); + + // Are we dealing with Category Expressions + if (nodeName == "CategoryExpression") + { + this->processDefCategoryExpressionNode(node, def); + return; + } + this->XmlDocV7Parser::processDefinitionChildNode(node, def); +} + +void XmlDocV8Parser::processDefCategoryExpressionNode(xml_node& node, DefinitionPtr& def) +{ + Categories::CombinationMode inheritanceMode = def->categoryInheritanceMode(); + this->processCategoryExpressionNode(node, def->localCategories(), inheritanceMode); + def->setCategoryInheritanceMode(inheritanceMode); +} diff --git a/smtk/io/XmlDocV8Parser.h b/smtk/io/XmlDocV8Parser.h new file mode 100644 index 000000000..3bcd886e2 --- /dev/null +++ b/smtk/io/XmlDocV8Parser.h @@ -0,0 +1,51 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +// .NAME XmlDocV8Parser.h - +// .SECTION Description +// .SECTION See Also + +#ifndef __smtk_io_XmlDocV8Parser_h +#define __smtk_io_XmlDocV8Parser_h + +#include "smtk/io/XmlDocV7Parser.h" + +#include + +namespace smtk +{ +namespace io +{ +class SMTKCORE_EXPORT XmlDocV8Parser : public XmlDocV7Parser +{ +public: + XmlDocV8Parser(smtk::attribute::ResourcePtr resource, smtk::io::Logger& logger); + ~XmlDocV8Parser() override; + + static bool canParse(pugi::xml_node& node); + static bool canParse(pugi::xml_document& doc); + +protected: + void processCategoryExpressionNode( + pugi::xml_node& node, + attribute::Categories::Expression& catExp, + attribute::Categories::CombinationMode& inheritanceMode); + void processItemDefChildNode(pugi::xml_node& node, const smtk::attribute::ItemDefinitionPtr& idef) + override; + void processItemDefCategoryExpressionNode( + pugi::xml_node& node, + smtk::attribute::ItemDefinitionPtr idef); + void processDefinitionChildNode(pugi::xml_node& node, smtk::attribute::DefinitionPtr& def) + override; + void processDefCategoryExpressionNode(pugi::xml_node& node, smtk::attribute::DefinitionPtr& def); +}; +} // namespace io +} // namespace smtk + +#endif /* __smtk_io_XmlDocV8Parser_h */ diff --git a/smtk/io/XmlV2StringWriter.cxx b/smtk/io/XmlV2StringWriter.cxx index 39be305ff..dbc9dbdae 100644 --- a/smtk/io/XmlV2StringWriter.cxx +++ b/smtk/io/XmlV2StringWriter.cxx @@ -115,7 +115,7 @@ void processDerivedValueDef(pugi::xml_node& node, ItemDefType idef) if (idef->isDiscrete()) { xml_node dnodes = node.append_child("DiscreteInfo"); - size_t j, i, nItems, nCats, n = idef->numberOfDiscreteValues(); + size_t j, i, nItems, n = idef->numberOfDiscreteValues(); xml_node dnode, snode, inodes; std::string ename; std::vector citems; @@ -126,11 +126,10 @@ void processDerivedValueDef(pugi::xml_node& node, ItemDefType idef) citems = idef->conditionalItems(ename); nItems = citems.size(); // Lets see if there are any categories - const Categories::Set& cats = idef->enumCategories(ename); - nCats = cats.includedCategoryNames().size() + cats.excludedCategoryNames().size(); + const Categories::Expression& catExp = idef->enumCategories(ename); // Use the Structure Form if the enum has either - // children item or explicit categories associated with it - if (nItems || nCats) + // children item or explicit category expression is associated with it + if (nItems || catExp.isSet()) { snode = dnodes.append_child("Structure"); dnode = snode.append_child("Value"); @@ -149,34 +148,24 @@ void processDerivedValueDef(pugi::xml_node& node, ItemDefType idef) inodes.append_child("Item").text().set(citems[j].c_str()); } } - // Lets write out the enum's category info - if (nCats) + // Lets write out the enum's category expression information + if (catExp.isSet()) { - xml_node catInfoNode = snode.append_child("CategoryInfo"); - xml_node catGroupNode; - catInfoNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(cats.combinationMode()).c_str()); - // Inclusion Categories - if (!cats.includedCategoryNames().empty()) + xml_node catExpNode = snode.append_child("CategoryExpression"); + if (!catExp.expression().empty()) { - catGroupNode = catInfoNode.append_child("Include"); - catGroupNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(cats.inclusionMode()).c_str()); - for (const auto& str : cats.includedCategoryNames()) - { - catGroupNode.append_child("Cat").text().set(str.c_str()); - } + catExpNode.text().set(catExp.expression().c_str()); } - // Exclusion Categories - if (!cats.excludedCategoryNames().empty()) + else { - catGroupNode = catInfoNode.append_child("Exclude"); - catGroupNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(cats.exclusionMode()).c_str()); - for (const auto& str : cats.excludedCategoryNames()) + if (catExp.allPass()) + { + catExpNode.append_attribute("PassMode").set_value("All"); + } + else { - catGroupNode.append_child("Cat").text().set(str.c_str()); + catExpNode.append_attribute("PassMode").set_value("None"); } } } @@ -736,7 +725,7 @@ void XmlV2StringWriter::processItemDefinition(xml_node& node, ItemDefinitionPtr void XmlV2StringWriter::processItemDefinitionAttributes(xml_node& node, ItemDefinitionPtr idef) { - xml_node child, catInfoNode, catGroupNode; + xml_node child, catExpNode, catGroupNode; node.append_attribute("Name").set_value(idef->name().c_str()); if (!idef->label().empty()) { @@ -750,32 +739,25 @@ void XmlV2StringWriter::processItemDefinitionAttributes(xml_node& node, ItemDefi } // Lets write out the category stuff auto& localCats = idef->localCategories(); - catInfoNode = node.append_child("CategoryInfo"); - catInfoNode.append_attribute("InheritanceMode") + catExpNode = node.append_child("CategoryExpression"); + catExpNode.append_attribute("InheritanceMode") .set_value(Categories::combinationModeAsString(idef->categoryInheritanceMode()).c_str()); - catInfoNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(localCats.combinationMode()).c_str()); - - // Inclusion Categories - if (!localCats.includedCategoryNames().empty()) + if (localCats.isSet()) { - catGroupNode = catInfoNode.append_child("Include"); - catGroupNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(localCats.inclusionMode()).c_str()); - for (const auto& str : localCats.includedCategoryNames()) + if (!localCats.expression().empty()) { - catGroupNode.append_child("Cat").text().set(str.c_str()); + catExpNode.text().set(localCats.expression().c_str()); } - } - // Exclusion Categories - if (!localCats.excludedCategoryNames().empty()) - { - catGroupNode = catInfoNode.append_child("Exclude"); - catGroupNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(localCats.exclusionMode()).c_str()); - for (const auto& str : localCats.excludedCategoryNames()) + else { - catGroupNode.append_child("Cat").text().set(str.c_str()); + if (localCats.allPass()) + { + catExpNode.append_attribute("PassMode").set_value("All"); + } + else + { + catExpNode.append_attribute("PassMode").set_value("None"); + } } } diff --git a/smtk/io/XmlV3StringWriter.cxx b/smtk/io/XmlV3StringWriter.cxx index 36abf247e..0bf11d1e5 100644 --- a/smtk/io/XmlV3StringWriter.cxx +++ b/smtk/io/XmlV3StringWriter.cxx @@ -250,34 +250,27 @@ void XmlV3StringWriter::processDefinitionInternal(xml_node& definition, Definiti } } - auto& localCats = def->localCategories(); + auto& localExpression = def->localCategories(); // Lets write out the category stuff - xml_node catGroupNode, catInfoNode = definition.append_child("CategoryInfo"); - catInfoNode.append_attribute("InheritanceMode") + xml_node catGroupNode, catExpNode = definition.append_child("CategoryExpression"); + catExpNode.append_attribute("InheritanceMode") .set_value(Categories::combinationModeAsString(def->categoryInheritanceMode()).c_str()); - catInfoNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(localCats.combinationMode()).c_str()); - - // Inclusion Categories - if (!localCats.includedCategoryNames().empty()) + if (localExpression.isSet()) { - catGroupNode = catInfoNode.append_child("Include"); - catGroupNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(localCats.inclusionMode()).c_str()); - for (const auto& str : localCats.includedCategoryNames()) + if (!localExpression.expression().empty()) { - catGroupNode.append_child("Cat").text().set(str.c_str()); + catExpNode.text().set(localExpression.expression().c_str()); } - } - // Exclusion Categories - if (!localCats.excludedCategoryNames().empty()) - { - catGroupNode = catInfoNode.append_child("Exclude"); - catGroupNode.append_attribute("Combination") - .set_value(Categories::combinationModeAsString(localCats.exclusionMode()).c_str()); - for (const auto& str : localCats.excludedCategoryNames()) + else { - catGroupNode.append_child("Cat").text().set(str.c_str()); + if (localExpression.allPass()) + { + catExpNode.append_attribute("PassMode").set_value("All"); + } + else + { + catExpNode.append_attribute("PassMode").set_value("None"); + } } } diff --git a/smtk/io/XmlV8StringWriter.cxx b/smtk/io/XmlV8StringWriter.cxx new file mode 100644 index 000000000..b8f6670a6 --- /dev/null +++ b/smtk/io/XmlV8StringWriter.cxx @@ -0,0 +1,46 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#include "smtk/io/XmlV8StringWriter.h" + +#define PUGIXML_HEADER_ONLY +// NOLINTNEXTLINE(bugprone-suspicious-include) +#include "pugixml/src/pugixml.cpp" + +using namespace pugi; +using namespace smtk; +using namespace smtk::attribute; + +namespace smtk +{ +namespace io +{ + +XmlV8StringWriter::XmlV8StringWriter( + const attribute::ResourcePtr myResource, + smtk::io::Logger& logger) + : XmlV7StringWriter(myResource, logger) +{ +} + +XmlV8StringWriter::~XmlV8StringWriter() = default; + +std::string XmlV8StringWriter::className() const +{ + return std::string("XmlV8StringWriter"); +} + +unsigned int XmlV8StringWriter::fileVersion() const +{ + return 8; +} + +} // namespace io +} // namespace smtk diff --git a/smtk/io/XmlV8StringWriter.h b/smtk/io/XmlV8StringWriter.h new file mode 100644 index 000000000..c6739b2e2 --- /dev/null +++ b/smtk/io/XmlV8StringWriter.h @@ -0,0 +1,45 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +// .NAME XmlV8StringWriter.h - +// .SECTION Description +// .SECTION See Also + +#ifndef __smtk_io_XmlV8StringWriter_h +#define __smtk_io_XmlV8StringWriter_h +#include "smtk/CoreExports.h" +#include "smtk/PublicPointerDefs.h" +#include "smtk/io/XmlV7StringWriter.h" + +#include "smtk/attribute/Resource.h" + +#include + +namespace smtk +{ +namespace io +{ +class SMTKCORE_EXPORT XmlV8StringWriter : public XmlV7StringWriter +{ +public: + XmlV8StringWriter(smtk::attribute::ResourcePtr resource, smtk::io::Logger& logger); + ~XmlV8StringWriter() override; + +protected: + // Override methods + // Three virtual methods for writing contents + std::string className() const override; + unsigned int fileVersion() const override; + +private: +}; +} // namespace io +} // namespace smtk + +#endif // __smtk_io_XmlV8StringWriter_h From 937102f260004039f16d7985d79ff39e252f5608 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Sat, 27 Apr 2024 09:08:29 -0400 Subject: [PATCH 08/42] smtk/attribute: install the `categories/Grammar.h` header This one is actually in the source tree. --- smtk/attribute/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtk/attribute/CMakeLists.txt b/smtk/attribute/CMakeLists.txt index 2f8d47c7b..44b865b76 100644 --- a/smtk/attribute/CMakeLists.txt +++ b/smtk/attribute/CMakeLists.txt @@ -143,7 +143,7 @@ set(attributeHeaders VoidItemDefinition.h categories/Actions.h categories/Evaluators.h - categories/Syntax.h + categories/Grammar.h filter/Attribute.h filter/Grammar.h update/AttributeUpdateFactory.h From ff1731a897bd5e61666b1b03c2fcf1d58cac70f2 Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Sun, 28 Apr 2024 10:52:20 -0400 Subject: [PATCH 09/42] smtk/io: fix `XmlV8StringWriter.h` filename typo --- smtk/io/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smtk/io/CMakeLists.txt b/smtk/io/CMakeLists.txt index 32fe42647..4233dab3d 100644 --- a/smtk/io/CMakeLists.txt +++ b/smtk/io/CMakeLists.txt @@ -51,7 +51,7 @@ set(ioHeaders XmlV5StringWriter.h XmlV6StringWriter.h XmlV7StringWriter.h - Xml87StringWriter.h + XmlV8StringWriter.h ) # mesh-related I/O From d1a4899777bcf51a19d5ce5762170f5d138c13bd Mon Sep 17 00:00:00 2001 From: Ben Boeckel Date: Sat, 4 May 2024 07:07:54 -0400 Subject: [PATCH 10/42] ci: update superbuild bundle for Xcode update This updates the bundle to agree with the Xcode usage now deployed on machines. --- .gitlab/ci/download_superbuild.cmake | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab/ci/download_superbuild.cmake b/.gitlab/ci/download_superbuild.cmake index f4907d4ed..3dcf53f9a 100644 --- a/.gitlab/ci/download_superbuild.cmake +++ b/.gitlab/ci/download_superbuild.cmake @@ -10,16 +10,16 @@ cmake_minimum_required(VERSION 3.12) set(data_host "https://data.kitware.com") # Determine the tarball to download. ci-smtk-ci-developer-{date}-{git-sha}.tar.gz -# 20231127: Enable Python in bundles +# 20240503: Refresh for new SDK usage (Xcode 14.2) if ("$ENV{CMAKE_CONFIGURATION}" MATCHES "vs2022") - set(file_item "6564b7f7c5a2b36857ad16c4") - set(file_hash "8b3687c140346c6b23629ff5373694cab068b5197be963ccf699b42ec708baaad80aa9d5e2eff26ee36c5eaea72629d212866c735789d1b0509c6f9756ec7a32") + set(file_item "6636158ec6586ba79a5656e3") + set(file_hash "d25862f76bbe4256498396cc5c0526ae74cdb6ed66eaf48bd9444edad4ece01f778d20db364a172abcea8a6b43573ead951052419b123d63ec1730b25e3a4ca7") elseif ("$ENV{CMAKE_CONFIGURATION}" MATCHES "macos_x86_64") - set(file_item "6564b7f7c5a2b36857ad16c2") - set(file_hash "06513bdf597172315a27fa4dce524ea4a8588721b33f5c86833140214e3f94d2e312c80cc2d5efb4b61226d4736bb0860c377722d96f89e4a20a5c91584c1329") + set(file_item "6636158ec6586ba79a5656df") + set(file_hash "811570d6bed9a808fd00c98ed7d435c98408034fcc2bd2705878014a88a66717af566ef549d579052a7d8582a0c7c7393164e4a22ad8abbdb2460d915de4887e") elseif ("$ENV{CMAKE_CONFIGURATION}" MATCHES "macos_arm64") - set(file_item "6564b7f6c5a2b36857ad16c0") - set(file_hash "cb44f9cf11f18c3517828cb76f893e8265ee01a4f1a0ae89e96695c7fa5a930226a422cb9151e7845245584e85585772eca24e029846d459e934717a8b11aace") + set(file_item "6636158ec6586ba79a5656dd") + set(file_hash "cc89f4ab4b71ef1946ed9199b9d6216827cd3c9a92a1520718f448aed3a4f9afb334d516cbe06a32f9d01f1dec8232915b4baa6c42632997e37fdc5565ee9a35") else () message(FATAL_ERROR "Unknown build to use for the superbuild") From 66596af5780f6e7f1b1895cabefbcf52b058110e Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 15 May 2024 11:33:34 -0400 Subject: [PATCH 11/42] ENH: Adding Splitter to Diagram UI This allows the Diagram Drawer to be resizable. --- .../project/testing/xml/WorkletPanel.xml | 6 +++--- smtk/extension/qt/diagram/qtDiagram.cxx | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/smtk/extension/paraview/project/testing/xml/WorkletPanel.xml b/smtk/extension/paraview/project/testing/xml/WorkletPanel.xml index b31beeb73..80efdfd94 100644 --- a/smtk/extension/paraview/project/testing/xml/WorkletPanel.xml +++ b/smtk/extension/paraview/project/testing/xml/WorkletPanel.xml @@ -30,13 +30,13 @@ - + - + - + + + + + Enclosure Radiation + Fluid Flow + Heat Transfer + Induction Heating + Solid Mechanics + + + + + ec2 + + + ec1 + + + Solid Mechanics + + + Fluid Flow + + + Heat Transfer + + + Induction Heating + + + Enclosure Radiation + + + + + + + + + + + + + a + ('ec1') + + + b + ('ec2') + + + c + + + + + + + ('Enclosure Radiation' + 'Induction Heating') + + Text added to top of input file + # Truchas simulation + + + + + + + + + + + + ('Heat Transfer') + + + + + + + ('Fluid Flow') + + + + + + + 0.0 + + + + + ('Fluid Flow') + Integration time step value used for +the first computation cycle + 1.0e-6 + + 0 + + + + ('Fluid Flow') + A factor to multiply the current integration time step +when estimating the next cycle time step. + 1.05 + + 1 + + + + ('Solid Mechanics') + Maximum allowable value for the time step. + 10.0 + + 0 + + + + ('Solid Mechanics') + Minimum allowable value for the time step. + 1.0e-6 + + 0 + + + + + + + + + + ('Induction Heating' ∨ 'Solid Mechanics') + + 0.0 + + 0 + + + + + ('Induction Heating' | 'Solid Mechanics') + + 1.0 + + 0 + + + + ('Heat Transfer') + 1.0 + + 0 + + + + + + + + ('Fluid Flow') + + + + + + + + + + + + + + + This item is superseded by the displacement sequence when +moving-enclosure radiation is enabled + + + ('Heat Transfer') + The list of starting times of each of the phases + + + ('Fluid Flow' | 'Heat Transfer' | 'Solid Mechanics') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/release/notes/categoryExpressions.rst b/doc/release/notes/categoryExpressions.rst index a4d611fb0..2ef474059 100644 --- a/doc/release/notes/categoryExpressions.rst +++ b/doc/release/notes/categoryExpressions.rst @@ -9,9 +9,18 @@ This not only provided greater flexibility but is also easier to define. For ex .. code-block:: xml - (a & !b) & (d | 'category with spaces') + (a * !b) * (d + 'category with spaces') -Note that in XML ``&`` represents ``&``. +You can use the following symbols to represent logical operators: + +* And + * ``∧``, ``*``, ``&`` +* Or + * ``∨``, ``+``, ``|`` +* Complement + * ``¬``, ``~``, ``!`` + +Note that in XML you must use ``&`` to represent ``&`` and that ``∨`` is not the letter v but the Unicode **and** symbol. In this example the expression will match if the test set of categories contains **a** and either **d** or **category with spaces** but not **b**. diff --git a/doc/userguide/attribute/file-syntax.rst b/doc/userguide/attribute/file-syntax.rst index e8e803ca2..3061236ef 100644 --- a/doc/userguide/attribute/file-syntax.rst +++ b/doc/userguide/attribute/file-syntax.rst @@ -935,20 +935,36 @@ The category expression can be specified using one of the following methods: * Specifying the PassMode attribute - this is used to create trivial pass all / reject all expressions * Specifying an expression string which is the contents of the CategoryExpression element -A category expression string is composed of series of category names separated by either a '|' (representing Or) or a '&' (representing And). -You can used '(' and ')' to model sub-expressions. You can also place a '!' before a category name or sub-expression to mean "not". In terms of categories names, -if the name could be consider a valid C++ variable name, you can simply use the name in the expression. If the name does not conform to this format you can enclose it in single quotes. +A category expression string is composed of series of category names separated by either an **Or** or **And** symbol. +You can used '(' and ')' to model sub-expressions. You can also place a compliment symbol before a category name or sub-expression to mean **Negate**. In terms of categories names, +if the name could be consider a valid C++ variable name, you can simply use the name in the expression. If the name does not conform to this format you can enclose it in single quotes. The table below shows the characters used for the logical operation symbols. + +.. list-table:: Supported Symbols for Representing Logical Operations + :widths: 10 40 + :header-rows: 1 + :class: smtk-xml-att-table + + * - Logical Operator + - Supported Symbols + + * - And + - ``&``, ``*``, ``∧`` + * - Or + - ``|``, ``+``, ``∨`` + * - Complement + - ``!``, ``~``, ``¬`` + Here is an example of a CategoryExpression Element. .. code-block:: xml - (a & !b) & (d | 'category with spaces') + (a * !b) * (d + 'category with spaces') -Note that in XML ``&`` represents ``&``. +Note that in XML, you must use ``&`` and not ``&`` and that ``∨`` is not the letter v but the Unicode **and** symbol. In this example the expression will match if the test set of categories contains **a** and either **d** or **category with spaces** but not **b**. @@ -979,6 +995,7 @@ The CategotyInfo Element itself can have the following XML Attributes: (Optional - the default is *And*) +See data/attribute/attribute_collection/ConfigurationTestV8.sbt as an example of how to use Category Expressions. CategoryInfo Format ~~~~~~~~~~~~~~~~~~~ diff --git a/smtk/attribute/categories/Grammar.h b/smtk/attribute/categories/Grammar.h index 70dcc7e88..97905fb7f 100644 --- a/smtk/attribute/categories/Grammar.h +++ b/smtk/attribute/categories/Grammar.h @@ -54,7 +54,7 @@ struct SMTKCORE_EXPORT CategoryNameSyntax }; struct SMTKCORE_EXPORT ComplementOperator - : TAO_PEGTL_ISTRING("!") + : sor { }; struct SMTKCORE_EXPORT ComplementSyntax @@ -69,11 +69,11 @@ struct SMTKCORE_EXPORT OperandSyntax }; struct SMTKCORE_EXPORT AndOperator - : TAO_PEGTL_ISTRING("&") + : sor { }; struct SMTKCORE_EXPORT OrOperator - : TAO_PEGTL_ISTRING("|") + : sor { }; struct SMTKCORE_EXPORT BinaryOperator @@ -82,7 +82,7 @@ struct SMTKCORE_EXPORT BinaryOperator }; struct SMTKCORE_EXPORT ExpressionSyntax - : list + : pad, space> { }; From 4ba3180e517c059ec6aaf4efdf739cc11954958a Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 29 May 2024 15:28:27 -0400 Subject: [PATCH 13/42] ENH: Supporting Vector Properties in XML Attribute Templates Starting in Version 8 XML Template Files, you can now define vector-based Properties on Attribute Resources and Attributes. Currently vector of doubles and vectors of strings are supported but this can be easily extended. Here is an example and is available in the data/attributes/attribute_collections directory as propertiesExample.sbt. .. code-block:: XML 42 3.141 Test string YES the dog a cat 69 3.141 1 10.0 20.0 --- .../propertiesExample.sbt | 10 ++++- doc/release/notes/propertiesEnhancements.rst | 42 +++++++++++++++++++ doc/userguide/attribute/file-syntax.rst | 9 +++- smtk/io/XmlDocV5Parser.cxx | 34 +++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 doc/release/notes/propertiesEnhancements.rst diff --git a/data/attribute/attribute_collection/propertiesExample.sbt b/data/attribute/attribute_collection/propertiesExample.sbt index 29dfa9948..d27ea1e3c 100644 --- a/data/attribute/attribute_collection/propertiesExample.sbt +++ b/data/attribute/attribute_collection/propertiesExample.sbt @@ -1,10 +1,14 @@ - + 42 3.141 Test string YES + + the dog + a cat + @@ -16,6 +20,10 @@ 3.141 1 + + 10.0 + 20.0 + diff --git a/doc/release/notes/propertiesEnhancements.rst b/doc/release/notes/propertiesEnhancements.rst new file mode 100644 index 000000000..bf6dc5ac4 --- /dev/null +++ b/doc/release/notes/propertiesEnhancements.rst @@ -0,0 +1,42 @@ +IO +== + +Supporting Vector Properties in XML Attribute Templates +------------------------------------------------------- + +Starting in Version 8 XML Template Files, you can now define vector-based Properties on Attribute Resources and Attributes. +Currently vector of doubles and vectors of strings are supported but this can be easily extended. + +Here is an example and is available in the data/attributes/attribute_collections directory as propertiesExample.sbt. + +.. code-block:: XML + + + + 42 + 3.141 + Test string + YES + + the dog + a cat + + + + + + + + + 69 + 3.141 + + 1 + + 10.0 + 20.0 + + + + + diff --git a/doc/userguide/attribute/file-syntax.rst b/doc/userguide/attribute/file-syntax.rst index 3061236ef..27c475b26 100644 --- a/doc/userguide/attribute/file-syntax.rst +++ b/doc/userguide/attribute/file-syntax.rst @@ -174,6 +174,10 @@ added to the Attribute Resource. The Property section is defined by a XML 3.141 Test string YES + + the dog + a cat + You can also look at data/attribute/attribute_collection/propertiesExample.rst and smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx for a sample XML file and test. @@ -200,9 +204,12 @@ The values that the **Type** attribute can be set to are: * int for an integer property * double for a double property * string for a string property +* vector[string] for a vector of strings property +* vector[double] for a vector of doubles property * bool for a boolean property -The node's value is the value of the property being set. +The node's value is the value of the property being set with the exception of the vector-based properties. For these, +the node should contain **Value Elements** whose content represents the appropriate values. Supported Values for Boolean Properties ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/smtk/io/XmlDocV5Parser.cxx b/smtk/io/XmlDocV5Parser.cxx index 903e63fd1..a1e9cd5f8 100644 --- a/smtk/io/XmlDocV5Parser.cxx +++ b/smtk/io/XmlDocV5Parser.cxx @@ -22,6 +22,32 @@ using namespace smtk; namespace { + +template +ValueType node_as(const xml_node& node); + +template<> +double node_as(const xml_node& node) +{ + return node.text().as_double(); +} + +template<> +std::string node_as(const xml_node& node) +{ + std::string s = node.text().as_string(); + return common::StringUtil::trim(s); +} + +template +void nodeToData(const xml_node& node, Container& container) +{ + for (const xml_node& child : node.children("Value")) + { + container.insert(container.end(), node_as(child)); + } +} + template void processProperties(T& object, xml_node& propertiesNode, Logger& logger) { @@ -58,6 +84,14 @@ void processProperties(T& object, xml_node& propertiesNode, Logger& logger) { object->properties().template get()[propName] = propNode.text().as_string(); } + else if (propType == "vector[string]") + { + nodeToData(propNode, object->properties().template get>()[propName]); + } + else if (propType == "vector[double]") + { + nodeToData(propNode, object->properties().template get>()[propName]); + } else if (propType == "bool") { std::string sval = propNode.text().as_string(); From 631e3375b866689964cd74df66d1ce4e939be0e6 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 5 Jun 2024 16:06:56 -0400 Subject: [PATCH 14/42] ENH: Supporting Explicit Units for DoubleItems DoubleItems can now have units explicitly assigned to them provided that their Definition does not specify units. This allows Items coming from the same Definition to have different units. Modified API * ``DoubleItemDefinition::hasSupportedUnits`` has been moved to ValueItemDefinition Added API * ValueItem * units() - returns the native units for the item * supportedUnits() - returns the supported units for the item. If there is no Units System assigned to its definition or if its units are supported by the Units System, an empty string is returned else it returns its units. * ValueItemDefinition * supportedUnits() - similar in concept as ValueItem's * DoubleItem * setUnits() - explicitly sets the units of the item * units() - overridden to support explicit units * hasExplicitUnits() - returns true if the item has explicit units. When changing the units of an Item, the system will see if the Item's current input string values are compatible, if they are not, the input value units are replaced with the new ones. See smtk/attribute/testing/cxx/unitDoubleTest.cxx for an example. Both XML and JSON formats have been updated to support this functionality as well as qtInputsItem.cxx, qtDoubleUnitsLineEdit{.h, .cxx}. --- doc/release/notes/supportingExplicitUnits.rst | 32 ++++ doc/userguide/attribute/concepts.rst | 26 +++ smtk/attribute/DoubleItem.cxx | 162 +++++++++++++----- smtk/attribute/DoubleItem.h | 17 ++ smtk/attribute/DoubleItemDefinition.cxx | 11 -- smtk/attribute/DoubleItemDefinition.h | 4 - smtk/attribute/ValueItem.cxx | 21 +++ smtk/attribute/ValueItem.h | 13 ++ smtk/attribute/ValueItemDefinition.cxx | 23 +++ smtk/attribute/ValueItemDefinition.h | 10 ++ smtk/attribute/json/jsonDoubleItem.cxx | 13 ++ smtk/attribute/pybind11/PybindDoubleItem.h | 1 + .../pybind11/PybindDoubleItemDefinition.h | 1 - smtk/attribute/pybind11/PybindValueItem.h | 2 + .../pybind11/PybindValueItemDefinition.h | 2 + smtk/attribute/testing/cxx/unitDoubleItem.cxx | 32 ++++ smtk/extension/qt/qtDoubleUnitsLineEdit.cxx | 92 +++++----- smtk/extension/qt/qtDoubleUnitsLineEdit.h | 4 +- smtk/extension/qt/qtInputsItem.cxx | 35 ++-- smtk/io/XmlDocV1Parser.cxx | 6 + smtk/io/XmlV2StringWriter.cxx | 5 + 21 files changed, 389 insertions(+), 123 deletions(-) create mode 100644 doc/release/notes/supportingExplicitUnits.rst diff --git a/doc/release/notes/supportingExplicitUnits.rst b/doc/release/notes/supportingExplicitUnits.rst new file mode 100644 index 000000000..b3e24d03b --- /dev/null +++ b/doc/release/notes/supportingExplicitUnits.rst @@ -0,0 +1,32 @@ +Attribute Resource Changes +========================== + +Supporting Explicit Units for DoubleItems +----------------------------------------- + +DoubleItems can now have units explicitly assigned to them provided that their Definition does not +specify units. This allows Items coming from the same Definition to have different units. + +Modified API +~~~~~~~~~~~~ + +* ``DoubleItemDefinition::hasSupportedUnits`` has been moved to ValueItemDefinition + +Added API +~~~~~~~~~ + +* ValueItem + * units() - returns the native units for the item + * supportedUnits() - returns the supported units for the item. If there is no Units System assigned to its definition or if its units are supported by the Units System, an empty string is returned else it returns its units. +* ValueItemDefinition + * supportedUnits() - similar in concept as ValueItem's +* DoubleItem + * setUnits() - explicitly sets the units of the item + * units() - overridden to support explicit units + * hasExplicitUnits() - returns true if the item has explicit units. + +When changing the units of an Item, the system will see if the Item's current input string values are compatible, if they are not, the input value units are replaced with the new ones. + +See smtk/attribute/testing/cxx/unitDoubleTest.cxx for an example. + +Both XML and JSON formats have been updated to support this functionality as well as qtInputsItem.cxx, qtDoubleUnitsLineEdit{.h, .cxx}. diff --git a/doc/userguide/attribute/concepts.rst b/doc/userguide/attribute/concepts.rst index ee498291b..60bf8c86f 100644 --- a/doc/userguide/attribute/concepts.rst +++ b/doc/userguide/attribute/concepts.rst @@ -253,6 +253,32 @@ the "construction method" value is represented as a tabbed widget with 1 tab for each of the "Structure" sections above. The default tab will be "2 points". +------------------ +Dealing with Units +------------------ + +SMTK attribute resources support units via the `units`_ library; +you can visit its repository for documentation on how to use the library, +but SMTK does not require you to use the units library for most tasks. +Instead, workflow designers specify units for items as a string and SMTK +user-interface components accept values from users in any compatible units. +This way, when you export a simulation attribute, all the unit conversions +are automatically performed by SMTK for you. + +The value-based Items (DoubleItem, StringItem, IntegerItem, etc..) can have +their *native* units (the units that the Item returns its value in) defined +in their :smtk:`ItemDefinition `. In the +case of DoubleItems, they can also be explicitly assigned to the item itself +(when its definition does not specify the units). + +.. _units: https://gitlab.kitware.com/units/units/ + +For example, a DoubleItem **foo** has its units specify by its Definition as "meters". If foo is assigned "10 cm", its returned value will be 0.1 (meters). + +Changing an Item's explicit units can affect its current values. For example if a DoubleItem **bar** is explicitly assigned "cm" as its units and its value is set to "100 m", its return value will be 10000 (cm). If its explicit units are then changed to "meters", its return value is now 100 (meters). However, if an Item is assigned new units that not compatible to its input values, the input values are replaced with the new units. For example, it we were to later change **bar**'s units to "K" (Kelvin), it return value will be 100 (Kelvin). + +Please see `unitDoubleItem `_ for a simple example of using units. + Migration to newer schema ------------------------- diff --git a/smtk/attribute/DoubleItem.cxx b/smtk/attribute/DoubleItem.cxx index f656a7442..fc6ced519 100644 --- a/smtk/attribute/DoubleItem.cxx +++ b/smtk/attribute/DoubleItem.cxx @@ -76,6 +76,85 @@ bool DoubleItem::initializeValues() return true; } +const std::string& DoubleItem::units() const +{ + // Check to see if we have explicit units + if (!m_units.empty()) + { + return m_units; + } + return ValueItem::units(); +} + +bool DoubleItem::setUnits(const std::string& newUnits) +{ + // make sure that the definition does not already specify units + const DoubleItemDefinition* def = + dynamic_cast(this->definition().get()); + const std::string& defUnits = def->units(); + if (!defUnits.empty()) + { + return false; // Units are coming from the Definition + } + + m_units = newUnits; + + // Now we need to see if the current values are compatible with the new units + std::string valStr, valUnitsStr; + bool foundFirstSetValue = false; + bool canTransferValues = false; + for (std::size_t i = 0; i < this->numberOfValues(); i++) + { + if (!this->isSet(i)) + { + continue; // skip unset values - nothing needs to be processed + } + // if this is the first set value we need to see if it (and all set values)'s units + // are compatible with the new units. NOTE - the assumption here is that the units + // system is consistent - meaning that if all of the existing values were convertible to + // the original units, and if one value can be converted to the new units, then all of the + // remaining values must also be convertible + if (!foundFirstSetValue) + { + // Lets see if we can use the units associate with the values + canTransferValues = this->setValueFromString(i, m_valuesAsString[i]); + if (!canTransferValues) + { + // So we can't use the value with its associated units so we will + // split the string and only keep the value + if (DoubleItemDefinition::splitStringStartingDouble( + m_valuesAsString[i], valStr, valUnitsStr)) + { + this->setValueFromString(i, valStr); + } + else + { + // There was a problem splitting the string (which should never happen) and the value should be unset + this->unset(i); + } + } + foundFirstSetValue = true; + } + // we have determine that the units associated with the values are compatible with the new units + else if (canTransferValues) + { + this->setValueFromString(i, m_valuesAsString[i]); + } + // We are only transferring the values and removing the units + else if (DoubleItemDefinition::splitStringStartingDouble( + m_valuesAsString[i], valStr, valUnitsStr)) + { + this->setValueFromString(i, valStr); + } + else + { + // There was a problem trying to split the string (which should never happen) and the value should be unset + this->unset(i); + } + } + return true; +} + bool DoubleItem::setValue(std::size_t element, const double& val) { if (element >= m_values.size()) @@ -104,19 +183,17 @@ bool DoubleItem::setValue(std::size_t element, const double& val) } m_valuesAsString[element] = this->streamValue(m_values[element]); - const DoubleItemDefinition* def = - dynamic_cast(this->definition().get()); - - // Append the definition's units if they are defined and supported - if (def->hasSupportedUnits()) + // See if we need to append units - this only needs to be done if the + // item's units are supported + auto myUnitStr = this->supportedUnits(); + if (!myUnitStr.empty()) { - const std::string& units = def->units(); - m_valuesAsString[element].append(" ").append(units); + m_valuesAsString[element].append(" ").append(myUnitStr); } return true; } -bool DoubleItem::setValue(std::size_t element, const double& val, const std::string& units) +bool DoubleItem::setValue(std::size_t element, const double& val, const std::string& valUnitStr) { if (element >= m_values.size()) { @@ -127,30 +204,29 @@ bool DoubleItem::setValue(std::size_t element, const double& val, const std::str double convertedVal = val; // Do we need to convert? - const DoubleItemDefinition* def = - dynamic_cast(this->definition().get()); - const std::string& dunits = def->units(); - if (dunits != units) + const std::string& myUnitStr = this->supportedUnits(); + if (myUnitStr != valUnitStr) { + auto unitsSystem = this->definition()->unitsSystem(); // Is there a units system specified? - if (!def->unitsSystem()) + if (!unitsSystem) { return false; // we can not convert units } bool status; - auto defUnits = def->unitsSystem()->unit(dunits, &status); + auto myUnits = unitsSystem->unit(myUnitStr, &status); if (!status) { - return false; // Could not find the definition's units + return false; // Could not find the base's units } - auto valUnits = def->unitsSystem()->unit(units, &status); + auto valUnits = unitsSystem->unit(valUnitStr, &status); if (!status) { return false; // Could not find vals' units } units::Measurement m(val, valUnits); - auto newM = def->unitsSystem()->convert(m, defUnits, &status); + auto newM = unitsSystem->convert(m, myUnits, &status); if (!status) { return false; // could not convert @@ -170,9 +246,9 @@ bool DoubleItem::setValue(std::size_t element, const double& val, const std::str return true; // nothing to be done } m_valuesAsString[element] = this->streamValue(m_values[element]); - if (def->hasSupportedUnits()) + if (!myUnitStr.empty()) { - m_valuesAsString[element].append(" ").append(units); + m_valuesAsString[element].append(" ").append(valUnitStr); } return true; } @@ -192,27 +268,26 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val) return false; // badly formatted string } - const DoubleItemDefinition* def = - dynamic_cast(this->definition().get()); - const std::string& dunits = def->units(); + auto unitsSystem = this->definition()->unitsSystem(); + const std::string& myUnitStr = this->supportedUnits(); - units::Unit defUnit; + units::Unit myUnit; // We can only do conversion if we have a units system and the - // definition has known units. Note that we don't need to do conversion + // item has known units. Note that we don't need to do conversion // if the value does not have units specified bool convert = false; - if (def->unitsSystem() && (!(dunits.empty() || valUnitsStr.empty()))) + if (unitsSystem && (!(myUnitStr.empty() || valUnitsStr.empty()))) { - // If we have a units System, let's see if the definition's units + // If we have a units System, let's see if the base units // are valid? - defUnit = def->unitsSystem()->unit(def->units(), &convert); + myUnit = unitsSystem->unit(myUnitStr, &convert); } double convertedVal; // Is conversion not possible or required if (!convert) { - if (!(valUnitsStr.empty() || (valUnitsStr == dunits))) + if (!(valUnitsStr.empty() || (valUnitsStr == myUnitStr))) { return false; // Units were specified that did not match the definition's } @@ -229,7 +304,7 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val) // We can convert units bool status; - auto valMeasure = def->unitsSystem()->measurement(val, &status); + auto valMeasure = unitsSystem->measurement(val, &status); if (!status) { // Could not parse the value @@ -237,7 +312,7 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val) } if (!valMeasure.m_units.dimensionless()) { - auto convertedMeasure = def->unitsSystem()->convert(valMeasure, defUnit, &status); + auto convertedMeasure = unitsSystem->convert(valMeasure, myUnit, &status); if (!status) { return false; @@ -257,15 +332,14 @@ bool DoubleItem::setValueFromString(std::size_t element, const std::string& val) } m_valuesAsString[element] = val; - // if the value didn't have units but the definition did and the units are supported - // by the units system, append the definition's units to the value - bool defUnitsSupported = def->hasSupportedUnits(); - if (valUnitsStr.empty() && defUnitsSupported) + // if the value didn't have units but the item has units defined and the units are supported + // by the units system, append the units to the value + if (valUnitsStr.empty() && !myUnitStr.empty()) { - m_valuesAsString[element].append(" ").append(dunits); + m_valuesAsString[element].append(" ").append(myUnitStr); } - // Else if the definition's units are not supported then drop the units from the string - else if (!defUnitsSupported) + // Else if the item's units are not supported then drop the units from the string + else if (myUnitStr.empty()) { m_valuesAsString[element] = valStr; } @@ -279,11 +353,10 @@ bool DoubleItem::appendValue(const double& val) return false; } std::string sval = this->streamValue(val); - const DoubleItemDefinition* def = - dynamic_cast(this->definition().get()); - if (!def->units().empty()) + std::string myUnitStr = this->supportedUnits(); + if (!myUnitStr.empty()) { - sval.append(" ").append(def->units()); + sval.append(" ").append(myUnitStr); } m_valuesAsString.push_back(sval); return true; @@ -308,12 +381,11 @@ bool DoubleItem::removeValue(std::size_t element) void DoubleItem::updateDiscreteValue(std::size_t element) { ValueItemTemplate::updateDiscreteValue(element); - const DoubleItemDefinition* def = - dynamic_cast(this->definition().get()); + std::string myUnitStr = this->supportedUnits(); m_valuesAsString[element] = this->streamValue(m_values[element]); - if (!def->units().empty()) + if (!myUnitStr.empty()) { - m_valuesAsString[element].append(" ").append(def->units()); + m_valuesAsString[element].append(" ").append(myUnitStr); } } diff --git a/smtk/attribute/DoubleItem.h b/smtk/attribute/DoubleItem.h index ffc03aa4e..fe98003b1 100644 --- a/smtk/attribute/DoubleItem.h +++ b/smtk/attribute/DoubleItem.h @@ -69,6 +69,22 @@ class SMTKCORE_EXPORT DoubleItem : public ValueItemTemplate /// and not the converted value. std::string valueAsString(std::size_t element) const override; + ///\brief Explicitly set the units of the item. + /// + /// This can only be done if the item's definition does not already specify units. + /// Changing the units will replace the units associated with the item's value strings + /// and can result in the item's values changing. This can happen if the string values + /// originally needed to be converted. + bool setUnits(const std::string& newUnits); + + ///\brief Return the units the item's values are specified in. + /// + /// They can come from either the item's definition or from being explicitly assigned + const std::string& units() const override; + + ///\brief Returns true if the item's units were explicitly assigned to the item. + bool hasExplicitUnits() const { return !m_units.empty(); } + protected: DoubleItem(Attribute* owningAttribute, int itemPosition); DoubleItem(Item* owningItem, int myPosition, int mySubGroupPosition); @@ -76,6 +92,7 @@ class SMTKCORE_EXPORT DoubleItem : public ValueItemTemplate void updateDiscreteValue(std::size_t element) override; std::vector m_valuesAsString; + std::string m_units; private: }; diff --git a/smtk/attribute/DoubleItemDefinition.cxx b/smtk/attribute/DoubleItemDefinition.cxx index a07a89caf..c697e9066 100644 --- a/smtk/attribute/DoubleItemDefinition.cxx +++ b/smtk/attribute/DoubleItemDefinition.cxx @@ -101,17 +101,6 @@ bool DoubleItemDefinition::setUnits(const std::string& newUnits) return true; } -bool DoubleItemDefinition::hasSupportedUnits() const -{ - if (!(m_units.empty() || (m_unitsSystem == nullptr))) - { - bool status; - auto defUnit = m_unitsSystem->unit(m_units, &status); - return status; - } - return false; -} - bool DoubleItemDefinition::setDefaultValue( const std::vector& vals, const std::string& units) diff --git a/smtk/attribute/DoubleItemDefinition.h b/smtk/attribute/DoubleItemDefinition.h index 791ab4b2d..ee95a605a 100644 --- a/smtk/attribute/DoubleItemDefinition.h +++ b/smtk/attribute/DoubleItemDefinition.h @@ -58,10 +58,6 @@ class SMTKCORE_EXPORT DoubleItemDefinition : public ValueItemDefinitionTemplate< smtk::attribute::ItemDefinitionPtr createCopy( smtk::attribute::ItemDefinition::CopyInfo& info) const override; - ///\brief Returns true if units and a units system have been specified and that the - /// specified units are supported by the units system - bool hasSupportedUnits() const; - bool setUnits(const std::string& newUnits) override; ///\brief Splits input string into 2 parts with first part representing double value. diff --git a/smtk/attribute/ValueItem.cxx b/smtk/attribute/ValueItem.cxx index f8d6eadcd..c02c076cd 100644 --- a/smtk/attribute/ValueItem.cxx +++ b/smtk/attribute/ValueItem.cxx @@ -16,6 +16,8 @@ #include "smtk/attribute/Resource.h" #include "smtk/attribute/ValueItemDefinition.h" +#include "units/System.h" + #include // for std::find #include @@ -854,3 +856,22 @@ std::vector ValueItem::relevantEnums( } return def->relevantEnums(false, dummy, includeReadAccess, readAccessLevel); } + +const std::string& ValueItem::units() const +{ + const ValueItemDefinition* def = static_cast(m_definition.get()); + return def->units(); +} + +std::string ValueItem::supportedUnits() const +{ + bool status = false; + auto munits = this->units(); + auto unitsSystem = m_definition->unitsSystem(); + + if (!(munits.empty() || (unitsSystem == nullptr))) + { + auto myUnit = unitsSystem->unit(munits, &status); + } + return (status ? munits : std::string()); +} diff --git a/smtk/attribute/ValueItem.h b/smtk/attribute/ValueItem.h index 3275016e6..54fb4e0de 100644 --- a/smtk/attribute/ValueItem.h +++ b/smtk/attribute/ValueItem.h @@ -137,6 +137,19 @@ class SMTKCORE_EXPORT ValueItem : public smtk::attribute::Item return m_activeChildrenItems[static_cast(i)]; } + ///\brief Returns the native units of the item. + /// + /// These are the units that the item's value methods are returned in and are by default + /// specified by the item's definition + virtual const std::string& units() const; + + ///\brief Return native units of the item that are supported by the units system assigned + /// to its definition. + /// + /// If there is no units system assigned to the definition or if the item's native units are + /// not supported by the units system, an empty string is returned. + virtual std::string supportedUnits() const; + using Item::assign; // Assigns this item to be equivalent to another. Options are processed by derived item classes // Returns true if success and false if a problem occurred. By default, an attribute being used by this diff --git a/smtk/attribute/ValueItemDefinition.cxx b/smtk/attribute/ValueItemDefinition.cxx index 9653b36f5..d8b7eebc9 100644 --- a/smtk/attribute/ValueItemDefinition.cxx +++ b/smtk/attribute/ValueItemDefinition.cxx @@ -17,6 +17,8 @@ #include "smtk/attribute/ValueItem.h" #include "smtk/attribute/ValueItemDefinition.h" +#include "units/System.h" + #include using namespace smtk::attribute; @@ -553,3 +555,24 @@ bool ValueItemDefinition::isDiscreteIndexValid(int index) const { return ((index > -1) && (static_cast(index) < m_discreteValueEnums.size())); } + +bool ValueItemDefinition::hasSupportedUnits() const +{ + if (!(m_units.empty() || (m_unitsSystem == nullptr))) + { + bool status; + auto defUnit = m_unitsSystem->unit(m_units, &status); + return status; + } + return false; +} + +std::string ValueItemDefinition::supportedUnits() const +{ + bool status = false; + if (!(m_units.empty() || (m_unitsSystem == nullptr))) + { + auto defUnit = m_unitsSystem->unit(m_units, &status); + } + return (status ? m_units : std::string()); +} diff --git a/smtk/attribute/ValueItemDefinition.h b/smtk/attribute/ValueItemDefinition.h index 0dd19352d..9e01ca333 100644 --- a/smtk/attribute/ValueItemDefinition.h +++ b/smtk/attribute/ValueItemDefinition.h @@ -48,6 +48,16 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti const std::string& units() const { return m_units; } virtual bool setUnits(const std::string& newUnits); + ///\brief Returns true if units and a units system have been specified and that the + /// specified units are supported by the units system + bool hasSupportedUnits() const; + + ///\brief Return native units of the definition that are supported by its units system. + /// + /// If there is no units system assigned to the definition or if the item's native units are + /// not supported by the units system, an empty string is returned. + std::string supportedUnits() const; + bool isDiscrete() const { return !m_discreteValueEnums.empty(); } std::size_t numberOfDiscreteValues() const { return m_discreteValueEnums.size(); } const std::string& discreteEnum(std::size_t ith) const diff --git a/smtk/attribute/json/jsonDoubleItem.cxx b/smtk/attribute/json/jsonDoubleItem.cxx index 373fc1bf8..5b0c21aac 100644 --- a/smtk/attribute/json/jsonDoubleItem.cxx +++ b/smtk/attribute/json/jsonDoubleItem.cxx @@ -29,6 +29,11 @@ SMTKCORE_EXPORT void to_json(json& j, const smtk::attribute::DoubleItemPtr& item { smtk::attribute::to_json(j, smtk::dynamic_pointer_cast(itemPtr)); smtk::attribute::processDerivedValueToJson(j, itemPtr); + // if the item has explicit units we need to save them + if (itemPtr->hasExplicitUnits()) + { + j["Units"] = itemPtr->units(); + } } SMTKCORE_EXPORT void from_json( @@ -42,6 +47,14 @@ SMTKCORE_EXPORT void from_json( { return; } + + // See if there are units + auto result = j.find("Units"); + if (result != j.end()) + { + itemPtr->setUnits(*result); + } + auto valItem = smtk::dynamic_pointer_cast(itemPtr); smtk::attribute::from_json(j, valItem, itemExpressionInfo, attRefInfo); smtk::attribute::processDerivedValueFromJson( diff --git a/smtk/attribute/pybind11/PybindDoubleItem.h b/smtk/attribute/pybind11/PybindDoubleItem.h index 77955c915..e1aa67f8d 100644 --- a/smtk/attribute/pybind11/PybindDoubleItem.h +++ b/smtk/attribute/pybind11/PybindDoubleItem.h @@ -24,6 +24,7 @@ inline PySharedPtrClass< smtk::attribute::DoubleItem, smtk::attribute::ValueItem .def("setValue", (bool (smtk::attribute::DoubleItem::*)(double const &)) &smtk::attribute::DoubleItem::setValue) .def("setValue", (bool (smtk::attribute::DoubleItem::*)(::size_t, double const &)) &smtk::attribute::DoubleItem::setValue) .def("setValue", (bool (smtk::attribute::DoubleItem::*)(::size_t, double const &, const std::string&)) &smtk::attribute::DoubleItem::setValue) + .def("hasExplicitUnits", &smtk::attribute::DoubleItem::hasExplicitUnits) .def(py::init<::smtk::attribute::DoubleItem const &>()) .def("type", &smtk::attribute::DoubleItem::type) .def_static("CastTo", [](const std::shared_ptr i) { diff --git a/smtk/attribute/pybind11/PybindDoubleItemDefinition.h b/smtk/attribute/pybind11/PybindDoubleItemDefinition.h index b941a1c19..d5b4e7119 100644 --- a/smtk/attribute/pybind11/PybindDoubleItemDefinition.h +++ b/smtk/attribute/pybind11/PybindDoubleItemDefinition.h @@ -30,7 +30,6 @@ inline PySharedPtrClass< smtk::attribute::DoubleItemDefinition, smtk::attribute: .def("buildItem", (smtk::attribute::ItemPtr (smtk::attribute::DoubleItemDefinition::*)(::smtk::attribute::Attribute *, int) const) &smtk::attribute::DoubleItemDefinition::buildItem, py::arg("owningAttribute"), py::arg("itemPosition")) .def("buildItem", (smtk::attribute::ItemPtr (smtk::attribute::DoubleItemDefinition::*)(::smtk::attribute::Item *, int, int) const) &smtk::attribute::DoubleItemDefinition::buildItem, py::arg("owningItem"), py::arg("position"), py::arg("subGroupPosition")) .def("createCopy", &smtk::attribute::DoubleItemDefinition::createCopy, py::arg("info")) - .def("hasSupportedUnits", &smtk::attribute::DoubleItemDefinition::hasSupportedUnits) .def("type", &smtk::attribute::DoubleItemDefinition::type) .def("setDefaultValue", (bool (smtk::attribute::DoubleItemDefinition::*)(double const &)) &smtk::attribute::DoubleItemDefinition::setDefaultValue) .def("setDefaultValue", (bool (smtk::attribute::DoubleItemDefinition::*)(::std::vector > const &)) &smtk::attribute::DoubleItemDefinition::setDefaultValue) diff --git a/smtk/attribute/pybind11/PybindValueItem.h b/smtk/attribute/pybind11/PybindValueItem.h index 23e868dc4..c4eac83ce 100644 --- a/smtk/attribute/pybind11/PybindValueItem.h +++ b/smtk/attribute/pybind11/PybindValueItem.h @@ -60,6 +60,8 @@ inline PySharedPtrClass< smtk::attribute::ValueItem, smtk::attribute::Item > pyb .def("valueAsString", (std::string (smtk::attribute::ValueItem::*)() const) &smtk::attribute::ValueItem::valueAsString) .def("valueAsString", (std::string (smtk::attribute::ValueItem::*)(::size_t) const) &smtk::attribute::ValueItem::valueAsString, py::arg("elementIndex")) .def("valueLabel", &smtk::attribute::ValueItem::valueLabel, py::arg("element")) + .def("units", &smtk::attribute::ValueItem::units) + .def("supportedUnits", &smtk::attribute::ValueItem::supportedUnits) .def_static("CastTo", [](const std::shared_ptr i) { return std::dynamic_pointer_cast(i); }) diff --git a/smtk/attribute/pybind11/PybindValueItemDefinition.h b/smtk/attribute/pybind11/PybindValueItemDefinition.h index 8c1003222..e629cdce3 100644 --- a/smtk/attribute/pybind11/PybindValueItemDefinition.h +++ b/smtk/attribute/pybind11/PybindValueItemDefinition.h @@ -69,6 +69,8 @@ inline PySharedPtrClass< smtk::attribute::ValueItemDefinition, smtk::attribute:: .def("units", &smtk::attribute::ValueItemDefinition::units) .def("usingCommonLabel", &smtk::attribute::ValueItemDefinition::usingCommonLabel) .def("valueLabel", &smtk::attribute::ValueItemDefinition::valueLabel, py::arg("element")) + .def("hasSupportedUnits", &smtk::attribute::ValueItemDefinition::hasSupportedUnits) + .def("supportedUnits", &smtk::attribute::ValueItemDefinition::supportedUnits) .def_static("ToItemDefinition", [](const std::shared_ptr d) { return std::dynamic_pointer_cast(d); }) diff --git a/smtk/attribute/testing/cxx/unitDoubleItem.cxx b/smtk/attribute/testing/cxx/unitDoubleItem.cxx index eac74929f..1d13500ba 100644 --- a/smtk/attribute/testing/cxx/unitDoubleItem.cxx +++ b/smtk/attribute/testing/cxx/unitDoubleItem.cxx @@ -229,6 +229,37 @@ void testBasicGettingValue() } } +void testExplicitUnits() +{ + DoubleItemTest doubleItemTest; + DoubleItemPtr item1 = doubleItemTest.getDoubleItem1(); + DoubleItemPtr item = doubleItemTest.getDoubleItem(); + + smtkTest( + item->setUnits("m"), + "Should be able to set explicit units to m on an Item whose Definition has no units!"); + smtkTest( + !item1->setUnits("m"), + "Should not be able to set explicit units on an Item whose Definition has units!"); + + smtkTest(item->setValue(100.0), "Could not set item to 100 m"); + smtkTest(item->valueAsString() == "100 m", "Expected value string to be 100 m."); + smtkTest( + item->setUnits("cm"), + "Should be able to set explicit units to cm on an Item whose Definition has no units!"); + smtkTest(item->valueAsString() == "100 m", "Expected value string to remain to be 100 m."); + smtkTest(item->value() == 10000.0, "Expected value to be 10000.0 cm."); + smtkTest( + item->setUnits("K"), + "Should be able to set explicit units to K on an Item whose Definition has no units!"); + smtkTest(item->valueAsString() == "100 K", "Expected value string to remain to be 100 K."); + smtkTest( + item->setUnits("madeUpUnits"), + "Should be able to set explicit units to madeUpUnits on an Item whose Definition has no " + "units!"); + smtkTest(item->valueAsString() == "100", "Expected value string to remain to be 100 (no units)."); +} + void testGettingValueWithExpression() { DoubleItemTest doubleItemTest; @@ -360,6 +391,7 @@ int unitDoubleItem(int /*argc*/, char** const /*argv*/) { smtk::io::Logger::instance().reset(); testBasicGettingValue(); + testExplicitUnits(); smtk::io::Logger::instance().reset(); testGettingValueWithExpression(); diff --git a/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx b/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx index 47a84fa7e..d0f5abbd0 100644 --- a/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx +++ b/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx @@ -31,6 +31,7 @@ #include // std::sort et al #include +using namespace smtk::attribute; using namespace smtk::extension; namespace @@ -147,15 +148,17 @@ qtDoubleUnitsLineEdit* qtDoubleUnitsLineEdit::checkAndCreate( qtInputsItem* inputsItem, const QString& tooltip) { - // Get the item definition and see if it specifies dimensional units - auto dDef = inputsItem->item()->definitionAs(); - if (dDef->units().empty()) + // Get the item see if it specifies dimensional units + auto dItem = inputsItem->itemAs(); + auto dUnits = dItem->units(); + + if (dUnits.empty()) { return nullptr; } // Sanity check that units only supported for numerical values - if (dDef->isDiscrete()) + if (dItem->isDiscrete()) { qWarning() << "Ignoring units for discrete or expression item \"" << inputsItem->item()->name().c_str() << "\"."; @@ -163,7 +166,7 @@ qtDoubleUnitsLineEdit* qtDoubleUnitsLineEdit::checkAndCreate( } // Get units system - auto unitsSystem = dDef->unitsSystem(); + auto unitsSystem = dItem->definition()->unitsSystem(); if (unitsSystem == nullptr) { return nullptr; @@ -171,47 +174,29 @@ qtDoubleUnitsLineEdit* qtDoubleUnitsLineEdit::checkAndCreate( // Try parsing the unit string bool parsedOK = false; - auto unit = unitsSystem->unit(dDef->units(), &parsedOK); + auto unit = unitsSystem->unit(dUnits, &parsedOK); if (!parsedOK) { - qWarning() << "Ignoring unrecognized units \"" << dDef->units().c_str() << "\"" + qWarning() << "Ignoring unrecognized units \"" << dUnits.c_str() << "\"" << " in attribute item \"" << inputsItem->item()->name().c_str() << "\"."; return nullptr; } - auto* editor = new qtDoubleUnitsLineEdit(inputsItem, unit, tooltip); + auto* editor = new qtDoubleUnitsLineEdit(inputsItem, tooltip); return editor; } -qtDoubleUnitsLineEdit::qtDoubleUnitsLineEdit( - qtInputsItem* item, - const units::Unit& unit, - const QString& tooltip) +qtDoubleUnitsLineEdit::qtDoubleUnitsLineEdit(qtInputsItem* item, const QString& tooltip) : QLineEdit(item->widget()) , m_inputsItem(item) - , m_unit(unit) , m_baseTooltip(tooltip) , m_internals(new qtDoubleUnitsLineEdit::qtInternals()) { - auto dDef = m_inputsItem->item()->definitionAs(); + auto dItem = m_inputsItem->itemAs(); + auto dUnits = dItem->units(); // Set placeholder text - this->setPlaceholderText(QString::fromStdString(dDef->units())); - - // Get list of compatible units - auto compatibleUnits = m_unit.system()->compatibleUnits(m_unit); - - // create a list of possible units names - for (const auto& unit : compatibleUnits) - { - m_unitChoices.push_back(unit.name().c_str()); - } - - // Lets remove duplicates and sort the list - m_unitChoices.removeDuplicates(); - m_unitChoices.sort(); - // Now make the Definition's units appear at the top - m_unitChoices.push_front(dDef->units().c_str()); + this->setPlaceholderText(QString::fromStdString(dUnits)); // Instantiate completer with (empty) string list model auto* model = new qtCompleterStringModel(this); @@ -314,6 +299,14 @@ void qtDoubleUnitsLineEdit::onTextEdited() { QPalette palette = this->palette(); + auto dItem = m_inputsItem->itemAs(); + auto dUnits = dItem->units(); + auto unitsSystem = dItem->definition()->unitsSystem(); + + // Parsing the Item's unit string + bool parsedOK = false; + auto unit = unitsSystem->unit(dUnits, &parsedOK); + QString text = this->text(); auto utext = text.toStdString(); if (utext.empty()) @@ -337,14 +330,31 @@ void qtDoubleUnitsLineEdit::onTextEdited() valueString += ' '; } + // Get list of compatible units + auto compatibleUnits = unitsSystem->compatibleUnits(unit); + QStringList unitChoices; + + // create a list of possible units names + for (const auto& unit : compatibleUnits) + { + unitChoices.push_back(unit.name().c_str()); + } + + // Lets remove duplicates and sort the list + unitChoices.removeDuplicates(); + unitChoices.sort(); + // Now make the Item's units appear at the top + unitChoices.push_front(dUnits.c_str()); + // Generate the completer strings - for (const QString& unit : m_unitChoices) + for (const QString& unit : unitChoices) { QString entry(valueString.c_str()); entry += unit; compatibleList << entry; } // for } // if (ok) + auto* model = dynamic_cast(m_completer->model()); model->setStringList(compatibleList); @@ -352,9 +362,9 @@ void qtDoubleUnitsLineEdit::onTextEdited() m_completer->complete(); // Update background based on current input string - bool didParse = false; - auto measurement = m_unit.system()->measurement(utext, &didParse); - if (!didParse) + parsedOK = false; + auto measurement = unitsSystem->measurement(utext, &parsedOK); + if (!parsedOK) { QColor invalidColor = m_inputsItem->uiManager()->correctedTempInvalidValueColor(); palette.setColor(QPalette::Base, invalidColor); @@ -363,7 +373,7 @@ void qtDoubleUnitsLineEdit::onTextEdited() } bool inputHasUnits = !smtk::common::StringUtil::trim(unitsString).empty(); - bool conformal = measurement.m_units.dimension() == m_unit.dimension(); + bool conformal = measurement.m_units.dimension() == unit.dimension(); if (!conformal && inputHasUnits) { QColor invalidColor = m_inputsItem->uiManager()->correctedInvalidValueColor().lighter(110); @@ -386,11 +396,11 @@ void qtDoubleUnitsLineEdit::onTextEdited() } else { - units::Measurement convertedMsmt = m_unit.system()->convert(measurement, m_unit, &converted); + units::Measurement convertedMsmt = unitsSystem->convert(measurement, unit, &converted); if (!converted) { std::ostringstream ss; - ss << "Failed to convert measurement: " << measurement << " to units: " << m_unit; + ss << "Failed to convert measurement: " << measurement << " to units: " << unit; qWarning() << ss.str().c_str(); } else @@ -438,9 +448,9 @@ void qtDoubleUnitsLineEdit::onEditFinished() std::string trimmedString = smtk::common::StringUtil::trim(unitsString); if (trimmedString.empty()) { - auto dDef = m_inputsItem->item()->definitionAs(); + auto dItem = m_inputsItem->itemAs(); std::ostringstream ss; - ss << valueString << ' ' << dDef->units(); + ss << valueString << ' ' << dItem->units(); this->blockSignals(true); this->setText(QString::fromStdString(ss.str())); this->blockSignals(false); @@ -472,9 +482,9 @@ void qtDoubleUnitsLineEdit::focusOutEvent(QFocusEvent* event) if (trimmedString.empty()) { QSignalBlocker blocker(this); - auto dDef = m_inputsItem->item()->definitionAs(); + auto dItem = m_inputsItem->itemAs(); std::ostringstream ss; - ss << valueString << ' ' << dDef->units(); + ss << valueString << ' ' << dItem->units(); this->setText(QString::fromStdString(ss.str())); } } diff --git a/smtk/extension/qt/qtDoubleUnitsLineEdit.h b/smtk/extension/qt/qtDoubleUnitsLineEdit.h index b06182218..ff0b2b9ff 100644 --- a/smtk/extension/qt/qtDoubleUnitsLineEdit.h +++ b/smtk/extension/qt/qtDoubleUnitsLineEdit.h @@ -51,7 +51,7 @@ class SMTKQTEXT_EXPORT qtDoubleUnitsLineEdit : public QLineEdit /** \brief Creates instance if double item has units; Returns editor as QWidget */ static qtDoubleUnitsLineEdit* checkAndCreate(qtInputsItem* item, const QString& tooltip); - qtDoubleUnitsLineEdit(qtInputsItem* item, const units::Unit& unit, const QString& tooltip); + qtDoubleUnitsLineEdit(qtInputsItem* item, const QString& tooltip); ~qtDoubleUnitsLineEdit() override; /** @@ -130,8 +130,6 @@ protected Q_SLOTS: void focusOutEvent(QFocusEvent* event) override; QPointer m_inputsItem; - units::Unit m_unit; - QStringList m_unitChoices; QString m_baseTooltip; int m_lastKey = -1; diff --git a/smtk/extension/qt/qtInputsItem.cxx b/smtk/extension/qt/qtInputsItem.cxx index 4d21c696a..de8615301 100644 --- a/smtk/extension/qt/qtInputsItem.cxx +++ b/smtk/extension/qt/qtInputsItem.cxx @@ -730,16 +730,15 @@ void qtInputsItem::showExpressionResultWidgets( // Add units to the text if definition has units string recognized by units system QString displayText = text; auto item = m_itemInfo.itemAs(); - auto def = - std::dynamic_pointer_cast(item->definition()); - if (!def->units().empty()) + auto itemUnits = item->units(); + if (!itemUnits.empty()) { - auto unitsSystem = item->attribute()->attributeResource()->unitsSystem(); + auto unitsSystem = item->definition()->unitsSystem(); bool parsed = false; - unitsSystem->unit(def->units(), &parsed); + unitsSystem->unit(itemUnits, &parsed); if (parsed) { - displayText = QString("%1 %2").arg(text).arg(def->units().c_str()); + displayText = QString("%1 %2").arg(text).arg(itemUnits.c_str()); } } m_internals->m_expressionResultLineEdit->setText(displayText); @@ -899,8 +898,6 @@ QFrame* qtInputsItem::createLabelFrame( const smtk::attribute::ValueItem* vitem, const smtk::attribute::ValueItemDefinition* vitemDef) { - smtk::attribute::ValueItemPtr dataObj = m_itemInfo.itemAs(); - auto itemDef = dataObj->definitionAs(); auto* iview = m_itemInfo.baseView(); // Lets create the label and proper decorations QSizePolicy sizeFixedPolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); @@ -963,7 +960,8 @@ QFrame* qtInputsItem::createLabelFrame( label->setToolTip(strBriefDescription.c_str()); } - if (!vitemDef->units().empty()) + const auto& valUnits = vitem->units(); + if (!valUnits.empty()) { // Check if we should units to the label std::string option = "LineEdit"; // default behavior @@ -972,18 +970,18 @@ QFrame* qtInputsItem::createLabelFrame( if (addUnitsLabel && !vitemDef->isDiscrete()) { // Check if units are "valid" - auto unitsSystem = vitem->attribute()->attributeResource()->unitsSystem(); + const auto& unitsSystem = vitemDef->unitsSystem(); if (unitsSystem) { bool unitsParsed = false; - units::Unit defUnit = unitsSystem->unit(vitemDef->units(), &unitsParsed); + units::Unit defUnit = unitsSystem->unit(valUnits, &unitsParsed); addUnitsLabel = addUnitsLabel && (!unitsParsed); } } if (addUnitsLabel) { QString unitText = label->text(); - unitText.append(" (").append(vitemDef->units().c_str()).append(")"); + unitText.append(" (").append(valUnits.c_str()).append(")"); label->setText(unitText); } } @@ -1603,6 +1601,7 @@ QWidget* qtInputsItem::createDoubleWidget( auto dDef = vitem->definitionAs(); auto ditem = dynamic_pointer_cast(vitem); double minVal, maxVal; + auto dUnits = vitem->units(); // Let get the range of the item minVal = smtk_DOUBLE_MIN; @@ -1703,7 +1702,7 @@ QWidget* qtInputsItem::createDoubleWidget( { tooltip.append("; "); } - tooltip.append("Val: ").append(val).append(" ").append(dDef->units().c_str()); + tooltip.append("Val: ").append(val).append(" ").append(dUnits.c_str()); } } } @@ -1772,10 +1771,10 @@ QWidget* qtInputsItem::createDoubleWidget( spinbox->setMinimum(minVal); double step; int decimals; - if (!dDef->units().empty()) + if (!dUnits.empty()) { QString ustring = " "; - ustring.append(dDef->units().c_str()); + ustring.append(dUnits.c_str()); spinbox->setSuffix(ustring); } if (m_itemInfo.component().attributeAsDouble("StepSize", step)) @@ -1878,10 +1877,10 @@ QWidget* qtInputsItem::createIntWidget( spinbox->setMaximum(maxVal); spinbox->setMinimum(minVal); int step; - if (!iDef->units().empty()) + if (!vitem->units().empty()) { QString ustring = " "; - ustring.append(iDef->units().c_str()); + ustring.append(vitem->units().c_str()); spinbox->setSuffix(ustring); } if (m_itemInfo.component().attributeAsInt("StepSize", step)) @@ -2339,7 +2338,7 @@ void qtInputsItem::onInputValueChanged(QObject* obj) { newToolTip.append("; "); } - newToolTip.append("Val: ").append(val).append(" ").append(dDef->units().c_str()); + newToolTip.append("Val: ").append(val).append(" ").append(ditem->units().c_str()); } valueUnitsBox->setToolTip(newToolTip); valChanged = true; diff --git a/smtk/io/XmlDocV1Parser.cxx b/smtk/io/XmlDocV1Parser.cxx index 716fd0098..9e43f5735 100644 --- a/smtk/io/XmlDocV1Parser.cxx +++ b/smtk/io/XmlDocV1Parser.cxx @@ -2559,6 +2559,12 @@ void XmlDocV1Parser::processDirectoryItem(pugi::xml_node& node, attribute::Direc void XmlDocV1Parser::processDoubleItem(pugi::xml_node& node, attribute::DoubleItemPtr item) { + xml_node unitsNode = node.child("Units"); + if (unitsNode) + { + item->setUnits(unitsNode.text().get()); + } + this->processValueItem(node, dynamic_pointer_cast(item)); processDerivedValue( node, item, m_resource, m_itemExpressionInfo, m_logger); diff --git a/smtk/io/XmlV2StringWriter.cxx b/smtk/io/XmlV2StringWriter.cxx index dbc9dbdae..cb42ed0ec 100644 --- a/smtk/io/XmlV2StringWriter.cxx +++ b/smtk/io/XmlV2StringWriter.cxx @@ -1304,6 +1304,11 @@ void XmlV2StringWriter::processDoubleItem(pugi::xml_node& node, attribute::Doubl { this->processValueItem(node, dynamic_pointer_cast(item)); processDerivedValue(node, item); + if (item->hasExplicitUnits()) + { + xml_node unitsNode = node.append_child("Units"); + unitsNode.text().set(item->units().c_str()); + } } void XmlV2StringWriter::processIntItem(pugi::xml_node& node, attribute::IntItemPtr item) From e8904d52b47014b96dd85ec70b0972a41f78c592 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Thu, 30 May 2024 11:59:40 -0400 Subject: [PATCH 15/42] ENH: Added ability to assign custom relevancy functions to Items SMTK now supports the ability to assign a function to an Attribute Item that will be used to determine if an Item is currently relevant. ``Item::isRelevant`` method has been refactored and most of the original logic has been moved to a new method called ``Item::defaultIsRelevant``. To set a custom relevancy function, use ``Item::setCustomIsRelevant``. To determine if an Item is relevant you will still call ``isRelevant`` and it will use the custom relevancy function if one has been set, else it will call the default method. Please see ``smtk/attribute/testing/cxx/customIsRelevantTest.cxx`` for an example of how to use this functionality. --- doc/release/notes/customIsRelevant.rst | 14 ++ doc/userguide/attribute/concepts.rst | 30 ++++ smtk/attribute/Attribute.cxx | 3 +- smtk/attribute/Item.cxx | 13 ++ smtk/attribute/Item.h | 31 +++- smtk/attribute/pybind11/PybindItem.h | 1 + smtk/attribute/testing/cxx/CMakeLists.txt | 5 +- .../testing/cxx/customIsRelevantTest.cxx | 136 ++++++++++++++++++ 8 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 doc/release/notes/customIsRelevant.rst create mode 100644 smtk/attribute/testing/cxx/customIsRelevantTest.cxx diff --git a/doc/release/notes/customIsRelevant.rst b/doc/release/notes/customIsRelevant.rst new file mode 100644 index 000000000..d2928328e --- /dev/null +++ b/doc/release/notes/customIsRelevant.rst @@ -0,0 +1,14 @@ +Changes in Attribute Resource +============================= + +Added ability to assign custom relevancy functions to Items +----------------------------------------------------------- + +SMTK now supports the ability to assign a function to an Attribute Item that would +be used to determine if an Item is currently relevant. ``Item::isRelevant`` +method has been refactored and most of the original logic has been moved to a new method called ``Item::defaultIsRelevant``. +To set a custom relevancy function, use ``Item::setCustomIsRelevant``. + +To determine if an Item is relevant you will still call ``isRelevant`` and it will use the custom relevancy function if one has been set, else it will call the default method. + +Please see ``smtk/attribute/testing/cxx/customIsRelevantTest.cxx`` for an example of how to use this functionality. diff --git a/doc/userguide/attribute/concepts.rst b/doc/userguide/attribute/concepts.rst index 60bf8c86f..1aecac889 100644 --- a/doc/userguide/attribute/concepts.rst +++ b/doc/userguide/attribute/concepts.rst @@ -253,6 +253,36 @@ the "construction method" value is represented as a tabbed widget with 1 tab for each of the "Structure" sections above. The default tab will be "2 points". +---------------------- +Dealing with Relevance +---------------------- + +In many workflows, especially ones that support complex aspects such as multi-physics, typically only a subset of the information specified is relevant with respects to the current task. For example, a hydrological workflow can have information related to constituent and heat transport; however, this information is not relevant if the task is to model only fluid flow. + +Once major aspect related to relevance is validity. If information is not relevant then it should not affect validity. In the above example, missing information concerning a constituent property should not indicate that the workflow is invalid (in this case invalid means incomplete); however, if the task was to change and require constituent transport, then it would be considered invalid. + +In SMTK's attribute resource, attributes and their items provide methods to indicate their relevance. By default, relevance depends on a set of categories (typically this is the attribute resource's set of active categories) . If the set of categories satisfies the category constraints associated with attribute or item, then it is considered relevant. + +For UI purposes, relevance can also take a requested read access level into consideration. For example, if a workflow is displaying basic information (indicated by read access level 0) and an attribute contains only items with a greater read level, then none of the attribute items would be displayed and in this scenario the attribute itself would be considered not relevant (w/r to the UI) and should not be displayed. + +Custom Relevance for Items +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +SMTK now supports the ability to assign a function to an attribute item that would +be used to determine if an item is currently relevant. For example, it is now possible to have the relevance of one item depend on the value of another. + +**Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call ``Item::setCustomIsRelevant()`` each time the item is loaded or created. + +Related API +~~~~~~~~~~~ + +* ``Attribute::isRelevant`` - method to call to see if an attribute is relevant +* ``Item::isRelevant`` - method to call to see if an item is relevant +* ``Item::defaultIsRelevant`` - default relevance method used by ``Attribute::isRelevant`` when no custom relevance function has been specified +* ``Item::setCustomIsRelevant`` - method used to set a custom relevance function. + +Please see `customIsRelevantTest `_ for an example of how to use this functionality. + ------------------ Dealing with Units ------------------ diff --git a/smtk/attribute/Attribute.cxx b/smtk/attribute/Attribute.cxx index 3912acf04..cb9a0eed1 100644 --- a/smtk/attribute/Attribute.cxx +++ b/smtk/attribute/Attribute.cxx @@ -365,7 +365,8 @@ bool Attribute::isRelevant( } } } - if (!includeReadAccess) + + if (m_items.empty() || !includeReadAccess) { return true; } diff --git a/smtk/attribute/Item.cxx b/smtk/attribute/Item.cxx index 70f68bead..c5cd25008 100644 --- a/smtk/attribute/Item.cxx +++ b/smtk/attribute/Item.cxx @@ -96,6 +96,19 @@ bool Item::isRelevant(bool includeCategories, bool includeReadAccess, unsigned i return false; // Item has been marked to be ignored } + if (m_customIsRelevant) + { + return m_customIsRelevant(this, includeCategories, includeReadAccess, readAccessLevel); + } + + return this->defaultIsRelevant(includeCategories, includeReadAccess, readAccessLevel); +} + +bool Item::defaultIsRelevant( + bool includeCategories, + bool includeReadAccess, + unsigned int readAccessLevel) const +{ if (includeCategories) { auto myAttribute = this->attribute(); diff --git a/smtk/attribute/Item.h b/smtk/attribute/Item.h index 8d0e0c0e3..5740ce016 100644 --- a/smtk/attribute/Item.h +++ b/smtk/attribute/Item.h @@ -53,6 +53,12 @@ class SMTKCORE_EXPORT Item : public smtk::enable_shared_from_this public: smtkTypeMacroBase(smtk::attribute::Item); + + ///\brief Typedef for custom relevance functions + typedef std::function< + bool(const Item*, bool includeCatagories, bool includeReadAccess, unsigned int readAccessLevel)> + RelevanceFunc; + enum Type { AttributeRefType, //!< Needed for backward compatibility w/r XML/JSON formats < 4.0 @@ -98,16 +104,32 @@ class SMTKCORE_EXPORT Item : public smtk::enable_shared_from_this } /// @} + ///@{ + ///\brief Set and Get Methods for specifying a custom isRelevant function + void setCustomIsRelevant(RelevanceFunc func) { m_customIsRelevant = func; } + RelevanceFunc customIsRelevant() const { return m_customIsRelevant; } + ///@} + ///\brief Returns true if the item is relevant. /// + /// If a customIsRelevant function has been set, then it will be used to calculate if the + /// item is relevant based on includeCategories and includeReadAccess parameters, else + /// the item's defaultIsRelevant method will be used + bool isRelevant( + bool includeCatagories = true, + bool includeReadAccess = false, + unsigned int readAccessLevel = 0) const; + + ///\brief Default isRelevant method that returns true if the item is relevant. + /// /// If the item is marked ignored then return false. /// If includeCatagories is true and the item does not pass it's category checks, then return false, /// If includeReadAccess is true, and the item's advanceLevel is > readAccessLevel then return false. /// Else return true. - virtual bool isRelevant( - bool includeCatagories = true, - bool includeReadAccess = false, - unsigned int readAccessLevel = 0) const; + virtual bool defaultIsRelevant( + bool includeCatagories, + bool includeReadAccess, + unsigned int readAccessLevel) const; /// @{ /// \brief return a child item that matches name and satisfies the SearchStyle @@ -304,6 +326,7 @@ class SMTKCORE_EXPORT Item : public smtk::enable_shared_from_this bool m_isIgnored; smtk::attribute::ConstItemDefinitionPtr m_definition; std::map m_userData; + RelevanceFunc m_customIsRelevant = nullptr; private: bool m_hasLocalAdvanceLevelInfo[2]; diff --git a/smtk/attribute/pybind11/PybindItem.h b/smtk/attribute/pybind11/PybindItem.h index 210fbf84b..35974b1bb 100644 --- a/smtk/attribute/pybind11/PybindItem.h +++ b/smtk/attribute/pybind11/PybindItem.h @@ -33,6 +33,7 @@ inline PySharedPtrClass< smtk::attribute::Item > pybind11_init_smtk_attribute_It .def("label", &smtk::attribute::Item::label) .def("type", &smtk::attribute::Item::type) .def("isRelevant", &smtk::attribute::Item::isRelevant, py::arg("includeCategoryChecking") = true, py::arg("includeReadAccess") = true, py::arg("readAccessLevel") = 0) + .def("defaultIsRelevant", &smtk::attribute::Item::defaultIsRelevant, py::arg("includeCategoryChecking"), py::arg("includeReadAccess"), py::arg("readAccessLevel")) .def("isValid", (bool (smtk::attribute::Item::*)(bool) const) &smtk::attribute::Item::isValid, py::arg("useActiveCategories") = true) .def("isValid", (bool (smtk::attribute::Item::*)(std::set const &) const) &smtk::attribute::Item::isValid, py::arg("categories")) .def("definition", &smtk::attribute::Item::definition) diff --git a/smtk/attribute/testing/cxx/CMakeLists.txt b/smtk/attribute/testing/cxx/CMakeLists.txt index c541a6518..c02a2c2f5 100644 --- a/smtk/attribute/testing/cxx/CMakeLists.txt +++ b/smtk/attribute/testing/cxx/CMakeLists.txt @@ -5,18 +5,19 @@ add_executable(attributeImportExportTest attributeImportExportTest.cxx) target_link_libraries(attributeImportExportTest smtkCore) set(attributeTests + attributeAutoNamingTest basicAttributeDefinitionTest basicAttributeDerivationTest basicAttributeFindTest basicAttributeXMLWriterTest + categoryTest childrenItemsTest + customIsRelevantTest discreteStringsWithDefaultTest expressionTest expressionTest2 extensibleAttributeCopyTest fileValidationTest - attributeAutoNamingTest - categoryTest referenceItemStorageTest ) set(basicAttributeXMLWriterTest_ARGS diff --git a/smtk/attribute/testing/cxx/customIsRelevantTest.cxx b/smtk/attribute/testing/cxx/customIsRelevantTest.cxx new file mode 100644 index 000000000..3f5a3990e --- /dev/null +++ b/smtk/attribute/testing/cxx/customIsRelevantTest.cxx @@ -0,0 +1,136 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#include "smtk/attribute/Attribute.h" +#include "smtk/attribute/Definition.h" +#include "smtk/attribute/DoubleItem.h" +#include "smtk/attribute/DoubleItemDefinition.h" +#include "smtk/attribute/IntItem.h" +#include "smtk/attribute/IntItemDefinition.h" +#include "smtk/attribute/Resource.h" +#include "smtk/attribute/StringItem.h" +#include "smtk/attribute/StringItemDefinition.h" +#include + +using namespace smtk::attribute; + +bool customIsRelevant( + const Item* item, + bool includeCategories, + bool includeReadAccess, + unsigned int readAccessLevel) +{ + // This function will add an extra relevancy constraint indicating that + // this item is relevant only if item b's value is between 0 and 10 inclusive + + // find Item b + if (item) + { + auto att = item->attribute(); + if (att) + { + auto itemB = att->findAs("b"); + if (itemB) + { + if (!itemB->isSet()) + { + // Item b has not value + return false; + } + double val = itemB->value(); + if ((val < 0) || (val > 10)) + { + return false; + } + } + } + } + return item->defaultIsRelevant(includeCategories, includeReadAccess, readAccessLevel); +} + +int main() +{ + int status = 0; + + smtk::attribute::ResourcePtr resource = Resource::create(); + std::cerr << "Resource Created\n"; + + // Lets create an attribute with 2 Double Items + smtk::attribute::DefinitionPtr attDef = resource->createDefinition("Test"); + auto adef = attDef->addItemDefinition("a"); + auto bdef = attDef->addItemDefinition("b"); + + auto att = resource->createAttribute("testAtt", attDef); + if (att) + { + std::cerr << "Attribute testAtt created\n"; + } + else + { + std::cerr << "ERROR: Attribute testAtt not created\n"; + status = -1; + } + + // By default, if we ignore categories and read access, the attribute and its two items + // should be considered relevant + if (att->isRelevant(false, false)) + { + std::cerr << "Initial Test: testAtt is Relevant\n"; + } + else + { + std::cerr << "ERROR: Initial Test: testAtt is NOT Relevant\n"; + status = -1; + } + + for (int i = 0; i < 2; i++) + { + if (att->item(i)->isRelevant(false, false)) + { + std::cerr << "Initial Test: Item " << att->item(i)->name() << " is Relevant\n"; + } + else + { + std::cerr << "ERROR: Initial Test: Item " << att->item(i)->name() << " is NOT Relevant\n"; + status = -1; + } + } + + // Now lets give Item a the custom relevancy function + att->item(0)->setCustomIsRelevant(customIsRelevant); + + // Since b is not currently set, a should not be relevant + if (!att->item(0)->isRelevant(false, false)) + { + std::cerr << "Pass Item b not set: Item " << att->item(0)->name() << " is not Relevant\n"; + } + else + { + std::cerr << "ERROR: Pass Item b not set: Item " << att->item(0)->name() << " IS Relevant\n"; + status = -1; + } + + // now lets set b to 5 which should make a relevant + auto itemB = att->findAs("b"); + itemB->setValue(5.0); + + if (att->item(0)->isRelevant(false, false)) + { + std::cerr << "Pass Item b set to 5: Item " << att->item(0)->name() << " is Relevant\n"; + } + else + { + std::cerr << "ERROR: Pass Item b set to 5: Item " << att->item(0)->name() + << " is NOT Relevant\n"; + status = -1; + } + + return status; +} From 63667462c87b30fe972735a209b709ebe2542f53 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Tue, 18 Jun 2024 16:52:54 -0400 Subject: [PATCH 16/42] BUG: Fix qtReferenceItem::updateItem The issue was that the original implementation simply reconstructed the entire widget when requesting it updated itself from its underlying item. This potentially caused the widget to change its position w/r its siblings. This change now prevents the widget from being rebuilt. --- smtk/extension/qt/qtDateTimeItem.cxx | 4 ++-- smtk/extension/qt/qtReferenceItem.cxx | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/smtk/extension/qt/qtDateTimeItem.cxx b/smtk/extension/qt/qtDateTimeItem.cxx index 6caad8c30..a07386f51 100644 --- a/smtk/extension/qt/qtDateTimeItem.cxx +++ b/smtk/extension/qt/qtDateTimeItem.cxx @@ -525,8 +525,8 @@ void qtDateTimeItem::updateUI() this->loadInputValues(); - // we need this layout so that for items with conditionan children, - // the label will line up at Top-left against the chilren's widgets. + // we need this layout so that for items with conditional children, + // the label will line up at Top-left against the children's widgets. // QVBoxLayout* vTLlayout = new QVBoxLayout; // vTLlayout->setObjectName("vTLlayout"); // vTLlayout->setMargin(0); diff --git a/smtk/extension/qt/qtReferenceItem.cxx b/smtk/extension/qt/qtReferenceItem.cxx index 90ce91a17..9a2998447 100644 --- a/smtk/extension/qt/qtReferenceItem.cxx +++ b/smtk/extension/qt/qtReferenceItem.cxx @@ -200,7 +200,15 @@ std::pair qtReferenceItem::selectionIconPaths() const void qtReferenceItem::updateItemData() { - this->updateUI(); + smtk::attribute::ItemPtr itm = m_itemInfo.item(); + if (itm->isOptional()) + { + this->setOutputOptional(itm->localEnabledState() ? 1 : 0); + } + + this->synchronize(UpdateSource::GUI_FROM_ITEM); + + this->updateSynopsisLabels(); this->Superclass::updateItemData(); } @@ -423,7 +431,7 @@ void qtReferenceItem::createWidget() } this->clearWidgets(); - this->updateItemData(); + this->updateUI(); } void qtReferenceItem::clearWidgets() @@ -646,12 +654,6 @@ void qtReferenceItem::updateUI() { m_itemInfo.parentWidget()->layout()->addWidget(m_widget); } - if (itm->isOptional()) - { - this->setOutputOptional(itm->localEnabledState() ? 1 : 0); - } - this->synchronize(UpdateSource::GUI_FROM_ITEM); - // Add a vertical spacer the same height as buttons that are sometimes hidden. m_widget->show(); entryLayout->addItem(new QSpacerItem( @@ -661,7 +663,7 @@ void qtReferenceItem::updateUI() QSizePolicy::Fixed)); this->sneakilyHideButtons(); - this->updateSynopsisLabels(); + this->updateItemData(); } void qtReferenceItem::popupClosing() From 0eea37490ad8b71ea30b02f5ae3a38b6017074bc Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Mon, 17 Jun 2024 15:09:26 -0400 Subject: [PATCH 17/42] ENH: Add Custom isEnumRelevant Functionality SMTK now supports the ability to assign a function to a Value Item Definition that would be used to determine if a Value Item's discrete enumeration value is currently relevant. Please see ``smtk/attribute/testing/cxx/customIsRelevantTest.cxx`` for an example of how to use this functionality. --- doc/release/notes/customIsEnumRelevant.rst | 10 ++ doc/userguide/attribute/concepts.rst | 19 ++- smtk/attribute/ValueItem.cxx | 67 ++++++++-- smtk/attribute/ValueItemDefinition.cxx | 53 ++++---- smtk/attribute/ValueItemDefinition.h | 29 +++++ smtk/attribute/pybind11/PybindItem.h | 2 + .../pybind11/PybindValueItemDefinition.h | 3 + .../testing/cxx/customIsRelevantTest.cxx | 116 +++++++++++++++++- 8 files changed, 261 insertions(+), 38 deletions(-) create mode 100644 doc/release/notes/customIsEnumRelevant.rst diff --git a/doc/release/notes/customIsEnumRelevant.rst b/doc/release/notes/customIsEnumRelevant.rst new file mode 100644 index 000000000..222a09c7c --- /dev/null +++ b/doc/release/notes/customIsEnumRelevant.rst @@ -0,0 +1,10 @@ +Changes in Attribute Resource +============================= + +Added ability to assign custom enum relevancy functions to Value Item Definitions +--------------------------------------------------------------------------------- + +SMTK now supports the ability to assign a function to a Value Item Definition that would +be used to determine if a Value Item's discrete enumeration value is currently relevant. + +Please see ``smtk/attribute/testing/cxx/customIsRelevantTest.cxx`` for an example of how to use this functionality. diff --git a/doc/userguide/attribute/concepts.rst b/doc/userguide/attribute/concepts.rst index 1aecac889..0205e6735 100644 --- a/doc/userguide/attribute/concepts.rst +++ b/doc/userguide/attribute/concepts.rst @@ -259,12 +259,15 @@ Dealing with Relevance In many workflows, especially ones that support complex aspects such as multi-physics, typically only a subset of the information specified is relevant with respects to the current task. For example, a hydrological workflow can have information related to constituent and heat transport; however, this information is not relevant if the task is to model only fluid flow. +Note that this concept of relevance can also apply to the values a discrete item (an item will a discrete list of enumerated values) can be set to. + Once major aspect related to relevance is validity. If information is not relevant then it should not affect validity. In the above example, missing information concerning a constituent property should not indicate that the workflow is invalid (in this case invalid means incomplete); however, if the task was to change and require constituent transport, then it would be considered invalid. In SMTK's attribute resource, attributes and their items provide methods to indicate their relevance. By default, relevance depends on a set of categories (typically this is the attribute resource's set of active categories) . If the set of categories satisfies the category constraints associated with attribute or item, then it is considered relevant. For UI purposes, relevance can also take a requested read access level into consideration. For example, if a workflow is displaying basic information (indicated by read access level 0) and an attribute contains only items with a greater read level, then none of the attribute items would be displayed and in this scenario the attribute itself would be considered not relevant (w/r to the UI) and should not be displayed. + Custom Relevance for Items ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -273,13 +276,27 @@ be used to determine if an item is currently relevant. For example, it is now po **Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call ``Item::setCustomIsRelevant()`` each time the item is loaded or created. +Custom Relevance for Enumerated Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +SMTK now supports the ability to assign a function to a value item's definition (consisting of discrete enumerated values). Similar to relevance function for item, this function would +be used to determine if an item's enum value is currently relevant. For example, it is now possible to restrict the possible relevant values an item can based on the value of another item. + +**Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call ``Item::setCustomIsRelevant()`` each time the item is loaded or created. + Related API ~~~~~~~~~~~ * ``Attribute::isRelevant`` - method to call to see if an attribute is relevant * ``Item::isRelevant`` - method to call to see if an item is relevant * ``Item::defaultIsRelevant`` - default relevance method used by ``Attribute::isRelevant`` when no custom relevance function has been specified -* ``Item::setCustomIsRelevant`` - method used to set a custom relevance function. +* ``Item::setCustomIsRelevant`` - method used to set a custom relevance function +* ``Item::customIsRelevant - method used to return the custom relevance function if one has been set +* ``ValueItem::relevantEnums`` - returns a list of enum strings representing the current relevant discrete values +* ``ValueItemDefinition::relevantEnums`` - the method called by ``ValueItem::relevantEnums`` when no custom relevance function has been specified +* ``ValueItemDefinition::defaultIsEnumRelevant`` - the default relevance method for enum values based solely on categories and read access level +* ``ValueItemDefinition::setCustomEnumIsRelevant - method used to set a custom enum relevance function +* ``ValueItemDefinition::customEnumIsRelevant - method used to return the custom enum relevance function if one has been set Please see `customIsRelevantTest `_ for an example of how to use this functionality. diff --git a/smtk/attribute/ValueItem.cxx b/smtk/attribute/ValueItem.cxx index c02c076cd..13dd79360 100644 --- a/smtk/attribute/ValueItem.cxx +++ b/smtk/attribute/ValueItem.cxx @@ -158,9 +158,9 @@ bool ValueItem::isValidInternal(bool useCategories, const std::set& // Let check to make sure all of the values are set. In the case of discrete values // we need to also make sure values are still valid since enumerations can depend on - // the set of categories enabled + // the set of categories enabled and possibly a custom isEnumRelevant function - if (!(this->isDiscrete() && useCategories)) + if (!this->isDiscrete()) // just need to test to make sure all values are set { for (std::size_t i = 0; i < m_isSet.size(); ++i) { @@ -172,18 +172,44 @@ bool ValueItem::isValidInternal(bool useCategories, const std::set& } else { - // Discrete Item w/ categories case const ValueItemDefinition* def = static_cast(m_definition.get()); if (!def) { return false; } - for (std::size_t i = 0; i < m_isSet.size(); ++i) + // Is there a custom function to determine if an enum is relevant? + auto customIsEnumRelevant = def->customEnumIsRelevant(); + if (customIsEnumRelevant) { - if (!(m_isSet[i] && def->isDiscreteIndexValid(m_discreteIndices[i], categories))) + for (std::size_t i = 0; i < m_isSet.size(); ++i) { - return false; + if (!(m_isSet[i] && + customIsEnumRelevant( + this, m_discreteIndices[i], useCategories, categories, false, 0))) + { + return false; + } + } + } + else if (useCategories) + { + for (std::size_t i = 0; i < m_isSet.size(); ++i) + { + if (!(m_isSet[i] && def->isDiscreteIndexValid(m_discreteIndices[i], categories))) + { + return false; + } + } + } + else // all enum values are valid - just need to make sure all values are set + { + for (std::size_t i = 0; i < m_isSet.size(); ++i) + { + if (!m_isSet[i]) + { + return false; + } } } } @@ -839,7 +865,9 @@ std::vector ValueItem::relevantEnums( unsigned int readAccessLevel) const { const ValueItemDefinition* def = static_cast(m_definition.get()); - std::set dummy; + std::set activeCategories; + bool useCategories = false; + if (includeCategories) { // See if we can get the active categories of the related Resource, else ignore categories @@ -849,12 +877,31 @@ std::vector ValueItem::relevantEnums( auto aResource = myAttribute->attributeResource(); if (aResource && aResource->activeCategoriesEnabled()) { - return def->relevantEnums( - true, aResource->activeCategories(), includeReadAccess, readAccessLevel); + useCategories = true; + activeCategories = aResource->activeCategories(); } } } - return def->relevantEnums(false, dummy, includeReadAccess, readAccessLevel); + + // Is there a custom enum relevant function? + auto customIsEnumRelevant = def->customEnumIsRelevant(); + + if (!customIsEnumRelevant) + { + return def->relevantEnums(useCategories, activeCategories, includeReadAccess, readAccessLevel); + } + + std::vector result; + std::size_t i, n = def->numberOfDiscreteValues(); + for (i = 0; i < n; i++) + { + if (customIsEnumRelevant( + this, i, useCategories, activeCategories, includeReadAccess, readAccessLevel)) + { + result.push_back(def->discreteEnum(i)); + } + } + return result; } const std::string& ValueItem::units() const diff --git a/smtk/attribute/ValueItemDefinition.cxx b/smtk/attribute/ValueItemDefinition.cxx index d8b7eebc9..9d0d57b3b 100644 --- a/smtk/attribute/ValueItemDefinition.cxx +++ b/smtk/attribute/ValueItemDefinition.cxx @@ -487,42 +487,51 @@ bool ValueItemDefinition::addItemDefinition(smtk::attribute::ItemDefinitionPtr c return true; } -std::vector ValueItemDefinition::relevantEnums( +bool ValueItemDefinition::defaultIsEnumRelevant( + int enumIndex, bool includeCategories, const std::set& testCategories, bool includeReadAccess, unsigned int readAccessLevel) const { - std::vector result; - if (!(includeCategories || includeReadAccess)) - { - return m_discreteValueEnums; - } - if (includeCategories) { - for (std::size_t i = 0; i < m_discreteValueEnums.size(); i++) + const auto& catExp = this->enumCategories(m_discreteValueEnums[enumIndex]); + if (catExp.allPass() || catExp.passes(testCategories)) { - const auto& catExp = this->enumCategories(m_discreteValueEnums[i]); - if (catExp.allPass() || catExp.passes(testCategories)) + if ( + (!includeReadAccess) || + (this->enumAdvanceLevel(m_discreteValueEnums[enumIndex]) <= readAccessLevel)) { - if ( - (!includeReadAccess) || - (this->enumAdvanceLevel(m_discreteValueEnums[i]) <= readAccessLevel)) - { - result.push_back(m_discreteValueEnums[i]); - } + return true; } } + return false; } - else + + return ( + (!includeReadAccess) || + (this->enumAdvanceLevel(m_discreteValueEnums[enumIndex]) <= readAccessLevel)); +} + +std::vector ValueItemDefinition::relevantEnums( + bool includeCategories, + const std::set& testCategories, + bool includeReadAccess, + unsigned int readAccessLevel) const +{ + std::vector result; + if (!(includeCategories || includeReadAccess)) { - for (std::size_t i = 0; i < m_discreteValueEnums.size(); i++) + return m_discreteValueEnums; + } + + for (std::size_t i = 0; i < m_discreteValueEnums.size(); i++) + { + if (this->defaultIsEnumRelevant( + i, includeCategories, testCategories, includeReadAccess, readAccessLevel)) { - if (this->enumAdvanceLevel(m_discreteValueEnums[i]) <= readAccessLevel) - { - result.push_back(m_discreteValueEnums[i]); - } + result.push_back(m_discreteValueEnums[i]); } } return result; diff --git a/smtk/attribute/ValueItemDefinition.h b/smtk/attribute/ValueItemDefinition.h index 9e01ca333..155fb8f61 100644 --- a/smtk/attribute/ValueItemDefinition.h +++ b/smtk/attribute/ValueItemDefinition.h @@ -45,6 +45,16 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti ValueItemDefinition(const std::string& myname); ~ValueItemDefinition() override; + ///\brief Typedef for custom relevance functions + typedef std::function& testCategories, + bool includeReadAccess, + unsigned int readAccessLevel)> + EnumRelevanceFunc; + const std::string& units() const { return m_units; } virtual bool setUnits(const std::string& newUnits); @@ -194,6 +204,12 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti bool addConditionalItem(const std::string& enumValue, const std::string& itemName); std::vector conditionalItems(const std::string& enumValue) const; + ///@{ + ///\brief Set and Get Methods for specifying a custom isEnumRelevant function + void setCustomEnumIsRelevant(EnumRelevanceFunc func) { m_customEnumIsRelevant = func; } + EnumRelevanceFunc customEnumIsRelevant() const { return m_customEnumIsRelevant; } + ///@} + ///\brief Return the enum strings that pass a set of categories and/or specified advance read access level. std::vector relevantEnums( bool includeCategories, @@ -201,6 +217,18 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti bool includeReadAccess, unsigned int readAccessLevel) const; + ///\brief Default isRelevant method that returns true if an Enum of the Definition is relevant. + /// + /// If includeCatagories is true and the Enum does not pass it's category checks, then return false, + /// If includeReadAccess is true, and the Enum's advanceLevel is > readAccessLevel then return false. + /// Else return true. + virtual bool defaultIsEnumRelevant( + int enumIndex, + bool includeCatagories, + const std::set& testCategories, + bool includeReadAccess, + unsigned int readAccessLevel) const; + protected: void copyTo(ValueItemDefinitionPtr def, smtk::attribute::ItemDefinition::CopyInfo& info) const; void applyCategories( @@ -229,6 +257,7 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti std::map> m_valueToItemAssociations; std::map m_valueToCategoryAssociations; std::map m_valueToAdvanceLevelAssociations; + EnumRelevanceFunc m_customEnumIsRelevant = nullptr; private: }; diff --git a/smtk/attribute/pybind11/PybindItem.h b/smtk/attribute/pybind11/PybindItem.h index 35974b1bb..fff509219 100644 --- a/smtk/attribute/pybind11/PybindItem.h +++ b/smtk/attribute/pybind11/PybindItem.h @@ -32,6 +32,8 @@ inline PySharedPtrClass< smtk::attribute::Item > pybind11_init_smtk_attribute_It .def("name", &smtk::attribute::Item::name) .def("label", &smtk::attribute::Item::label) .def("type", &smtk::attribute::Item::type) + .def("customIsRelevant", &smtk::attribute::Item::customIsRelevant) + .def("setCustomIsRelevant", &smtk::attribute::Item::setCustomIsRelevant, py::arg("customIsRelevantFunc")) .def("isRelevant", &smtk::attribute::Item::isRelevant, py::arg("includeCategoryChecking") = true, py::arg("includeReadAccess") = true, py::arg("readAccessLevel") = 0) .def("defaultIsRelevant", &smtk::attribute::Item::defaultIsRelevant, py::arg("includeCategoryChecking"), py::arg("includeReadAccess"), py::arg("readAccessLevel")) .def("isValid", (bool (smtk::attribute::Item::*)(bool) const) &smtk::attribute::Item::isValid, py::arg("useActiveCategories") = true) diff --git a/smtk/attribute/pybind11/PybindValueItemDefinition.h b/smtk/attribute/pybind11/PybindValueItemDefinition.h index e629cdce3..c63837734 100644 --- a/smtk/attribute/pybind11/PybindValueItemDefinition.h +++ b/smtk/attribute/pybind11/PybindValueItemDefinition.h @@ -43,6 +43,9 @@ inline PySharedPtrClass< smtk::attribute::ValueItemDefinition, smtk::attribute:: .def("enumAdvanceLevel", &smtk::attribute::ValueItemDefinition::enumAdvanceLevel, py::arg("enumValue")) .def("hasEnumAdvanceLevel", &smtk::attribute::ValueItemDefinition::hasEnumAdvanceLevel, py::arg("enumValue")) .def("enumAdvanceLevelInfo", &smtk::attribute::ValueItemDefinition::enumAdvanceLevelInfo) + .def("customEnumIsRelevant", &smtk::attribute::ValueItemDefinition::customEnumIsRelevant) + .def("setCustomEnumIsRelevant", &smtk::attribute::ValueItemDefinition::setCustomEnumIsRelevant, py::arg("customEnumIsRelevantFunc")) + .def("relevantEnums", &smtk::attribute::ValueItemDefinition::relevantEnums, py::arg("includeCategories"), py::arg("testCategories"), py::arg("includeReadAccess"), py::arg("readAccessLevel")) .def("expressionDefinition", &smtk::attribute::ValueItemDefinition::expressionDefinition) .def("hasChildItemDefinition", (bool (smtk::attribute::ValueItemDefinition::*)(::std::string const &) const) &smtk::attribute::ValueItemDefinition::hasChildItemDefinition, py::arg("itemName")) .def("hasChildItemDefinition", (bool (smtk::attribute::ValueItemDefinition::*)(::std::string const &, ::std::string const &)) &smtk::attribute::ValueItemDefinition::hasChildItemDefinition, py::arg("valueName"), py::arg("itemName")) diff --git a/smtk/attribute/testing/cxx/customIsRelevantTest.cxx b/smtk/attribute/testing/cxx/customIsRelevantTest.cxx index 3f5a3990e..d27bb7579 100644 --- a/smtk/attribute/testing/cxx/customIsRelevantTest.cxx +++ b/smtk/attribute/testing/cxx/customIsRelevantTest.cxx @@ -27,7 +27,7 @@ bool customIsRelevant( bool includeReadAccess, unsigned int readAccessLevel) { - // This function will add an extra relevancy constraint indicating that + // This function will add an extra relevance constraint indicating that // this item is relevant only if item b's value is between 0 and 10 inclusive // find Item b @@ -55,6 +55,44 @@ bool customIsRelevant( return item->defaultIsRelevant(includeCategories, includeReadAccess, readAccessLevel); } +bool customEnumIsRelevant( + const Item* item, + int enumIndex, + bool includeCategories, + const std::set& testCategories, + bool includeReadAccess, + unsigned int readAccessLevel) +{ + // This function will add an extra relevance constraint indicating that + // the "alpha" value is relevant only if item b's value is between 0 and 10 inclusive + + const auto def = std::dynamic_pointer_cast(item->definition()); + if (enumIndex == 0) // Alpha's index + { + // find Item b + auto att = item->attribute(); + if (att) + { + auto itemB = att->findAs("b"); + if (itemB) + { + if (!itemB->isSet()) + { + // Item b has not value + return false; + } + double val = itemB->value(); + if ((val < 0) || (val > 10)) + { + return false; + } + } + } + } + return def->defaultIsEnumRelevant( + enumIndex, includeCategories, testCategories, includeReadAccess, readAccessLevel); +} + int main() { int status = 0; @@ -66,6 +104,11 @@ int main() smtk::attribute::DefinitionPtr attDef = resource->createDefinition("Test"); auto adef = attDef->addItemDefinition("a"); auto bdef = attDef->addItemDefinition("b"); + auto cdef = attDef->addItemDefinition("c"); + + // Lets set some discrete values for Item c + cdef->addDiscreteValue(1, "alpha"); + cdef->addDiscreteValue(2, "beta"); auto att = resource->createAttribute("testAtt", attDef); if (att) @@ -90,7 +133,9 @@ int main() status = -1; } - for (int i = 0; i < 2; i++) + // Lets grab the discrete item + auto discreteItem = std::dynamic_pointer_cast(att->item(2)); + for (int i = 0; i < 3; i++) { if (att->item(i)->isRelevant(false, false)) { @@ -103,10 +148,33 @@ int main() } } - // Now lets give Item a the custom relevancy function + // Lets test the discrete Item's enums - if we ignore categories and read access there should be 2 + auto relevantEnums = discreteItem->relevantEnums(false, false, 0); + if (relevantEnums.size() != 2) + { + std::cerr << "ERROR: Initial Test: Item " << discreteItem->name() + << " has incorrect relevant enums values : "; + status = -1; + } + else + { + std::cerr << "Initial Test: Item " << discreteItem->name() + << " has correct relevant enum values : "; + } + + for (const auto& estring : relevantEnums) + { + std::cerr << estring << " "; + } + std::cerr << std::endl; + + // Now lets give Item a the custom relevance function att->item(0)->setCustomIsRelevant(customIsRelevant); - // Since b is not currently set, a should not be relevant + // Let give Item c's definition a custom enum relevance function + cdef->setCustomEnumIsRelevant(customEnumIsRelevant); + + // Since b is not currently set, a should not be relevant and discreteItem should have only one relevant enum if (!att->item(0)->isRelevant(false, false)) { std::cerr << "Pass Item b not set: Item " << att->item(0)->name() << " is not Relevant\n"; @@ -117,7 +185,26 @@ int main() status = -1; } - // now lets set b to 5 which should make a relevant + relevantEnums = discreteItem->relevantEnums(false, false, 0); + if (!((relevantEnums.size() == 1) && (relevantEnums[0] == "beta"))) + { + std::cerr << "ERROR: Item b not set: Item " << discreteItem->name() + << " has incorrect relevant enums values : "; + status = -1; + } + else + { + std::cerr << "Initial Item b not set: Item " << discreteItem->name() + << " has correct relevant enum values : "; + } + + for (const auto& estring : relevantEnums) + { + std::cerr << estring << " "; + } + std::cerr << std::endl; + + // now lets set b to 5 which should make a relevant as well as both of the discreteItem's enums auto itemB = att->findAs("b"); itemB->setValue(5.0); @@ -132,5 +219,24 @@ int main() status = -1; } + relevantEnums = discreteItem->relevantEnums(false, false, 0); + if (relevantEnums.size() != 2) + { + std::cerr << "ERROR: Item b set to 5: Item " << discreteItem->name() + << " has incorrect relevant enums values : "; + status = -1; + } + else + { + std::cerr << "Initial Item b set to 5: Item " << discreteItem->name() + << " has correct relevant enum values : "; + } + + for (const auto& estring : relevantEnums) + { + std::cerr << estring << " "; + } + std::cerr << std::endl; + return status; } From eed8ecf2d48d926bdef37a9c8c53a5b92bc92fc7 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Mon, 1 Jul 2024 10:15:13 -0400 Subject: [PATCH 18/42] BUG: Adding Missing cassert include --- smtk/common/Links.h | 1 + 1 file changed, 1 insertion(+) diff --git a/smtk/common/Links.h b/smtk/common/Links.h index c8eea5685..3170736f2 100644 --- a/smtk/common/Links.h +++ b/smtk/common/Links.h @@ -21,6 +21,7 @@ SMTK_THIRDPARTY_PRE_INCLUDE #include SMTK_THIRDPARTY_POST_INCLUDE +#include #include #include #include From 0ccb188c4482e966636565a3bc48e5096a22ae6b Mon Sep 17 00:00:00 2001 From: David Thompson Date: Tue, 2 Jul 2024 14:06:17 -0400 Subject: [PATCH 19/42] Fix #540 by using `units::PreferredUnits`. --- doc/release/notes/qt-unit-suggestions.rst | 10 ++++ smtk/extension/qt/qtDoubleUnitsLineEdit.cxx | 59 +++++++++++++++------ 2 files changed, 52 insertions(+), 17 deletions(-) create mode 100644 doc/release/notes/qt-unit-suggestions.rst diff --git a/doc/release/notes/qt-unit-suggestions.rst b/doc/release/notes/qt-unit-suggestions.rst new file mode 100644 index 000000000..6d03706d5 --- /dev/null +++ b/doc/release/notes/qt-unit-suggestions.rst @@ -0,0 +1,10 @@ +Qt extensions +============= + +Double-item with unit completion +-------------------------------- + +The ``qtDoubleUnitsLineEdit.cxx`` class now uses the unit-library's +``PreferredUnits`` class to suggest unit completions. This allows +workflow developers to provide a tailored list of suggested completions +rather than listing every available compatible unit. diff --git a/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx b/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx index d0f5abbd0..da2f75a28 100644 --- a/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx +++ b/smtk/extension/qt/qtDoubleUnitsLineEdit.cxx @@ -27,6 +27,7 @@ #include #include "units/Measurement.h" +#include "units/PreferredUnits.h" #include // std::sort et al #include @@ -166,15 +167,15 @@ qtDoubleUnitsLineEdit* qtDoubleUnitsLineEdit::checkAndCreate( } // Get units system - auto unitsSystem = dItem->definition()->unitsSystem(); - if (unitsSystem == nullptr) + auto unitSystem = dItem->definition()->unitsSystem(); + if (unitSystem == nullptr) { return nullptr; } // Try parsing the unit string bool parsedOK = false; - auto unit = unitsSystem->unit(dUnits, &parsedOK); + auto unit = unitSystem->unit(dUnits, &parsedOK); if (!parsedOK) { qWarning() << "Ignoring unrecognized units \"" << dUnits.c_str() << "\"" @@ -301,11 +302,18 @@ void qtDoubleUnitsLineEdit::onTextEdited() auto dItem = m_inputsItem->itemAs(); auto dUnits = dItem->units(); - auto unitsSystem = dItem->definition()->unitsSystem(); + auto unitSystem = dItem->definition()->unitsSystem(); // Parsing the Item's unit string bool parsedOK = false; - auto unit = unitsSystem->unit(dUnits, &parsedOK); + auto unit = unitSystem->unit(dUnits, &parsedOK); + + std::shared_ptr preferred; + auto cit = unitSystem->m_unitContexts.find(unitSystem->m_activeUnitContext); + if (cit != unitSystem->m_unitContexts.end()) + { + preferred = cit->second; + } QString text = this->text(); auto utext = text.toStdString(); @@ -331,20 +339,37 @@ void qtDoubleUnitsLineEdit::onTextEdited() } // Get list of compatible units - auto compatibleUnits = unitsSystem->compatibleUnits(unit); + auto compatibleUnits = unitSystem->compatibleUnits(unit); QStringList unitChoices; - // create a list of possible units names - for (const auto& unit : compatibleUnits) + // Create a list of possible units names: + if (preferred) { - unitChoices.push_back(unit.name().c_str()); + // We have a context; this provides preferred units as a + // developer-ordered list of units, placing dUnits at + // position 0 of the suggestions. + units::CompatibleUnitOptions opts; + opts.m_inputUnitPriority = 0; + for (const auto& suggestion : preferred->suggestedUnits(dUnits, opts)) + { + unitChoices.push_back(suggestion.c_str()); + } + } + else + { + // We don't have a unit-system context; just + // find compatible units. This may present + // duplicates and is definitely not sorted. + for (const auto& unit : compatibleUnits) + { + unitChoices.push_back(unit.name().c_str()); + } + // Lets remove duplicates and sort the list + unitChoices.removeDuplicates(); + unitChoices.sort(); + // Now make the Item's units appear at the top + unitChoices.push_front(dUnits.c_str()); } - - // Lets remove duplicates and sort the list - unitChoices.removeDuplicates(); - unitChoices.sort(); - // Now make the Item's units appear at the top - unitChoices.push_front(dUnits.c_str()); // Generate the completer strings for (const QString& unit : unitChoices) @@ -363,7 +388,7 @@ void qtDoubleUnitsLineEdit::onTextEdited() // Update background based on current input string parsedOK = false; - auto measurement = unitsSystem->measurement(utext, &parsedOK); + auto measurement = unitSystem->measurement(utext, &parsedOK); if (!parsedOK) { QColor invalidColor = m_inputsItem->uiManager()->correctedTempInvalidValueColor(); @@ -396,7 +421,7 @@ void qtDoubleUnitsLineEdit::onTextEdited() } else { - units::Measurement convertedMsmt = unitsSystem->convert(measurement, unit, &converted); + units::Measurement convertedMsmt = unitSystem->convert(measurement, unit, &converted); if (!converted) { std::ostringstream ss; From 57b3100491adc3c5af2f73783b7f6be520dca4c7 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Fri, 5 Jul 2024 13:47:46 -0400 Subject: [PATCH 20/42] BUG: Adding Issues with Copying Attribute Resources 1. Attribute Resource Associations are now copied in ``copyFinalized()`` 2. Attribute Active Categories are now copied in ``copyInitialized()`` 3. Attribute Resource Analyses are now copied in ``copyInitialized()`` Also added assignment method to Analyses class. 4. Properties on Resources (in particular with Attribute and Graph Resources are now copied) The issue was that source and copied resource uuids were not being added to the object mapping in their clone methods. **Note: All Resources that can be cloned will need to do this.** 5. Removed undefinedRole as the default role type when creating links. This lead to mistakes where specified roles were not being used when creating between resources. This role type has been removed. Resource based Links now use the same value to represent an Link with an invalid role type. Also by default, links with invalid role types will no longer be copied when a resource is copied. smtk/resource/testing/python/testCloneResources.py has been updated to verify these changes. Also replaced all std::cerr and std::cout statements in smtk/attribute/Resource.cxx with Logger messages. --- .../attribute_collection/cloneTest.smtk | 4 +- doc/release/notes/attributeChanges.rst | 7 +++ doc/release/notes/changedToLinks.rst | 11 ++++ smtk/attribute/Analyses.cxx | 58 +++++++++++++++++-- smtk/attribute/Analyses.h | 20 +++++-- smtk/attribute/Resource.cxx | 57 ++++++++++++++---- smtk/attribute/pybind11/PybindResource.h | 2 +- .../cxx/basicAttributeXMLWriterTest.cxx | 2 +- smtk/common/Links.h | 25 ++------ smtk/common/testing/cxx/UnitTestLinks.cxx | 23 ++++---- smtk/graph/Resource.h | 4 ++ smtk/io/XmlDocV3Parser.cxx | 6 +- smtk/resource/Links.cxx | 7 ++- smtk/resource/Links.h | 7 ++- smtk/resource/Resource.cxx | 7 ++- .../testing/python/testCloneResources.py | 51 ++++++++++++++++ 16 files changed, 225 insertions(+), 66 deletions(-) create mode 100644 doc/release/notes/changedToLinks.rst diff --git a/data/attribute/attribute_collection/cloneTest.smtk b/data/attribute/attribute_collection/cloneTest.smtk index 7c851f2c9..64fd6ffb4 100644 --- a/data/attribute/attribute_collection/cloneTest.smtk +++ b/data/attribute/attribute_collection/cloneTest.smtk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20119083672e22e48d89862ad00ddeb258d0156645663f4407232aa521b13b35 -size 6153 +oid sha256:6a13051c24999b3c031029f1a87c186bf729b7a010fad2fc83a1f9136f74917b +size 6135 diff --git a/doc/release/notes/attributeChanges.rst b/doc/release/notes/attributeChanges.rst index 026633943..c41f49868 100644 --- a/doc/release/notes/attributeChanges.rst +++ b/doc/release/notes/attributeChanges.rst @@ -1,7 +1,14 @@ Changes to Attribute Resource ============================= + Added `attribute::Resource::hasDefinition(type)` Method ------------------------------------------------------ This method will return true if the resource already contains a Definition with the requested type. + +Added `attribute::Analyses assignment method +-------------------------------------------- + +You can now assign the contents of one Analyses instance to another. This will create copies +of all of the Analysis instances owned by the source Analyses. diff --git a/doc/release/notes/changedToLinks.rst b/doc/release/notes/changedToLinks.rst new file mode 100644 index 000000000..c0904b6f7 --- /dev/null +++ b/doc/release/notes/changedToLinks.rst @@ -0,0 +1,11 @@ +Changes to SMTK Common +====================== + +Removing Default Role Type for Links +------------------------------------ + +Links will no longer have a default role type of undefinedRole since this lead to issues where the specified role was not being used. +This role type has been removed. + +Resource based Links now use the same value to represent an Link with an invalid role type. +Also by default, links with invalid role types will no longer be copied when a resource is copied. diff --git a/smtk/attribute/Analyses.cxx b/smtk/attribute/Analyses.cxx index 955ca498f..c46da3989 100644 --- a/smtk/attribute/Analyses.cxx +++ b/smtk/attribute/Analyses.cxx @@ -18,6 +18,8 @@ #include "smtk/attribute/VoidItem.h" #include "smtk/attribute/VoidItemDefinition.h" +#include "smtk/io/Logger.h" + using namespace smtk::attribute; namespace @@ -147,8 +149,10 @@ void Analyses::Analysis::buildAnalysisItem(StringItemDefinitionPtr& pitem) const if (m_required) { - std::cerr << "Warning: Analysis: " << m_name << " is marked Required but is part of an analysis" - << " whose children are Exclusive - The Required property will be ignored!\n"; + smtkWarningMacro( + smtk::io::Logger::instance(), + "Analysis: " << m_name << " is marked Required but is part of an analysis" + << " whose children are Exclusive - The Required property will be ignored!"); } if (m_exclusive) { @@ -171,7 +175,18 @@ void Analyses::Analysis::buildAnalysisItem(StringItemDefinitionPtr& pitem) const } } -Analyses::~Analyses() +Analyses::Analysis& Analyses::Analysis::operator=(const Analyses::Analysis& src) +{ + // assign the src information with the exception of name and parent/child information to this + this->m_exclusive = src.m_exclusive; + this->m_required = src.m_required; + this->m_label = src.m_label; + this->m_categories = src.m_categories; + + return *this; +} + +void Analyses::clear() { // Delete all analyses contained for (auto it = m_analyses.begin(); it != m_analyses.end(); ++it) @@ -181,6 +196,11 @@ Analyses::~Analyses() m_analyses.clear(); } +Analyses::~Analyses() +{ + this->clear(); +} + Analyses::Analysis* Analyses::create(const std::string& name) { // Let first see if we already have one with that name if so @@ -319,7 +339,7 @@ void Analyses::getAnalysisItemCategories( } else { - std::cerr << "Could not find Analysis: " << item->name() << std::endl; + smtkErrorMacro(smtk::io::Logger::instance(), "Could not find Analysis: " << item->name()); } } @@ -343,7 +363,7 @@ void Analyses::getAnalysisItemCategories( } else { - std::cerr << "Could not find Analysis: " << sitem->value() << std::endl; + smtkErrorMacro(smtk::io::Logger::instance(), "Could not find Analysis: " << sitem->value()); } // Lets check its active children int i, n = static_cast(sitem->numberOfActiveChildrenItems()); @@ -384,3 +404,31 @@ std::set Analyses::getAnalysisAttributeCategories(ConstAttributePtr this->getAnalysisAttributeCategories(attribute, cats); return cats; } + +Analyses& Analyses::operator=(const Analyses& src) +{ + this->clear(); + + std::map aMap; + + m_topLevelExclusive = src.m_topLevelExclusive; + + // Create a copy each analysis from the source + for (const auto* srcAnal : src.m_analyses) + { + auto* copyAnal = this->create(srcAnal->name()); + *copyAnal = *srcAnal; + aMap[srcAnal] = copyAnal; + } + + // Now iterate again and add the parent information which will also update the children info + std::size_t i, n = m_analyses.size(); + for (i = 0; i < n; i++) + { + if (src.m_analyses[i]->parent()) + { + this->m_analyses[i]->setParent(aMap[src.m_analyses[i]->parent()]); + } + } + return *this; +} diff --git a/smtk/attribute/Analyses.h b/smtk/attribute/Analyses.h index 558b700f8..ba619aed4 100644 --- a/smtk/attribute/Analyses.h +++ b/smtk/attribute/Analyses.h @@ -94,6 +94,9 @@ class SMTKCORE_EXPORT Analyses void buildAnalysisItem(smtk::attribute::StringItemDefinitionPtr& sitem) const; /// @} + ///\brief copy all but the parent children information of an existing analysis + Analysis& operator=(const Analysis& src); + protected: Analysis(const std::string& name) : m_name(name) @@ -112,6 +115,10 @@ class SMTKCORE_EXPORT Analyses /// \brief Basic constructor - Note that by default top level Analyses are not Exclusive Analyses() = default; + + ///\brief Destroys the Instance and deletes all Analysis Instances contained within. + ~Analyses(); + /// \brief Create a new Analysis and return it. /// Note that the name must be unique with respects to the other Analysis Instances defined within /// this Instance. If the name is not unique no Analysis is created and nullptr is returned. @@ -129,16 +136,21 @@ class SMTKCORE_EXPORT Analyses /// \brief Return the number of Analysis Instances std::size_t size() const { return m_analyses.size(); } + ///\brief Assign the contents of an Analyses object to this one + /// + /// This will copy all of the analysis objects contained in src. + Analyses& operator=(const Analyses& src); + /// @{ /// \brief Methods to set and retrieve the exclusivity property pertaining the top level Analysis Instances. /// /// This property behaves similarly to an Analysis's Exclusive property. If true, then only one of the top level - /// Analysis Instances can be choosen. Else any combination of top level analysis instances are allowed. + /// Analysis instances can be chosen. Else any combination of top level analysis instances are allowed. void setTopLevelExclusive(bool mode) { m_topLevelExclusive = mode; } bool areTopLevelExclusive() const { return m_topLevelExclusive; } /// @} - /// \brief Convience method that set's an Analysis' Parent by using their names. + /// \brief Convenience method that set's an Analysis' Parent by using their names. /// /// If neither name corresponds to an existing Analysis then the method returns false. bool setAnalysisParent(const std::string& analysis, const std::string& parent); @@ -156,8 +168,8 @@ class SMTKCORE_EXPORT Analyses std::set getAnalysisAttributeCategories( smtk::attribute::ConstAttributePtr attribute); - /// \brief Destroys the Instance and deletes all Analysis Instances contained within. - ~Analyses(); + /// \brief Deletes all Analysis Instances contained within. + void clear(); protected: /// \brief Calculate the set of categories associated with an Analysis Attribute's Item. diff --git a/smtk/attribute/Resource.cxx b/smtk/attribute/Resource.cxx index d628fd6cd..1a1b2e455 100644 --- a/smtk/attribute/Resource.cxx +++ b/smtk/attribute/Resource.cxx @@ -149,9 +149,9 @@ bool Resource::removeDefinition(DefinitionPtr def) { if (!def || def->resource() != shared_from_this()) { - std::cerr << "ERROR: " << def->type() - << " does not belong to the specified" - " attribute Resource!"; + smtkErrorMacro( + smtk::io::Logger::instance(), + def->type() << " does not belong to the specified attribute Resource!"); return false; } @@ -159,8 +159,9 @@ bool Resource::removeDefinition(DefinitionPtr def) const bool hasChildren = childrenIt != m_derivedDefInfo.cend() && !(*childrenIt).second.empty(); if (hasChildren) { - std::cerr << "ERROR: Removing base Definition instances is not" - " supported!\n"; + smtkErrorMacro( + smtk::io::Logger::instance(), + "Can not delete " << def->type() << " since it is the basis for other definitions!"); return false; } @@ -339,7 +340,8 @@ bool Resource::removeAttribute(smtk::attribute::AttributePtr att) if (m_attributes.find(att->name()) == m_attributes.end()) { - std::cerr << "Resource doesn't have attribute named: " << att->name() << std::endl; + smtkErrorMacro( + smtk::io::Logger::instance(), "Resource doesn't have attribute named: " << att->name()); return false; } m_attributes.erase(att->name()); @@ -713,18 +715,20 @@ smtk::attribute::DefinitionPtr Resource::copyDefinition( if (sourceExpDef) { // Attempt to Copy definition - std::cout << "Copying \"" << type << "\" definition" << std::endl; expDef = this->copyDefinition(sourceExpDef); if (!expDef) { - std::cerr << "ERROR: Unable to copy expression definition " << type - << " -- copy operation incomplete" << std::endl; + smtkErrorMacro( + smtk::io::Logger::instance(), + "Unable to copy expression definition " << type << " -- copy operation incomplete"); } } else { - std::cout << "Unable to find source expression definition " << type - << " -- assuming it is external to the Resource" << std::endl; + smtkWarningMacro( + smtk::io::Logger::instance(), + "Unable to find source expression definition " + << type << " -- assuming it is external to the Resource"); } } if (expDef) @@ -750,7 +754,8 @@ bool Resource::copyDefinitionImpl( std::string typeName = sourceDef->type(); if (this->findDefinition(typeName)) { - std::cerr << "WARNING: Will not overwrite attribute definition " << typeName << std::endl; + smtkWarningMacro( + smtk::io::Logger::instance(), "Will not overwrite attribute definition " << typeName); return false; } @@ -1201,6 +1206,10 @@ std::shared_ptr Resource::clone( return rsrc; } + // Insert the source of the original into the object mapping + // so its properties can be copied if need + options.objectMapping()[this->id()] = rsrc.get(); + if (this->isNameSet()) { rsrc->setName(this->name()); @@ -1244,6 +1253,8 @@ std::shared_ptr Resource::clone( } } + // Copy all of its analyses + rsrc->m_analyses = this->m_analyses; return rsrc; } @@ -1258,6 +1269,10 @@ bool Resource::copyInitialize( return false; } + // copy the active category information + m_activeCategoriesEnabled = source->m_activeCategoriesEnabled; + m_activeCategories = source->m_activeCategories; + std::vector allAttributes; if (options.copyComponents()) { @@ -1372,6 +1387,24 @@ bool Resource::copyFinalize( } } + // Deal with the resources directly associated with this one + auto associatedResources = sourceAttResource->associations(); + for (const auto& assoRes : associatedResources) + { + // See if this resource was copied + auto* destRes = options.targetObjectFromSourceId(assoRes->id()); + + if (destRes == nullptr) + { + // This resource was not copied so just add it to the copied attribute resource + this->associate(assoRes); + } + else + { + this->associate(destRes->shared_from_this()); + } + } + // II. Copy any other link data (not part of associations or references) // First, ensure AssociationRole and ReferenceRole are excluded. bool removeAssocLinks = options.addLinkRoleToExclude(smtk::attribute::Resource::AssociationRole); diff --git a/smtk/attribute/pybind11/PybindResource.h b/smtk/attribute/pybind11/PybindResource.h index 0d968a37d..b4c32ca08 100644 --- a/smtk/attribute/pybind11/PybindResource.h +++ b/smtk/attribute/pybind11/PybindResource.h @@ -40,7 +40,7 @@ inline PySharedPtrClass< smtk::attribute::Resource> pybind11_init_smtk_attribute .def("addStyle", &smtk::attribute::Resource::addStyle, py::arg("defTypeName"), py::arg("style")) .def("advanceLevelColor", &smtk::attribute::Resource::advanceLevelColor, py::arg("level")) .def("advanceLevels", &smtk::attribute::Resource::advanceLevels) - .def("analyses", &smtk::attribute::Resource::analyses) + .def("analyses", &smtk::attribute::Resource::analyses, py::return_value_policy::reference_internal) .def("associations", &smtk::attribute::Resource::associations) .def("associate", &smtk::attribute::Resource::associate) .def("attributes", (std::set (smtk::attribute::Resource::*)(const smtk::resource::ConstPersistentObjectPtr&) const) &smtk::attribute::Resource::attributes, py::arg("object")) diff --git a/smtk/attribute/testing/cxx/basicAttributeXMLWriterTest.cxx b/smtk/attribute/testing/cxx/basicAttributeXMLWriterTest.cxx index 8b5d85b0f..12e1ba8cb 100644 --- a/smtk/attribute/testing/cxx/basicAttributeXMLWriterTest.cxx +++ b/smtk/attribute/testing/cxx/basicAttributeXMLWriterTest.cxx @@ -59,7 +59,7 @@ int main(int argc, char* argv[]) smtk::attribute::Resource& resource(*resptr); std::cout << "Resource Created\n"; // Lets add some analyses - auto analyses = resptr->analyses(); + auto& analyses = resptr->analyses(); std::set cats; cats.insert("Flow"); cats.insert("General"); diff --git a/smtk/common/Links.h b/smtk/common/Links.h index c8eea5685..883729a52 100644 --- a/smtk/common/Links.h +++ b/smtk/common/Links.h @@ -272,27 +272,19 @@ class Links : public detail::LinkContainer insert( - base_type&&, - const id_type&, - const left_type&, - const right_type&, - const role_type& role = undefinedRole); + std::pair + insert(base_type&&, const id_type&, const left_type&, const right_type&, const role_type& role); /// If the base_type is default-constructible, this insertion method allows /// you to omit the base_type instance. A new base_type will be used and the /// left and right types are passed to the new link using move semantics. template> typename std::enable_if::value, return_value>::type - insert( - const id_type& id, - const left_type& left, - const right_type& right, - const role_type& role = undefinedRole) + insert(const id_type& id, const left_type& left, const right_type& right, const role_type& role) { return insert(std::move(base_type()), id, left, right, role); } @@ -508,15 +500,6 @@ class Links : public detail::LinkContainer -const role_type Links::undefinedRole = - std::numeric_limits::lowest(); - template< typename id_type, typename left_type, diff --git a/smtk/common/testing/cxx/UnitTestLinks.cxx b/smtk/common/testing/cxx/UnitTestLinks.cxx index 3de9fc3fa..f43c5a408 100644 --- a/smtk/common/testing/cxx/UnitTestLinks.cxx +++ b/smtk/common/testing/cxx/UnitTestLinks.cxx @@ -17,6 +17,7 @@ namespace { + struct MyBase { MyBase() = default; @@ -51,7 +52,7 @@ void UnitTest() smtkTest(links.size() == 1, "Should have 1 link."); // Try to insert another link with the same id (should fail). - inserted = links.insert(0, 2, 3); + inserted = links.insert(0, 2, 3, -100); smtkTest(inserted.second == false, "Should not be able to insert a link with the same id."); // Add a link that reuses one of the original values @@ -143,7 +144,7 @@ void UnitTestLinksEraseAll() // return true. { MyLinks links; - links.insert(MyBase("myBase1"), 0, 0, 0); + links.insert(MyBase("myBase1"), 0, 0, 0, -100); smtkTest(links.erase_all(0) == true, ""); smtkTest(links.empty(), ""); } @@ -152,7 +153,7 @@ void UnitTestLinksEraseAll() // should return false. { MyLinks links; - links.insert(MyBase("myBase1"), 1, 1, 1); + links.insert(MyBase("myBase1"), 1, 1, 1, -100); smtkTest(links.erase_all(0) == false, ""); smtkTest(links.size() == 1, ""); } @@ -161,8 +162,8 @@ void UnitTestLinksEraseAll() // Links should return false. { MyLinks links; - links.insert(MyBase("myBase1"), 0, 0, 0); - links.insert(MyBase("myBase2"), 1, 1, 1); + links.insert(MyBase("myBase1"), 0, 0, 0, -100); + links.insert(MyBase("myBase2"), 1, 1, 1, -100); smtkTest(links.erase_all(2) == false, ""); smtkTest(links.size() == 2, ""); } @@ -171,8 +172,8 @@ void UnitTestLinksEraseAll() // Links should return true. { MyLinks links; - links.insert(MyBase("myBase1"), 0, 0, 0); - links.insert(MyBase("myBase2"), 1, 1, 1); + links.insert(MyBase("myBase1"), 0, 0, 0, -100); + links.insert(MyBase("myBase2"), 1, 1, 1, -100); smtkTest(links.erase_all(1) == true, ""); smtkTest(links.size() == 1, ""); } @@ -181,9 +182,9 @@ void UnitTestLinksEraseAll() // entries present in Links should return true. { MyLinks links; - links.insert(MyBase("myBase1"), 0, 0, 0); - links.insert(MyBase("myBase2"), 1, 1, 1); - links.insert(MyBase("myBase2"), 1, 1, 1); + links.insert(MyBase("myBase1"), 0, 0, 0, -100); + links.insert(MyBase("myBase2"), 1, 1, 1, -100); + links.insert(MyBase("myBase2"), 1, 1, 1, -100); smtkTest(links.erase_all(1) == true, ""); smtkTest(links.size() == 1, ""); } @@ -227,7 +228,7 @@ void MoveOnlyTest() smtkTest(links.size() == 1, "Should have 1 link."); // Try to insert another link with the same id (should fail). - inserted = links.insert(MyMoveOnlyBase("m"), 0, 2, 3); + inserted = links.insert(MyMoveOnlyBase("m"), 0, 2, 3, -100); smtkTest(inserted.second == false, "Should not be able to insert a link with the same id."); // Add a link that reuses one of the original values diff --git a/smtk/graph/Resource.h b/smtk/graph/Resource.h index df1051d72..8a6e6cde1 100644 --- a/smtk/graph/Resource.h +++ b/smtk/graph/Resource.h @@ -463,6 +463,10 @@ std::shared_ptr Resource::clone( return result; } + // Insert the source of the original into the object mapping + // so its properties can be copied if need + options.objectMapping()[this->id()] = result.get(); + if (this->isNameSet()) { result->setName(this->name()); diff --git a/smtk/io/XmlDocV3Parser.cxx b/smtk/io/XmlDocV3Parser.cxx index 059ee6372..3a1b775e0 100644 --- a/smtk/io/XmlDocV3Parser.cxx +++ b/smtk/io/XmlDocV3Parser.cxx @@ -793,7 +793,8 @@ void XmlDocV3Parser::processReferenceItem(pugi::xml_node& node, ReferenceItemPtr surrogateIndex, surrogateTypeName, surrogateId, surrogateLocation), key.first, item->attribute()->resource()->id(), - rhs1); + rhs1, + role); } links.value(key.first).insert(key.second, item->attribute()->id(), rhs2, role); @@ -836,7 +837,8 @@ void XmlDocV3Parser::processReferenceItem(pugi::xml_node& node, ReferenceItemPtr surrogateIndex, surrogateTypeName, surrogateId, surrogateLocation), key.first, item->attribute()->resource()->id(), - rhs1); + rhs1, + role); } links.value(key.first).insert(key.second, item->attribute()->id(), rhs2, role); diff --git a/smtk/resource/Links.cxx b/smtk/resource/Links.cxx index 583322a85..31bcf2b9a 100644 --- a/smtk/resource/Links.cxx +++ b/smtk/resource/Links.cxx @@ -26,6 +26,7 @@ namespace smtk { namespace resource { + bool Links::isLinkedTo(const ResourcePtr& rhs1, const RoleType& role) const { return this->isLinkedTo( @@ -198,7 +199,7 @@ Links::Key Links::addLinkTo( } resourceLinkData.insert( - ResourceLinkData::LinkBase(rhs1), resourceLinkId, lhs1->id(), rhs1->id()); + ResourceLinkData::LinkBase(rhs1), resourceLinkId, lhs1->id(), rhs1->id(), role); componentLinkData = &resourceLinkData.value(resourceLinkId); } else @@ -406,7 +407,7 @@ std::pair Links::linkedObjectAndRole( const ResourceLinkData& resourceLinkData = lhs1->links().data(); if (!resourceLinkData.contains(key.first)) { - return std::make_pair(ResourcePtr(), Component::Links::Data::undefinedRole); + return std::make_pair(ResourcePtr(), invalidRoleType()); } const auto& resourceLink = resourceLinkData.value(key.first); @@ -445,7 +446,7 @@ std::pair Links::linkedObjectIdAndRole( const ResourceLinkData& resourceLinkData = lhs1->links().data(); if (!resourceLinkData.contains(key.first)) { - return std::make_pair(smtk::common::UUID::null(), Component::Links::Data::undefinedRole); + return std::make_pair(smtk::common::UUID::null(), invalidRoleType()); } const auto& resourceLink = resourceLinkData.value(key.first); diff --git a/smtk/resource/Links.h b/smtk/resource/Links.h index 1aa547863..7df2fe69c 100644 --- a/smtk/resource/Links.h +++ b/smtk/resource/Links.h @@ -15,6 +15,8 @@ #include "smtk/PublicPointerDefs.h" #include "smtk/common/UUID.h" +#include + namespace smtk { namespace resource @@ -34,6 +36,9 @@ class SMTKCORE_EXPORT Links typedef std::pair Key; typedef int RoleType; + /// Special RoleType indicating that the link is invalid + static RoleType invalidRoleType() { return std::numeric_limits::lowest(); } + /// Given a resource or component and a role, check if a link of this role type /// exists between us and the input object. bool isLinkedTo(const ResourcePtr&, const RoleType&) const; @@ -41,7 +46,7 @@ class SMTKCORE_EXPORT Links /// Given a resource or component and a role type, construct a link from /// us to the input object and assign the link a random UUID. Return a key - /// that uniquely identifies the link if successfull, or return a key + /// that uniquely identifies the link if successful, or return a key /// comprised of a pair of null UUIDs if the link construction failed. Key addLinkTo(const ResourcePtr&, const RoleType&); Key addLinkTo(const ComponentPtr&, const RoleType&); diff --git a/smtk/resource/Resource.cxx b/smtk/resource/Resource.cxx index 9b3ef7f67..8b2323963 100644 --- a/smtk/resource/Resource.cxx +++ b/smtk/resource/Resource.cxx @@ -316,7 +316,9 @@ void Resource::copyLinks(const std::shared_ptr& rsrc, const Copy { if ( options.shouldExcludeLinksInRole(sourceLink.role) || options.shouldOmitId(sourceLink.left) || - options.shouldOmitId(sourceLink.right) || options.shouldOmitId(sourceLink.id)) + options.shouldOmitId(sourceLink.right) || options.shouldOmitId(sourceLink.id) || + // Ignore links with invalid roles + sourceLink.role == Links::invalidRoleType()) { continue; } @@ -359,7 +361,6 @@ void Resource::copyProperties(const std::shared_ptr& rsrc, CopyO for (const auto& sourceEntry : sourceMap) { auto it = targetMap.find(sourceEntry.first); - std::cout << "Copy " << sourceEntry.first << " data\n"; if (it == targetMap.end()) { smtkErrorMacro(options.log(), "No matching \"" << sourceEntry.first << "\" property data."); @@ -386,7 +387,7 @@ void Resource::copyProperties(const std::shared_ptr& rsrc, CopyO } smtk::common::UUID targetId; - if (auto* targetObj = options.targetObjectFromSourceId(sourceId)) + if (auto* targetObj = options.targetObjectFromSourceId(sourceId)) { targetId = targetObj->id(); } diff --git a/smtk/resource/testing/python/testCloneResources.py b/smtk/resource/testing/python/testCloneResources.py index 7ecbbf00c..315cd48aa 100644 --- a/smtk/resource/testing/python/testCloneResources.py +++ b/smtk/resource/testing/python/testCloneResources.py @@ -36,6 +36,7 @@ def setUp(self): smtk.testing.DATA_DIR, 'attribute', 'attribute_collection', 'cloneTest.smtk') rr = smtk.read(attFilename) self.origAttResource = rr[0] + self.origAttResource.setName('attResource') # Read in the original markup resource markupFilename = os.path.join( smtk.testing.DATA_DIR, 'model', '3d', 'smtk', 'coarse-knee.smtk') @@ -44,12 +45,21 @@ def setUp(self): # add a resource property to xx to test that it gets copied # even when component properties are not. self.origMarkupResource.stringProperties().set('foo', 'bar') + self.origAttResource.stringProperties().set('foo', 'bar') + + # Let's associate the markup resource to the attribute resource + self.origAttResource.associate(self.origMarkupResource) compset = self.origMarkupResource.filter( 'smtk::markup::UnstructuredData') att = self.origAttResource.findAttribute("Test Attribute") refitem = att.associations() refitem.setValue(0, compset.pop()) + # The original attribute did not have any active categories or analyses so let's add some + self.origAttResource.setActiveCategories({"A", "B"}) + self.origAttResource.setActiveCategoriesEnabled(True) + analysis = self.origAttResource.analyses().create("foo") + analysis.setLocalCategories({"A"}) print('------- Setup Results --------') print(' +++++ Original Attribute Resource ++++++') self.printResource(self.origAttResource) @@ -72,6 +82,15 @@ def printResource(self, resource): self.printAttInfo(resource) def printAttInfo(self, resource): + # Print the number of resources directly associated with it + print(' number of associated resources:', + len(resource.associations())) + # Print the number of analyses + print(' number of analyses:', resource.analyses().size()) + # print the active categories + print(' active categories:', resource.activeCategories()) + print(' active categories enabled:', + resource.activeCategoriesEnabled()) # Find the test attribute att = resource.findAttribute("Test Attribute") refitem = att.associations() @@ -139,6 +158,22 @@ def testCloneAllResource(self): self.compareClonedAttResource(clonedAtts, 0) def compareClonedAttResource(self, attRes, sameMarkup): + if attRes.stringProperties().contains('foo'): + if attRes.stringProperties().at('foo') != 'bar': + raise RuntimeError( + 'Copied Attribute Resource do not have a string property foo equal to bar') + else: + raise RuntimeError( + 'Copied Attribute Resource do not have a string property foo') + if attRes.activeCategoriesEnabled() != self.origAttResource.activeCategoriesEnabled(): + raise RuntimeError( + 'Attribute Resources do not have the same active categories enabled option') + if attRes.activeCategories() != self.origAttResource.activeCategories(): + raise RuntimeError( + 'Attribute Resources do not have the same active categories') + if attRes.analyses().size() != self.origAttResource.analyses().size(): + raise RuntimeError( + 'Attribute Resources do not have the same number of analyses') if attRes.id() == self.origAttResource.id(): raise RuntimeError('Attribute Resources have same ID') @@ -157,13 +192,29 @@ def compareClonedAttResource(self, attRes, sameMarkup): if d1.expression().id() == origD1.expression().id(): raise RuntimeError('Items d1 do the same expression attribute') + attResAsso = attRes.associations() + origAttResAsso = self.origAttResource.associations() + # The copy should have the same number of associates resources as the source + if len(attResAsso) != len(origAttResAsso): + raise RuntimeError( + 'Copied Attribute Resources has incorrect number of associated resources') + attAsso = att.associations() origAttAsso = origAtt.associations() + if bool(sameMarkup): + if len(attResAsso): + if not (self.origMarkupResource in attResAsso): + raise RuntimeError( + 'Copied Attribute Resource is not associated to the origin MarkUp Resource and should be') if attAsso.value(0).id() != origAttAsso.value(0).id(): raise RuntimeError( 'Attributes are not associated with the same component and should be') else: + if len(attResAsso): + if self.origMarkupResource in attResAsso: + raise RuntimeError( + 'Copied Attribute Resource is associated to the origin MarkUp Resource and not should be') if attAsso.value(0).id() == origAttAsso.value(0).id(): raise RuntimeError( 'Attributes are associated with the same component and should not be') From 7fd49b5b640c8270388a6aa4f9eb70e65dc213d3 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Tue, 9 Jul 2024 17:19:09 -0400 Subject: [PATCH 21/42] ENH: Adding Units Support for Definitions and Attributes You can now assign units to Definitions and Attributes. This can be useful when an Attribute represents a concept that has units but does not have an explicit value. For example, if an Attribute represents a temperature field that would be created by a simulation, you may want to assign a default unit to its Definition (such as Kelvin) but allow the user to change the Attribute's units to Celsius. The rules for assigning local units to an Attribute that override those inherited through its definition are the same as the case of assigning a value with units to a ValueItem. --- doc/release/notes/attributeChanges.rst | 12 + smtk/attribute/Attribute.cxx | 58 +++++ smtk/attribute/Attribute.h | 25 ++ smtk/attribute/Definition.cxx | 23 +- smtk/attribute/Definition.h | 15 ++ smtk/attribute/Resource.cxx | 1 + smtk/attribute/ValueItemDefinition.h | 6 + smtk/attribute/json/jsonAttribute.cxx | 11 + smtk/attribute/json/jsonDefinition.cxx | 13 ++ smtk/attribute/pybind11/PybindDefinition.h | 2 + smtk/attribute/testing/cxx/CMakeLists.txt | 1 + .../testing/cxx/unitAttributeUnits.cxx | 221 ++++++++++++++++++ smtk/io/XmlDocV1Parser.cxx | 13 +- smtk/io/XmlDocV1Parser.h | 2 +- smtk/io/XmlDocV5Parser.cxx | 25 +- smtk/io/XmlDocV5Parser.h | 2 +- smtk/io/XmlDocV8Parser.cxx | 24 ++ smtk/io/XmlDocV8Parser.h | 2 + smtk/io/XmlV2StringWriter.cxx | 5 + smtk/io/XmlV8StringWriter.cxx | 12 + smtk/io/XmlV8StringWriter.h | 2 + 21 files changed, 451 insertions(+), 24 deletions(-) create mode 100644 smtk/attribute/testing/cxx/unitAttributeUnits.cxx diff --git a/doc/release/notes/attributeChanges.rst b/doc/release/notes/attributeChanges.rst index c41f49868..d8c1faf30 100644 --- a/doc/release/notes/attributeChanges.rst +++ b/doc/release/notes/attributeChanges.rst @@ -12,3 +12,15 @@ Added `attribute::Analyses assignment method You can now assign the contents of one Analyses instance to another. This will create copies of all of the Analysis instances owned by the source Analyses. + +Units Support for Definitions and Attributes +-------------------------------------------- + +You can now assign units to Definitions and Attributes. This can be useful when an Attribute +represents a concept that has units but does not have an explicit value. For example, if an +Attribute represents a temperature field that would be created by a simulation, you may want to +assign a default unit to its Definition (such as Kelvin) but allow the user to change the +Attribute's units to Celsius. + +The rules for assigning local units to an Attribute that override those inherited through its definition +are the same as the case of assigning a value with units to a ValueItem. diff --git a/smtk/attribute/Attribute.cxx b/smtk/attribute/Attribute.cxx index cb9a0eed1..55c593057 100644 --- a/smtk/attribute/Attribute.cxx +++ b/smtk/attribute/Attribute.cxx @@ -37,6 +37,9 @@ #include "smtk/common/StringUtil.h" #include "smtk/common/UUIDGenerator.h" +#include "units/Converter.h" +#include "units/System.h" + #include #include #include @@ -1094,6 +1097,7 @@ bool Attribute::assign( } this->setAppliesToBoundaryNodes(sourceAtt->appliesToBoundaryNodes()); this->setAppliesToInteriorNodes(sourceAtt->appliesToInteriorNodes()); + this->setLocalUnits(sourceAtt->localUnits()); // Update items for (std::size_t i = 0; i < sourceAtt->numberOfItems(); ++i) { @@ -1181,3 +1185,57 @@ bool Attribute::assign( // TODO what about m_userData and Properties? } + +const std::string& Attribute::units() const +{ + if (!(m_localUnits.empty() && m_definition)) + { + return m_localUnits; + } + + return m_definition->units(); +} + +bool Attribute::setLocalUnits(const std::string& newUnits) +{ + const std::string& currentUnits = this->units(); + if (newUnits == currentUnits) + { + return true; + } + + if (currentUnits.empty() || !m_definition) + { + return false; + } + + const auto& unitSys = m_definition->unitsSystem(); + // Can't determine if the units are compatible w/o units system + if (!unitSys) + { + return false; + } + + // Can we convert from the current units to the proposed new units? + bool unitsSupported; + auto cUnits = unitSys->unit(currentUnits, &unitsSupported); + if (!unitsSupported) + { + // the current units are not supported by the units system + return false; + } + auto nUnits = unitSys->unit(newUnits, &unitsSupported); + if (!unitsSupported) + { + // the new units are not supported by the units system + return false; + } + + if (unitSys->convert(cUnits, nUnits)) + { + m_localUnits = newUnits; + return true; + } + // No known conversion + return false; +} diff --git a/smtk/attribute/Attribute.h b/smtk/attribute/Attribute.h index a69688e61..cd99c0d1c 100644 --- a/smtk/attribute/Attribute.h +++ b/smtk/attribute/Attribute.h @@ -402,6 +402,30 @@ class SMTKCORE_EXPORT Attribute : public resource::Component // registered with an Evaluator, else returns nullptr. std::unique_ptr createEvaluator() const; + /// @{ + ///\brief Set/Get the units string associated with the attribute + /// + /// This means that the information that the attributes represents + /// conceptually has units but unlike attribute Items, does not have a numerical value + /// associated with it. For example an attribute may be used to indicate that a numerical + /// field being output by a simulation represents a temperature whose units should be in Kelvin. + /// + ///\brief Return the units associated to the attribute + /// + /// Returns the units that have been explicitly on the attribute, else it will return those + /// set on its definition + const std::string& units() const; + ///\brief Return the units explicitly set on the attribute + const std::string& localUnits() const { return m_localUnits; } + ///\brief Explicitly sets the units on the attribute + /// + /// This will fail and return false under the following circumstances: + /// 1. There are no units associated with its definition and the newUnits string is not empty + /// 2. There is no units system associated with its definition and newUnits is not the same as those on the definition + /// 3. There is no way to convert between the units associated with its definition and newUnits + bool setLocalUnits(const std::string& newUnits); + /// @} + class GuardedLinks { public: @@ -467,6 +491,7 @@ class SMTKCORE_EXPORT Attribute : public resource::Component std::size_t m_includeIndex; bool m_hasLocalAdvanceLevelInfo[2]; unsigned int m_localAdvanceLevel[2]; + std::string m_localUnits; }; inline smtk::simulation::UserDataPtr Attribute::userData(const std::string& key) const diff --git a/smtk/attribute/Definition.cxx b/smtk/attribute/Definition.cxx index cd9d19adf..6426fbfb1 100644 --- a/smtk/attribute/Definition.cxx +++ b/smtk/attribute/Definition.cxx @@ -672,11 +672,11 @@ bool Definition::addItemDefinition(smtk::attribute::ItemDefinitionPtr cdef) void Definition::setItemDefinitionUnitsSystem( const smtk::attribute::ItemDefinitionPtr& itemDef) const { - // Get the units system of the attribute resource + const auto& defUnitsSystem = this->unitsSystem(); auto attRes = this->resource(); - if (attRes) + if (defUnitsSystem) { - itemDef->setUnitsSystem(attRes->unitsSystem()); + itemDef->setUnitsSystem(defUnitsSystem); } } @@ -885,3 +885,20 @@ void Definition::applyAdvanceLevels( def->applyAdvanceLevels(m_advanceLevel[0], m_advanceLevel[1]); } } + +const std::shared_ptr& Definition::unitsSystem() const +{ + static std::shared_ptr nullUnitsSystem; + auto attRes = this->resource(); + if (attRes) + { + return attRes->unitsSystem(); + } + return nullUnitsSystem; +} + +bool Definition::setUnits(const std::string& newUnits) +{ + m_units = newUnits; + return true; +} diff --git a/smtk/attribute/Definition.h b/smtk/attribute/Definition.h index 7983fa778..7c42c27dc 100644 --- a/smtk/attribute/Definition.h +++ b/smtk/attribute/Definition.h @@ -426,6 +426,20 @@ class SMTKCORE_EXPORT Definition : public smtk::enable_shared_from_this& unitsSystem() const; + protected: friend class smtk::attribute::Resource; /// AttributeDefinitions can only be created by an attribute resource @@ -490,6 +504,7 @@ class SMTKCORE_EXPORT Definition : public smtk::enable_shared_from_thissetLabel(sourceDef->label()); newDef->setVersion(sourceDef->version()); newDef->setIsAbstract(sourceDef->isAbstract()); + newDef->setUnits(sourceDef->units()); if (sourceDef->hasLocalAdvanceLevelInfo(0)) { newDef->setLocalAdvanceLevel(0, sourceDef->localAdvanceLevel(0)); diff --git a/smtk/attribute/ValueItemDefinition.h b/smtk/attribute/ValueItemDefinition.h index 155fb8f61..9cb3f18aa 100644 --- a/smtk/attribute/ValueItemDefinition.h +++ b/smtk/attribute/ValueItemDefinition.h @@ -55,8 +55,14 @@ class SMTKCORE_EXPORT ValueItemDefinition : public smtk::attribute::ItemDefiniti unsigned int readAccessLevel)> EnumRelevanceFunc; + /// @{ + ///\brief Set/Get the units string associated with the definition + /// + /// If the units string is supported by the units system assigned to + /// the definition, then unit conversion will be supported. const std::string& units() const { return m_units; } virtual bool setUnits(const std::string& newUnits); + /// @} ///\brief Returns true if units and a units system have been specified and that the /// specified units are supported by the units system diff --git a/smtk/attribute/json/jsonAttribute.cxx b/smtk/attribute/json/jsonAttribute.cxx index 12aba0fbb..53b2ca1ac 100644 --- a/smtk/attribute/json/jsonAttribute.cxx +++ b/smtk/attribute/json/jsonAttribute.cxx @@ -74,6 +74,11 @@ SMTKCORE_EXPORT void to_json(json& j, const smtk::attribute::AttributePtr& att) j["AdvanceWriteLevel"] = att->localAdvanceLevel(1); } + if (!att->localUnits().empty()) + { + j["Units"] = att->localUnits(); + } + // Process its Items int i, n = static_cast(att->numberOfItems()); if (n) @@ -133,6 +138,12 @@ SMTKCORE_EXPORT void from_json( att->setLocalAdvanceLevel(1, *result); } + result = j.find("Units"); + if (result != j.end()) + { + att->setLocalUnits(*result); + } + // Process items result = j.find("Items"); if (result != j.end()) diff --git a/smtk/attribute/json/jsonDefinition.cxx b/smtk/attribute/json/jsonDefinition.cxx index adba7f13a..5a7ae7338 100644 --- a/smtk/attribute/json/jsonDefinition.cxx +++ b/smtk/attribute/json/jsonDefinition.cxx @@ -78,6 +78,13 @@ SMTKCORE_EXPORT void to_json(nlohmann::json& j, const smtk::attribute::Definitio { j["Nodal"] = true; } + + // Does the Definition have units? + if (!defPtr->units().empty()) + { + j["Units"] = defPtr->units(); + } + // Process Local Category Expression if (defPtr->localCategories().isSet()) { @@ -245,6 +252,12 @@ SMTKCORE_EXPORT void from_json( defPtr->setIgnoreCategories(*result); } + result = j.find("Units"); + if (result != j.end()) + { + defPtr->setUnits(*result); + } + // Process Category Info () smtk::attribute::Categories::CombinationMode cmode; auto catExp = j.find("CategoryExpression"); // Current Form diff --git a/smtk/attribute/pybind11/PybindDefinition.h b/smtk/attribute/pybind11/PybindDefinition.h index 890c08e91..24fc02f14 100644 --- a/smtk/attribute/pybind11/PybindDefinition.h +++ b/smtk/attribute/pybind11/PybindDefinition.h @@ -104,6 +104,8 @@ inline PySharedPtrClass< smtk::attribute::Definition > pybind11_init_smtk_attrib .def("removeTag", &smtk::attribute::Definition::removeTag) .def("ignoreCategories", &smtk::attribute::Definition::ignoreCategories) .def("setIgnoreCategories", &smtk::attribute::Definition::setIgnoreCategories, py::arg("val")) + .def("setUnits", &smtk::attribute::Definition::setUnits, py::arg("newUnits")) + .def("units", &smtk::attribute::Definition::units) ; return instance; } diff --git a/smtk/attribute/testing/cxx/CMakeLists.txt b/smtk/attribute/testing/cxx/CMakeLists.txt index c02a2c2f5..a72b0056b 100644 --- a/smtk/attribute/testing/cxx/CMakeLists.txt +++ b/smtk/attribute/testing/cxx/CMakeLists.txt @@ -83,6 +83,7 @@ set(unit_tests unitAttributeAssociationConstraints.cxx unitAttributeBasics.cxx unitAttributeExclusiveAnalysis.cxx + unitAttributeUnits.cxx unitReferenceItemChildrenTest.cxx unitCategories.cxx unitComponentItem.cxx diff --git a/smtk/attribute/testing/cxx/unitAttributeUnits.cxx b/smtk/attribute/testing/cxx/unitAttributeUnits.cxx new file mode 100644 index 000000000..5dfc06763 --- /dev/null +++ b/smtk/attribute/testing/cxx/unitAttributeUnits.cxx @@ -0,0 +1,221 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/attribute/Attribute.h" +#include "smtk/attribute/Definition.h" +#include "smtk/attribute/FileItem.h" +#include "smtk/attribute/IntItem.h" +#include "smtk/attribute/Resource.h" +#include "smtk/attribute/ResourceItem.h" +#include "smtk/attribute/operators/Read.h" +#include "smtk/attribute/operators/Write.h" +#include "smtk/io/AttributeReader.h" +#include "smtk/io/AttributeWriter.h" +#include "smtk/io/Logger.h" + +#include "smtk/common/testing/cxx/helpers.h" + +using namespace smtk::attribute; +using namespace smtk::common; +using namespace smtk; + +namespace +{ + +bool testAttributeUnits(const attribute::ResourcePtr& attRes, const std::string& prefix) +{ + bool status = true; + // Lets find the Attributes + AttributePtr a = attRes->findAttribute("a"); + AttributePtr b = attRes->findAttribute("b"); + AttributePtr b1 = attRes->findAttribute("b1"); + AttributePtr c = attRes->findAttribute("c"); + + if (!a->definition()->units().empty()) + { + std::cerr << prefix << "Definition A has units: " << a->definition()->units() + << " but should have none!\n"; + status = false; + } + if (b->definition()->units() != "meters") + { + std::cerr << prefix << "Definition B has units: " << b->definition()->units() + << " but should be in meters!\n"; + status = false; + } + if (c->definition()->units() != "foo") + { + std::cerr << prefix << "Definition C has units: " << c->definition()->units() + << " but should be in foo!\n"; + status = false; + } + if (!a->units().empty()) + { + std::cerr << prefix << "Attribute a has units: " << a->units() << " but should have none!\n"; + status = false; + } + if (b->units() != "meters") + { + std::cerr << prefix << "Attribute b has units: " << b->units() << " but should be in meters!\n"; + status = false; + } + if (b1->units() != "feet") + { + std::cerr << prefix << "Attribute b has units: " << b1->units() << " but should be in feet!\n"; + status = false; + } + if (c->units() != "foo") + { + std::cerr << prefix << "Attribute c has units: " << c->units() << " but should be in foo!\n"; + status = false; + } + return status; +} + +} // namespace + +int unitAttributeUnits(int /*unused*/, char* /*unused*/[]) +{ + int status = 0; + // I. Let's create an attribute resource and some definitions and attributes + attribute::ResourcePtr attRes = attribute::Resource::create(); + + // A will not have any units specified + DefinitionPtr A = attRes->createDefinition("A"); + // B will have a unit of length (meters) + DefinitionPtr B = attRes->createDefinition("B"); + B->setUnits("meters"); + // C will have non supported units (foo) + DefinitionPtr C = attRes->createDefinition("C"); + C->setUnits("foo"); + + // Lets create some attributes + auto a = attRes->createAttribute("a", "A"); + auto b = attRes->createAttribute("b", "B"); + auto b1 = attRes->createAttribute("b1", "B"); + auto c = attRes->createAttribute("c", "C"); + + // Lets do some unit assignments + // Should not be able to assign any units to an attribute whose + // definition has no units + if (a->setLocalUnits("feet")) + { + std::cerr << "Error - was able to set a to be in feet!\n"; + status = -1; + } + // Should not be able to assign temperature units to an attribute + // whose definition is in meters + if (b->setLocalUnits("K")) + { + std::cerr << "Error - was able to set b to be in Kelvin!\n"; + status = -1; + } + // Should not be able to assign unsupported units to an attribute + // whose definition is in meters + if (b->setLocalUnits("foo")) + { + std::cerr << "Error - was able to set b to be in foo!\n"; + status = -1; + } + // Should be able to assign the units of feet to an attribute + // whose definition is in meters + if (!b1->setLocalUnits("feet")) + { + std::cerr << "Error - was not able to set b1 to be in feet!\n"; + status = -1; + } + // Should not be able to assign units to an attribute + // whose definition's units are not supported + if (c->setLocalUnits("feet")) + { + std::cerr << "Error - was able to set c to be in feet!\n"; + status = -1; + } + + if (status == 0) + { + std::cerr << "Initial Unit Assignment Tests - Passed!\n"; + } + + if (testAttributeUnits(attRes, "First Pass - ")) + { + std::cerr << "Creation Pass - Passed!\n"; + } + else + { + std::cerr << "Creation Pass - Failed!\n"; + status = -1; + } + io::AttributeWriter writer; + io::AttributeReader reader; + io::Logger logger; + std::string writeRroot(SMTK_SCRATCH_DIR); + std::string fname = writeRroot + "/unitAttributeUnits.sbi"; + std::string rname = writeRroot + "/unitAttributeUnits.smtk"; + + //Test JSON File I/O + attRes->setLocation(rname); + smtk::attribute::Write::Ptr writeOp = smtk::attribute::Write::create(); + writeOp->parameters()->associate(attRes); + auto opresult = writeOp->operate(); + + smtkTest( + opresult->findInt("outcome")->value() == + static_cast(smtk::operation::Operation::Outcome::SUCCEEDED), + "JSON Write operation failed\n" + << writeOp->log().convertToString()); + attRes = nullptr; + smtk::attribute::Read::Ptr readOp = smtk::attribute::Read::create(); + readOp->parameters()->findFile("filename")->setValue(rname); + opresult = readOp->operate(); + smtkTest( + opresult->findInt("outcome")->value() == + static_cast(smtk::operation::Operation::Outcome::SUCCEEDED), + "JSON Read operation failed\n" + << writeOp->log().convertToString()); + attRes = std::dynamic_pointer_cast( + opresult->findResource("resource")->value()); + + //Test the resource created using JSON + if (testAttributeUnits(attRes, "JSON Pass - ")) + { + std::cerr << "JSON Pass - Passed!\n"; + } + else + { + std::cerr << "JSON Pass - Failed!\n"; + status = -1; + } + + //Test XML File I/O + writer.write(attRes, fname, logger); + smtkTest( + !logger.hasErrors(), + "Error Generated when XML writing file (" << fname << "):\n" + << logger.convertToString()); + + attRes = attribute::Resource::create(); + reader.read(attRes, fname, logger); + smtkTest( + !logger.hasErrors(), + "Error Generated when XML reading file (" << fname << "):\n" + << logger.convertToString()); + //Test the resource created using XML + if (testAttributeUnits(attRes, "XML Pass - ")) + { + std::cerr << "XML Pass - Passed!\n"; + } + else + { + std::cerr << "XML Pass - Failed!\n"; + status = -1; + } + + return status; +} diff --git a/smtk/io/XmlDocV1Parser.cxx b/smtk/io/XmlDocV1Parser.cxx index 9e43f5735..1155a80aa 100644 --- a/smtk/io/XmlDocV1Parser.cxx +++ b/smtk/io/XmlDocV1Parser.cxx @@ -2011,7 +2011,7 @@ smtk::common::UUID XmlDocV1Parser::getAttributeID(xml_node& attNode) return smtk::common::UUID::null(); } -void XmlDocV1Parser::processAttribute(xml_node& attNode) +smtk::attribute::AttributePtr XmlDocV1Parser::processAttribute(xml_node& attNode) { xml_node itemsNode, assocsNode, iNode, node; std::string name, type; @@ -2025,14 +2025,14 @@ void XmlDocV1Parser::processAttribute(xml_node& attNode) if (!xatt) { smtkErrorMacro(m_logger, "Invalid Attribute! - Missing XML Attribute Name"); - return; + return smtk::attribute::AttributePtr(); } name = xatt.value(); xatt = attNode.attribute("Type"); if (!xatt) { smtkErrorMacro(m_logger, "Invalid Attribute: " << name << " - Missing XML Attribute Type"); - return; + return smtk::attribute::AttributePtr(); } type = xatt.value(); @@ -2044,7 +2044,7 @@ void XmlDocV1Parser::processAttribute(xml_node& attNode) smtkErrorMacro( m_logger, "Attribute: " << name << " of Type: " << type << " - can not find attribute definition"); - return; + return smtk::attribute::AttributePtr(); } // Is the definition abstract? @@ -2053,7 +2053,7 @@ void XmlDocV1Parser::processAttribute(xml_node& attNode) smtkErrorMacro( m_logger, "Attribute: " << name << " of Type: " << type << " - is based on an abstract definition"); - return; + return smtk::attribute::AttributePtr(); } // Do we have a valid uuid? @@ -2072,7 +2072,7 @@ void XmlDocV1Parser::processAttribute(xml_node& attNode) m_logger, "Attribute: " << name << " of Type: " << type << " - could not be created - is the name in use"); - return; + return smtk::attribute::AttributePtr(); } // Set the attribute include index @@ -2185,6 +2185,7 @@ void XmlDocV1Parser::processAttribute(xml_node& attNode) } } } + return att; } void XmlDocV1Parser::processItem(xml_node& node, ItemPtr item) diff --git a/smtk/io/XmlDocV1Parser.h b/smtk/io/XmlDocV1Parser.h index ce8829b8c..681d88391 100644 --- a/smtk/io/XmlDocV1Parser.h +++ b/smtk/io/XmlDocV1Parser.h @@ -134,7 +134,7 @@ class SMTKCORE_EXPORT XmlDocV1Parser smtk::attribute::DefinitionPtr& def); virtual void processAssociationDef(pugi::xml_node& node, smtk::attribute::DefinitionPtr def); - virtual void processAttribute(pugi::xml_node& attNode); + virtual smtk::attribute::AttributePtr processAttribute(pugi::xml_node& attNode); virtual void processItem(pugi::xml_node& node, smtk::attribute::ItemPtr item); void addDefaultCategoryIfNeeded(const smtk::attribute::ItemDefinitionPtr& idef); diff --git a/smtk/io/XmlDocV5Parser.cxx b/smtk/io/XmlDocV5Parser.cxx index a1e9cd5f8..a9f6da1f3 100644 --- a/smtk/io/XmlDocV5Parser.cxx +++ b/smtk/io/XmlDocV5Parser.cxx @@ -147,25 +147,24 @@ void XmlDocV5Parser::process( } } -void XmlDocV5Parser::processAttribute(pugi::xml_node& attNode) +smtk::attribute::AttributePtr XmlDocV5Parser::processAttribute(pugi::xml_node& attNode) { - XmlDocV4Parser::processAttribute(attNode); + auto att = XmlDocV4Parser::processAttribute(attNode); - xml_node propertiesNode = attNode.child("Properties"); - if (propertiesNode) + if (att) { - xml_attribute xatt = attNode.attribute("Name"); - if (!xatt) - { - return; - } - std::string name = xatt.value(); - auto att = m_resource->findAttribute(name); - if (att) + xml_node propertiesNode = attNode.child("Properties"); + if (propertiesNode) { - processProperties(att, propertiesNode, m_logger); + xml_attribute xatt = attNode.attribute("Name"); + if (xatt) + { + std::string name = xatt.value(); + processProperties(att, propertiesNode, m_logger); + } } } + return att; } bool XmlDocV5Parser::canParse(pugi::xml_document& doc) diff --git a/smtk/io/XmlDocV5Parser.h b/smtk/io/XmlDocV5Parser.h index cce9340a3..b90880825 100644 --- a/smtk/io/XmlDocV5Parser.h +++ b/smtk/io/XmlDocV5Parser.h @@ -33,11 +33,11 @@ class SMTKCORE_EXPORT XmlDocV5Parser : public XmlDocV4Parser pugi::xml_node& rootNode, std::map>& globalTemplateMap) override; - void processAttribute(pugi::xml_node& attNode) override; static bool canParse(pugi::xml_node& node); static bool canParse(pugi::xml_document& doc); protected: + smtk::attribute::AttributePtr processAttribute(pugi::xml_node& attNode) override; void processHints(pugi::xml_node& root) override; }; } // namespace io diff --git a/smtk/io/XmlDocV8Parser.cxx b/smtk/io/XmlDocV8Parser.cxx index dabc3fb89..62b08c0c1 100644 --- a/smtk/io/XmlDocV8Parser.cxx +++ b/smtk/io/XmlDocV8Parser.cxx @@ -150,3 +150,27 @@ void XmlDocV8Parser::processDefCategoryExpressionNode(xml_node& node, Definition this->processCategoryExpressionNode(node, def->localCategories(), inheritanceMode); def->setCategoryInheritanceMode(inheritanceMode); } + +void XmlDocV8Parser::processDefinitionAtts(xml_node& defNode, smtk::attribute::DefinitionPtr& def) +{ + pugi::xml_attribute xatt = defNode.attribute("Units"); + if (xatt) + { + def->setUnits(xatt.value()); + } + + this->XmlDocV7Parser::processDefinitionAtts(defNode, def); +} + +smtk::attribute::AttributePtr XmlDocV8Parser::processAttribute(pugi::xml_node& attNode) +{ + auto att = XmlDocV7Parser::processAttribute(attNode); + + xml_attribute xatt = attNode.attribute("Units"); + if (att && xatt) + { + att->setLocalUnits(xatt.value()); + } + + return att; +} diff --git a/smtk/io/XmlDocV8Parser.h b/smtk/io/XmlDocV8Parser.h index 3bcd886e2..ee4883ae7 100644 --- a/smtk/io/XmlDocV8Parser.h +++ b/smtk/io/XmlDocV8Parser.h @@ -32,10 +32,12 @@ class SMTKCORE_EXPORT XmlDocV8Parser : public XmlDocV7Parser static bool canParse(pugi::xml_document& doc); protected: + smtk::attribute::AttributePtr processAttribute(pugi::xml_node& attNode) override; void processCategoryExpressionNode( pugi::xml_node& node, attribute::Categories::Expression& catExp, attribute::Categories::CombinationMode& inheritanceMode); + void processDefinitionAtts(pugi::xml_node& node, smtk::attribute::DefinitionPtr& def) override; void processItemDefChildNode(pugi::xml_node& node, const smtk::attribute::ItemDefinitionPtr& idef) override; void processItemDefCategoryExpressionNode( diff --git a/smtk/io/XmlV2StringWriter.cxx b/smtk/io/XmlV2StringWriter.cxx index cb42ed0ec..df985168d 100644 --- a/smtk/io/XmlV2StringWriter.cxx +++ b/smtk/io/XmlV2StringWriter.cxx @@ -1119,6 +1119,11 @@ void XmlV2StringWriter::processAttribute(xml_node& attributes, attribute::Attrib } node.append_attribute("ID").set_value(att->id().toString().c_str()); + if (!att->localUnits().empty()) + { + node.append_attribute("Units").set_value(att->localUnits().c_str()); + } + // Are we saving attribute association information? if (m_includeAttributeAssociations) { diff --git a/smtk/io/XmlV8StringWriter.cxx b/smtk/io/XmlV8StringWriter.cxx index b8f6670a6..b6cbaaf37 100644 --- a/smtk/io/XmlV8StringWriter.cxx +++ b/smtk/io/XmlV8StringWriter.cxx @@ -42,5 +42,17 @@ unsigned int XmlV8StringWriter::fileVersion() const return 8; } +void XmlV8StringWriter::processDefinitionInternal( + pugi::xml_node& definition, + smtk::attribute::DefinitionPtr def) +{ + XmlV7StringWriter::processDefinitionInternal(definition, def); + + if (!def->units().empty()) + { + definition.append_attribute("Units").set_value(def->units().c_str()); + } +} + } // namespace io } // namespace smtk diff --git a/smtk/io/XmlV8StringWriter.h b/smtk/io/XmlV8StringWriter.h index c6739b2e2..79c243c5c 100644 --- a/smtk/io/XmlV8StringWriter.h +++ b/smtk/io/XmlV8StringWriter.h @@ -36,6 +36,8 @@ class SMTKCORE_EXPORT XmlV8StringWriter : public XmlV7StringWriter // Three virtual methods for writing contents std::string className() const override; unsigned int fileVersion() const override; + void processDefinitionInternal(pugi::xml_node& definition, smtk::attribute::DefinitionPtr def) + override; private: }; From 8391421d6460b90948929122ff98a731497f45b3 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 10 Jul 2024 13:29:33 -0400 Subject: [PATCH 22/42] BUG: Attribute Resource Copy Finalize Ignoring Attribute Copy Options The method will now the attribute specific copy options if they are provided. Also removed explicit attribute copy options that are the defaults for the case no attribute copy options are provided. Also added some additional outputs to the testCloneResources test. This may be related to Issue #538 Closes #541 --- smtk/attribute/Resource.cxx | 46 ++++++++++--------- smtk/attribute/Resource.h | 27 +++++++++-- .../testing/python/testCloneResources.py | 12 +++++ 3 files changed, 59 insertions(+), 26 deletions(-) diff --git a/smtk/attribute/Resource.cxx b/smtk/attribute/Resource.cxx index 149f1c375..fd625c113 100644 --- a/smtk/attribute/Resource.cxx +++ b/smtk/attribute/Resource.cxx @@ -1280,16 +1280,17 @@ bool Resource::copyInitialize( // Construct options for copying attributes but not their contents. smtk::attribute::CopyAssignmentOptions attOptions; - attOptions.copyOptions.setCopyUUID(false); - attOptions.copyOptions.setPerformAssignment(false); - attOptions.copyOptions.setCopyDefinition( - false); // These should already be present from clone(). - if (options.suboptions().contains()) { // Our caller is providing an override. attOptions = options.suboptions().get(); } + // Since this method only copies the structure of the resource and not the attributes' + // items' value and the fact that the definition information should have already been + // copied in the clone method - these options should always be off. + attOptions.copyOptions.setCopyUUID(false); + attOptions.copyOptions.setPerformAssignment(false); + attOptions.copyOptions.setCopyDefinition(false); // Copy all the attributes. source->attributes(allAttributes); @@ -1328,27 +1329,28 @@ bool Resource::copyFinalize( } smtk::attribute::CopyAssignmentOptions attOptions; + if (options.suboptions().contains()) + { + // Our caller is providing an override. + attOptions = options.suboptions().get(); + } + else + { + // Set some realistic defaults - // I. Copy associations and reference-item data. - - // None of the copy options should be needed since all of the - // attributes should already be copied - attOptions.copyOptions.setCopyUUID(false); - attOptions.copyOptions.setPerformAssignment(true); - attOptions.copyOptions.setCopyDefinition(false); // These should already be present from clone(). + // There should be no need to validate these + attOptions.attributeOptions.setDoNotValidateAssociations(true); + attOptions.itemOptions.setDoNotValidateReferenceInfo(true); - attOptions.attributeOptions.setIgnoreMissingItems(false); - attOptions.attributeOptions.setAllowPartialAssociations(false); - attOptions.attributeOptions.setDoNotValidateAssociations( - true); // There should be no need to validate - attOptions.attributeOptions.setCopyAssociations(true); - attOptions.attributeOptions.setObjectMapping(&options.objectMapping()); + attOptions.attributeOptions.setCopyAssociations(true); + } - attOptions.itemOptions.setIgnoreMissingChildren(false); - attOptions.itemOptions.setAllowPartialValues(false); - attOptions.itemOptions.setIgnoreReferenceValues(false); - attOptions.itemOptions.setDoNotValidateReferenceInfo(true); + // All of the required attributes should already be copied - this option should + // always be off attOptions.itemOptions.setDisableCopyAttributes(true); + + // Set the object mapping + attOptions.attributeOptions.setObjectMapping(&options.objectMapping()); attOptions.itemOptions.setObjectMapping(&options.objectMapping()); // for each attribute in the source resource copy assign its data to its copy diff --git a/smtk/attribute/Resource.h b/smtk/attribute/Resource.h index 456cd82b8..340badd6f 100644 --- a/smtk/attribute/Resource.h +++ b/smtk/attribute/Resource.h @@ -413,20 +413,39 @@ class SMTKCORE_EXPORT Resource bool setTemplateVersion(std::size_t templateVersion) override; std::size_t templateVersion() const override { return m_templateVersion; } - /// Create an empty, un-managed clone of this resource instance. + ///\brief Create an empty, un-managed clone of this resource instance's meta information. /// /// If \a options has copyTemplateData() set to true, then this resource's /// Definition instances will be copied to the output resources. + /// In addition, unitsSystem and Analysis information is copied. std::shared_ptr clone( smtk::resource::CopyOptions& options) const override; - /// Copy data from the \a other resource into this resource, as specified by \a options. + ///\brief Copy data from the \a other resource into this resource, as specified by \a options. + /// + /// In the case of attribute resources - only the structure defined by the source's attributes + /// are copied (note their items' values), as well as properties, views, geometry (if any), + /// and active category information. + /// Note that the following attribute copy options will always be assumed to be set and cannot + /// be overridden by \a options: + /// CopyUUID(false) + /// PerformAssignment(false) + /// CopyDefinition(false) + bool copyInitialize( const std::shared_ptr& other, smtk::resource::CopyOptions& options) override; - /// Copy relations (associations, references) from the \a source resource - /// into this resource, as specified by \a options. + ///\brief Copy attribute resource contents. + /// + /// Besides items' values this includes: + /// Relations (associations, references) from the \a source resource + /// into this resource, as specified by \a options. + /// Associated resource information. + /// Link information + /// + /// Note that since the structure of the resource is assumed to be copied + /// in copyFinalize, the attribute copy option DisableCopyAttributes always set to true. bool copyFinalize( const std::shared_ptr& source, smtk::resource::CopyOptions& options) override; diff --git a/smtk/resource/testing/python/testCloneResources.py b/smtk/resource/testing/python/testCloneResources.py index 315cd48aa..f51d200c5 100644 --- a/smtk/resource/testing/python/testCloneResources.py +++ b/smtk/resource/testing/python/testCloneResources.py @@ -115,11 +115,15 @@ def testCloneAttributeResourceOnly(self): clonedAtts = self.origAttResource.clone(opts) if not clonedAtts.copyInitialize(self.origAttResource, opts): + print('Logger Output:\n') + print(smtk.io.Logger.instance().convertToString(True), '\n') raise RuntimeError( 'Copy Initialized Failed for Attribute Resource') print('Copy Initialized for Attributes Worked.') if not clonedAtts.copyFinalize(self.origAttResource, opts): + print('Logger Output:\n') + print(smtk.io.Logger.instance().convertToString(True), '\n') raise RuntimeError( 'Copy Finalized Failed for Attribute Resource') @@ -136,18 +140,26 @@ def testCloneAllResource(self): clonedAtts = self.origAttResource.clone(opts) clonedMarkup = self.origMarkupResource.clone(opts) if not clonedAtts.copyInitialize(self.origAttResource, opts): + print('Logger Output:\n') + print(smtk.io.Logger.instance().convertToString(True), '\n') raise RuntimeError( 'Copy Initialized Failed for Attribute Resource') if not clonedMarkup.copyInitialize(self.origMarkupResource, opts): + print('Logger Output:\n') + print(smtk.io.Logger.instance().convertToString(True), '\n') raise RuntimeError('Copy Initialized Failed for Markup Resource') print('Copy Initialized for Resources Worked.') if not clonedAtts.copyFinalize(self.origAttResource, opts): + print('Logger Output:\n') + print(smtk.io.Logger.instance().convertToString(True), '\n') raise RuntimeError('Copy Finalized Failed for Attribute Resource') if not clonedMarkup.copyFinalize(self.origMarkupResource, opts): + print('Logger Output:\n') + print(smtk.io.Logger.instance().convertToString(True), '\n') raise RuntimeError('Copy Finalized Failed for Markup Resource') print('Copy Finalized for Resources Worked.') From d6fd13c7e91118ce35d06400f8089c8fe703ba18 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 12 Jul 2024 14:59:07 -0400 Subject: [PATCH 23/42] Fix an issue with the way 3-d widgets were updated. --- data/baseline/smtk/widgets/FrameWidget.png | 4 +- .../notes/paraview-widgets-observer.rst | 14 +++++++ .../widgets/pqSMTKAttributeItemWidget.cxx | 38 ------------------- .../widgets/pqSMTKAttributeItemWidgetP.h | 1 - .../widgets/testing/xml/FrameWidget.xml | 28 +++++++++++++- 5 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 doc/release/notes/paraview-widgets-observer.rst diff --git a/data/baseline/smtk/widgets/FrameWidget.png b/data/baseline/smtk/widgets/FrameWidget.png index c7d8220ac..b6cf35fd4 100644 --- a/data/baseline/smtk/widgets/FrameWidget.png +++ b/data/baseline/smtk/widgets/FrameWidget.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a8f6a29d27ff574db412a683bdf86bd8e55352625f4a0626720c592b448a595 -size 4640 +oid sha256:ab0460238d1378e7f21cb3a44d42263137a2305469eb60f63b4d349deb7bb248 +size 5160 diff --git a/doc/release/notes/paraview-widgets-observer.rst b/doc/release/notes/paraview-widgets-observer.rst new file mode 100644 index 000000000..bdfb54588 --- /dev/null +++ b/doc/release/notes/paraview-widgets-observer.rst @@ -0,0 +1,14 @@ +ParaView extensions +=================== + +3-d widget operation observer +----------------------------- + +The :smtk:`pqSMTKAttributeItemWidget` class was monitoring operations to determine +when the attribute holding its items was modified. +It should not do this, instead relying on qtAttributeView or qtInstancedView to +call its ``updateItemData()`` method when an operation modifies the widget. +(It was already doing this.) Because its operation observer was not validating +that the operation causing changes was triggered by itself, a race condition +existed that could undo changes to one item controlled by the widget while +parsing updates from another item. diff --git a/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidget.cxx b/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidget.cxx index e681fcc4f..dcc87f108 100644 --- a/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidget.cxx +++ b/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidget.cxx @@ -204,23 +204,6 @@ pqSMTKAttributeItemWidget::pqSMTKAttributeItemWidget( m_p->m_overrideWhen = pqSMTKAttributeItemWidget::OverrideWhenConvert(ow); m_p->m_fallbackStrategy = pqSMTKAttributeItemWidget::FallbackStrategyConvert(fs); m_p->m_geometrySource = pqSMTKAttributeItemWidget::GeometrySourceConvert(gs); - QPointer guardedObject(this); - m_p->m_opObserver = info.baseView()->uiManager()->operationManager()->observers().insert( - [guardedObject]( - const smtk::operation::Operation& op, - smtk::operation::EventType event, - smtk::operation::Operation::Result res) { - if ( - event == smtk::operation::EventType::DID_OPERATE && - dynamic_cast(&op) && guardedObject && - guardedObject->item() && - res->findReference("modified")->contains(guardedObject->item()->attribute())) - { - guardedObject->updateWidgetFromItem(); - } - return 0; - }, - "pqSMTKAttributeItemWidget: Update widget from item when parent attribute is modified."); } pqSMTKAttributeItemWidget::pqSMTKAttributeItemWidget( @@ -232,23 +215,6 @@ pqSMTKAttributeItemWidget::pqSMTKAttributeItemWidget( smtk::extension::qtAttributeItemInfo(itm, smtk::view::Configuration::Component(), p, bview)) { m_p = new Internal(itm, this->widget(), bview, orient); - QPointer guardedObject(this); - m_p->m_opObserver = bview->uiManager()->operationManager()->observers().insert( - [guardedObject]( - const smtk::operation::Operation& op, - smtk::operation::EventType event, - smtk::operation::Operation::Result res) { - if ( - event == smtk::operation::EventType::DID_OPERATE && - dynamic_cast(&op) && guardedObject && - guardedObject->item() && - res->findReference("modified")->contains(guardedObject->item()->attribute())) - { - guardedObject->updateWidgetFromItem(); - } - return 0; - }, - "pqSMTKAttributeItemWidget: Update widget from item when parent attribute is modified."); m_isLeafItem = true; this->createWidget(); } @@ -257,10 +223,6 @@ pqSMTKAttributeItemWidget::~pqSMTKAttributeItemWidget() { auto* ui = this->uiManager(); auto operationManager = ui ? ui->operationManager() : nullptr; - if (operationManager && m_p->m_opObserver.assigned()) - { - operationManager->observers().erase(m_p->m_opObserver); - } delete this->m_p; this->m_p = nullptr; } diff --git a/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h b/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h index 920e51e4a..b17f75def 100644 --- a/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h +++ b/smtk/extension/paraview/widgets/pqSMTKAttributeItemWidgetP.h @@ -69,6 +69,5 @@ class pqSMTKAttributeItemWidget::Internal // state of children QMap> m_children; - smtk::operation::Observers::Key m_opObserver; State m_state{ State::Idle }; }; diff --git a/smtk/extension/paraview/widgets/testing/xml/FrameWidget.xml b/smtk/extension/paraview/widgets/testing/xml/FrameWidget.xml index e41f761f2..726e63c01 100644 --- a/smtk/extension/paraview/widgets/testing/xml/FrameWidget.xml +++ b/smtk/extension/paraview/widgets/testing/xml/FrameWidget.xml @@ -11,5 +11,31 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + From 0432f77c996875e5cd8afc2f756dba923b7350d7 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 15 Jul 2024 16:54:26 -0400 Subject: [PATCH 24/42] Add documentation of `qtItem` design patterns. --- doc/CMakeLists.txt | 5 ++- doc/userguide/extension/index.rst | 2 +- .../extension/qt/attribute-views.rst | 35 +++++++++++++++++++ doc/userguide/extension/qt/concepts.rst | 33 +++++++++++++++++ .../extension/{qt.rst => qt/diagram.rst} | 21 ----------- doc/userguide/extension/qt/index.rst | 29 +++++++++++++++ 6 files changed, 102 insertions(+), 23 deletions(-) create mode 100644 doc/userguide/extension/qt/attribute-views.rst create mode 100644 doc/userguide/extension/qt/concepts.rst rename doc/userguide/extension/{qt.rst => qt/diagram.rst} (91%) create mode 100644 doc/userguide/extension/qt/index.rst diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 652beb3ce..ccd2974da 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -126,7 +126,10 @@ if (SPHINX_FOUND) userguide/extension/paraview/widgets/design.rst userguide/extension/paraview/widgets/gallery.rst userguide/extension/paraview/widgets/index.rst - userguide/extension/qt.rst + userguide/extension/qt/index.rst + userguide/extension/qt/concepts.rst + userguide/extension/qt/attribute-views.rst + userguide/extension/qt/diagram.rst userguide/extension/vtk.rst userguide/geometry/concepts.rst userguide/geometry/index.rst diff --git a/doc/userguide/extension/index.rst b/doc/userguide/extension/index.rst index 634e9b88a..ae26a0427 100644 --- a/doc/userguide/extension/index.rst +++ b/doc/userguide/extension/index.rst @@ -14,6 +14,6 @@ additional functionality. .. toctree:: :maxdepth: 3 - qt.rst + qt/index.rst vtk.rst paraview/index.rst diff --git a/doc/userguide/extension/qt/attribute-views.rst b/doc/userguide/extension/qt/attribute-views.rst new file mode 100644 index 000000000..185b85226 --- /dev/null +++ b/doc/userguide/extension/qt/attribute-views.rst @@ -0,0 +1,35 @@ +Attribute and Item Views +------------------------ + +Most of the Qt subsytem is dedicated to providing users with ways +to create, delete, and modify attributes. +There are two Qt-specific subclasses of :smtk:`smtk::view::BaseView` +dedicated to this: + ++ :smtk:`smtk::extension::qtInstancedView` is a view dedicated to + displaying and editing a single, named instance of an attribute. ++ :smtk:`smtk::extension::qtAttributeView` is a view dedicated to + creating, destroying, and editing any number of attributes that + share some common base definition (such as a material or boundary + condition). + +Each of these views inherits :smtk:`smtk::extension::qtBaseAttributeView` +and displays items of the attribute. + +Items displayed inside these views all inherit `smtk::extension::qtItem`. + +Qt view classes also provide facilities that adapt SMTK's +:ref:`observers-pattern` to Qt's signals-and-slots system. +When an item indicates it has modified its parent attribute +(via a Qt signal), the view creates and launches a +:smtk:`smtk::attribute::Signal` operation to notify +core SMTK objects of the change. +Thus, if you are implementing a custom Qt item class, you +should emit a ``modified()`` signal; the parent attribute-view +will consume this signal and run the operation for you. + +Likewise, subclasses of `:smtk::extension::qtItem` should not +observe SMTK operations themselves but instead rely on the +Qt subsystem to observe operations. Each time an operation +completes, the Qt view observes it and calls the +``updateItemData()`` on each of its attributes' items. diff --git a/doc/userguide/extension/qt/concepts.rst b/doc/userguide/extension/qt/concepts.rst new file mode 100644 index 000000000..a491b1076 --- /dev/null +++ b/doc/userguide/extension/qt/concepts.rst @@ -0,0 +1,33 @@ +Key Concepts +------------ + +One of the primary goals in the design of SMTK's Qt subsystem +was to allow operations to run in the background while keeping +the user interface responsive. +The Qt library supports threads, but requires any code that +modifies widgets to run on the "GUI thread" (i.e., thread 0, +the main application thread). + +Designing thread support into a library is difficult, and +Qt's decision to force user-interface code to run on a specific +thread simplifies its design. +Similarly, SMTK makes some assumptions in order to simplify +support for simultaneous :smtk:`smtk::operation::Operation` +instances running in the background: + +1. Locking occurs with a granularity of :smtk:`smtk::resource::Resource` + instances, not components or other objects. +2. Locks on resources must **never** be acquired on the GUI thread. +3. Operation observers must always be invoked on the GUI thread + while the locks the the operation's resources are held by the + thread on which the operation is running. +4. Operations should be run via a :smtk:`smtk::operation::Launcher` + so that they run asynchronously in the background as the + operation's thread is able to acquire the locks it needs. + +To ensure that operation observers are invoked on the GUI thread, +your application must create and use an instance of the +:smtk:`qtSMTKCallObserversOnMainThreadBehavior` class. +This class overrides the operation observer-invoker with a Qt-object +that uses Qt's signal-and-slot functionality to transfer execution +of observers to the GUI thread. diff --git a/doc/userguide/extension/qt.rst b/doc/userguide/extension/qt/diagram.rst similarity index 91% rename from doc/userguide/extension/qt.rst rename to doc/userguide/extension/qt/diagram.rst index 0942ccf11..a37cff470 100644 --- a/doc/userguide/extension/qt.rst +++ b/doc/userguide/extension/qt/diagram.rst @@ -1,24 +1,3 @@ -.. _smtk-qt-sys: - -Qt -== - -SMTK provides Qt classes that allow users to edit attributes and inspect model and mesh resources. - -* :smtk:`qtBaseView ` – a base class for all views implemented with Qt -* :smtk:`qtAttributeView ` – a view that allows users to create - and destroy attributes that inherit a designer-specified set of - :smtk:`Definition ` instances. -* :smtk:`qtInstancedView ` – a view that allows users to edit - designer-specified :smtk:`Attribute ` instances. -* :smtk:`qtAssociationView ` – a view that allows users to edit - the objects associated to an attribute or collection of attributes. -* :smtk:`qtResourceBrowser ` – a view that arranges - persistent objects into a tree. - -Besides Qt-based attribute and phrase views that use traditional widgets, -SMTK also provides a facility for schematic diagrams discussed below. - Diagram views ------------- diff --git a/doc/userguide/extension/qt/index.rst b/doc/userguide/extension/qt/index.rst new file mode 100644 index 000000000..0006f3dee --- /dev/null +++ b/doc/userguide/extension/qt/index.rst @@ -0,0 +1,29 @@ +.. _smtk-qt-sys: + +Qt +== + +SMTK provides Qt classes that allow users to edit attributes and inspect model and mesh resources. +Most of the Qt support library is focused on providing Qt implementations of the +:smtk:`smtk::view::BaseView` class so that users can view and edit attribute resources. + +* :smtk:`qtBaseView ` – a base class for all views implemented with Qt +* :smtk:`qtAttributeView ` – a view that allows users to create + and destroy attributes that inherit a designer-specified set of + :smtk:`Definition ` instances. +* :smtk:`qtInstancedView ` – a view that allows users to edit + designer-specified :smtk:`Attribute ` instances. +* :smtk:`qtAssociationView ` – a view that allows users to edit + the objects associated to an attribute or collection of attributes. +* :smtk:`qtResourceBrowser ` – a view that arranges + persistent objects into a tree. + +Besides Qt-based attribute and phrase views that use traditional widgets, +SMTK also provides a facility for schematic diagrams discussed below. + +.. toctree:: + :maxdepth: 3 + + concepts.rst + attribute-views.rst + diagram.rst From 5e9f78931decda0d4c795dbbc5ed7068b0ae6430 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Mon, 15 Jul 2024 10:19:43 -0400 Subject: [PATCH 25/42] Copy links properly. --- .../attribute_collection/cloneTest.smtk | 4 +- doc/userguide/common/links.rst | 6 + doc/userguide/resource/concepts.rst | 33 +-- doc/userguide/resource/index.rst | 1 + doc/userguide/resource/links.rst | 61 ++++ smtk/attribute/Resource.h | 8 + smtk/attribute/pybind11/PybindAttribute.cxx | 4 +- smtk/attribute/pybind11/PybindResource.h | 2 + smtk/common/Links.h | 10 + smtk/markup/Component.cxx | 2 +- smtk/mesh/core/Resource.h | 4 + smtk/mesh/pybind11/PybindResource.h | 1 + smtk/resource/ComponentLinks.h | 2 + smtk/resource/Links.cxx | 7 +- smtk/resource/Links.h | 35 ++- smtk/resource/Resource.cxx | 39 +-- smtk/resource/Resource.h | 4 + smtk/resource/ResourceLinks.cxx | 119 ++++++++ smtk/resource/ResourceLinks.h | 11 +- smtk/resource/pybind11/PybindComponent.h | 2 + smtk/resource/pybind11/PybindComponentLinks.h | 2 - smtk/resource/pybind11/PybindCopyOptions.h | 18 ++ smtk/resource/pybind11/PybindLinks.h | 72 +++++ smtk/resource/pybind11/PybindResource.cxx | 8 + smtk/resource/pybind11/PybindResource.h | 2 + smtk/resource/pybind11/PybindResourceLinks.h | 34 +++ .../testing/python/testCloneResources.py | 269 ++++++++++-------- 27 files changed, 547 insertions(+), 213 deletions(-) create mode 100644 doc/userguide/resource/links.rst create mode 100644 smtk/resource/pybind11/PybindLinks.h create mode 100644 smtk/resource/pybind11/PybindResourceLinks.h diff --git a/data/attribute/attribute_collection/cloneTest.smtk b/data/attribute/attribute_collection/cloneTest.smtk index 64fd6ffb4..ecba16b65 100644 --- a/data/attribute/attribute_collection/cloneTest.smtk +++ b/data/attribute/attribute_collection/cloneTest.smtk @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a13051c24999b3c031029f1a87c186bf729b7a010fad2fc83a1f9136f74917b -size 6135 +oid sha256:3d1181f5a862d73bc242425e1a44e413f230c351e01b1e9f010906348e68116f +size 6154 diff --git a/doc/userguide/common/links.rst b/doc/userguide/common/links.rst index 4a026208b..b6a6afa05 100644 --- a/doc/userguide/common/links.rst +++ b/doc/userguide/common/links.rst @@ -11,3 +11,9 @@ according to their "left", "right" and "role" values. An example that demonstrates the prinicples and API of this pattern can be found at `smtk/comon/testing/cxx/UnitTestLinks.cxx`. + +Note that the links template is used by SMTK's resource system in a +two-level hierarchy: one Links instance connecting resources to each +other with a "generic" role holds a child Links instance that connects +both components and resources in specific, user-provided roles. +See :ref:`resource links` for more information. diff --git a/doc/userguide/resource/concepts.rst b/doc/userguide/resource/concepts.rst index bdcedbeea..0f0c779e9 100644 --- a/doc/userguide/resource/concepts.rst +++ b/doc/userguide/resource/concepts.rst @@ -44,37 +44,8 @@ In addition to these useful base classes, :smtk:`Links ` Resource links connect persistent objects via a unidirectional - relationship that exists for a specific purpose, or role. Links are - stored separately from the objects they connect, but each resource - instance owns the link that holds all outgoing connections from the - resource and its components. The smtk::resource::Link class is an - abstract API provided on both resources and components; on the - resource, the link subclass provides actual storage, while on - components, the subclass asks its parent resource for the links object - in order to search for connections. Roles are simply integers. Roles - less than zero are reserved for internal SMTK use while positive roles - are available for user-specified (i.e., application-specific) roles. - - .. list-table:: SMTK system roles for Links - :widths: 50 50 - :header-rows: 1 - - * - Role - - Purpose - * - :smtk:`smtk::attribute::Resource::AssociationRole` - - Link objects to an *instance* of an attribute to - indicate they possess, exhibit, or manifest the attribute. - * - :smtk:`smtk::attribute::Resource::ReferenceRole` - - Link objects to an *item* of an attribute to indicate the - item contains or references the object. - * - :smtk:`smtk::mesh::Resource::ClassificationRole` - - Link a mesh-set object to a source component whose geometry - the mesh-set discretizes. - * - :smtk:`smtk::resource::Resource::VisuallyLinkedRole` - - Link objects to one another to indicate the source - (left-hand) object (which does not have renderable geometry) - should be provided with visibility controls for the target - (right-hand) object(s) (which should have renderable geometry). + relationship that exists for a specific purpose, or role. + Links are discussed further below in the :ref:`resource links` section. :smtk:`Properties ` Resources and components contain a dictionary-like data structure that diff --git a/doc/userguide/resource/index.rst b/doc/userguide/resource/index.rst index 2dc2e6053..77c89ee0f 100644 --- a/doc/userguide/resource/index.rst +++ b/doc/userguide/resource/index.rst @@ -14,4 +14,5 @@ so that the provenance of all the inputs to a simulation can be tracked. concepts.rst filtering-and-searching.rst + links.rst copy-template-update.rst diff --git a/doc/userguide/resource/links.rst b/doc/userguide/resource/links.rst new file mode 100644 index 000000000..36bfbb231 --- /dev/null +++ b/doc/userguide/resource/links.rst @@ -0,0 +1,61 @@ +.. _resource links: + +Resource Links +============== + +Resources and components provide a facility, implemented via the +:smtk:`smtk::common::Links` template, to store relationships between +persistent objects. Each link is tagged with a *role* that indicates +the functional purpose of the relationship. + +Links are stored separately from the objects they connect, but each +resource instance owns the link that holds all outgoing connections from the +resource and its components. The :smtk:`smtk::resource::Link` class is an +abstract API provided on both resources and components; on the +resource, the link subclass provides actual storage, while on +components, the subclass asks its parent resource for the links object +in order to search for connections. Roles are simply integers. Roles +less than zero are reserved for internal SMTK use while positive roles +are available for user-specified (i.e., application-specific) roles. + +.. list-table:: SMTK system roles for Links + :widths: 50 50 + :header-rows: 1 + + * - Role + - Purpose + * - :smtk:`smtk::attribute::Resource::AssociationRole` + - Link objects to an *instance* of an attribute to + indicate they possess, exhibit, or manifest the attribute. + * - :smtk:`smtk::attribute::Resource::ReferenceRole` + - Link objects to an *item* of an attribute to indicate the + item contains or references the object. + * - :smtk:`smtk::mesh::Resource::ClassificationRole` + - Link a mesh-set object to a source component whose geometry + the mesh-set discretizes. + * - :smtk:`smtk::resource::Resource::VisuallyLinkedRole` + - Link objects to one another to indicate the source + (left-hand) object (which does not have renderable geometry) + should be provided with visibility controls for the target + (right-hand) object(s) (which should have renderable geometry). + +Persistent objects in the link system are referenced by their UUID. +This allows links to refer to resources and components not currently +loaded into an application (for instance, large meshes do not need +to be loaded to gather metadata on a simulation that uses the meshes +as domain discretizations). +The :smtk:`smtk::resource::Surrogate` class acts as a placeholder for +resources not currently loaded. + +Links may have :smtk:`information ` stored +with them (such as human-presentable names) so that resources not in memory +can still be displayed to users to provide context. + +Links are stored in a two-level hierarchy of :smtk:`smtk::common::Links` instances: +the top-level Links instance holds connections between pairs of resources. +Resources connected at the top level always have a link role of ``smtk::resource::Links::TopLevel``. +Each top-level Link (singular) inherits a second Links (via the ``base_type`` template parameter) +instance holding all of the connections beteen resources and/or components. +In the bottom-level Links container, the user-provided roles are stored. +Resources are identified by null UUIDs for the ``left`` or ``right`` members +while components are identified by their UUID. diff --git a/smtk/attribute/Resource.h b/smtk/attribute/Resource.h index 340badd6f..76960614a 100644 --- a/smtk/attribute/Resource.h +++ b/smtk/attribute/Resource.h @@ -87,10 +87,18 @@ class SMTKCORE_EXPORT Resource /// Link from an attribute to persistent objects /// which manifest, exhibit, or possess the attribute. static constexpr smtk::resource::Links::RoleType AssociationRole = -1; + static constexpr smtk::resource::Links::RoleType associationRole() + { + return Resource::AssociationRole; + } /// Link from an attribute item to persistent objects /// which the item contains or references. static constexpr smtk::resource::Links::RoleType ReferenceRole = -2; + static constexpr smtk::resource::Links::RoleType referenceRole() + { + return Resource::ReferenceRole; + } ~Resource() override; diff --git a/smtk/attribute/pybind11/PybindAttribute.cxx b/smtk/attribute/pybind11/PybindAttribute.cxx index 83dac3cb3..f7feb2224 100644 --- a/smtk/attribute/pybind11/PybindAttribute.cxx +++ b/smtk/attribute/pybind11/PybindAttribute.cxx @@ -30,8 +30,8 @@ PYBIND11_MODULE(_smtkPybindAttribute, attribute) { attribute.doc() = ""; - py::module::import("smtk.common"); - py::module::import("smtk.resource"); + auto sc = py::module::import("smtk.common"); + auto sr = py::module::import("smtk.resource"); attributePart1(attribute); attributePart2(attribute); diff --git a/smtk/attribute/pybind11/PybindResource.h b/smtk/attribute/pybind11/PybindResource.h index b4c32ca08..ca7292936 100644 --- a/smtk/attribute/pybind11/PybindResource.h +++ b/smtk/attribute/pybind11/PybindResource.h @@ -107,6 +107,8 @@ inline PySharedPtrClass< smtk::attribute::Resource> pybind11_init_smtk_attribute }) .def("copyAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(const smtk::attribute::AttributePtr& att, const smtk::attribute::CopyAssignmentOptions&)) &smtk::attribute::Resource::copyAttribute, py::arg("sourceAttribute"), py::arg("options") = smtk::attribute::CopyAssignmentOptions()) .def("copyAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(const smtk::attribute::AttributePtr& att, const smtk::attribute::CopyAssignmentOptions&, smtk::io::Logger&)) &smtk::attribute::Resource::copyAttribute, py::arg("sourceAttribute"), py::arg("options"), py::arg("logger")) + .def_static("associationRole", &smtk::attribute::Resource::associationRole) + .def_static("referenceRole", &smtk::attribute::Resource::referenceRole) ; return instance; } diff --git a/smtk/common/Links.h b/smtk/common/Links.h index 4b0c7048c..5e8478b11 100644 --- a/smtk/common/Links.h +++ b/smtk/common/Links.h @@ -397,6 +397,16 @@ class Links : public detail::LinkContainer visitor) const + { + const auto& self = this->Parent::template get(); + for (const auto& entry : self) + { + visitor(entry.id); + } + } + /// Return a set of ids corresponding to the input value for the tagged search /// criterion. template diff --git a/smtk/markup/Component.cxx b/smtk/markup/Component.cxx index 58620a2a4..33ba64a70 100644 --- a/smtk/markup/Component.cxx +++ b/smtk/markup/Component.cxx @@ -39,7 +39,7 @@ Component::~Component() = default; void Component::initialize(const nlohmann::json& data, smtk::resource::json::Helper& helper) { (void)helper; - std::cout << "init comp " << this << " with " << data.dump(2) << "\n"; + // std::cout << "init comp " << this << " with " << data.dump(2) << "\n"; auto it = data.find("name"); if (it != data.end()) { diff --git a/smtk/mesh/core/Resource.h b/smtk/mesh/core/Resource.h index e683e4133..f7e9dd502 100644 --- a/smtk/mesh/core/Resource.h +++ b/smtk/mesh/core/Resource.h @@ -81,6 +81,10 @@ class SMTKCORE_EXPORT Resource /// A mesh resource may be classified to a model, indicating the mesh set /// discretizes the linked model component. static constexpr smtk::resource::Links::RoleType ClassificationRole = -3; + static constexpr smtk::resource::Links::RoleType classificationRole() + { + return Resource::ClassificationRole; + } static smtk::shared_ptr create(const smtk::common::UUID& resourceID) { diff --git a/smtk/mesh/pybind11/PybindResource.h b/smtk/mesh/pybind11/PybindResource.h index e17d574d0..2ef497554 100644 --- a/smtk/mesh/pybind11/PybindResource.h +++ b/smtk/mesh/pybind11/PybindResource.h @@ -90,6 +90,7 @@ inline PySharedPtrClass< smtk::mesh::Resource> pybind11_init_smtk_mesh_Resource( .def("writeLocation", (void (smtk::mesh::Resource::*)(::smtk::common::FileLocation const &)) &smtk::mesh::Resource::writeLocation, py::arg("path")) .def("writeLocation", (void (smtk::mesh::Resource::*)(::std::string const &)) &smtk::mesh::Resource::writeLocation, py::arg("path")) .def("writeLocation", (smtk::common::FileLocation const & (smtk::mesh::Resource::*)() const) &smtk::mesh::Resource::writeLocation) + .def_static("classificationRole", &smtk::mesh::Resource::classificationRole) .def_static("CastTo", [](const std::shared_ptr i) { return std::dynamic_pointer_cast(i); }) diff --git a/smtk/resource/ComponentLinks.h b/smtk/resource/ComponentLinks.h index 375a4c8bb..db52e1d05 100644 --- a/smtk/resource/ComponentLinks.h +++ b/smtk/resource/ComponentLinks.h @@ -46,6 +46,8 @@ class SMTKCORE_EXPORT ComponentLinks : public Links friend class smtk::resource::Component; + ~ComponentLinks() override = default; + private: ComponentLinks(const Component*); diff --git a/smtk/resource/Links.cxx b/smtk/resource/Links.cxx index 31bcf2b9a..e5f81e39c 100644 --- a/smtk/resource/Links.cxx +++ b/smtk/resource/Links.cxx @@ -199,8 +199,9 @@ Links::Key Links::addLinkTo( } resourceLinkData.insert( - ResourceLinkData::LinkBase(rhs1), resourceLinkId, lhs1->id(), rhs1->id(), role); + ResourceLinkData::LinkBase(rhs1), resourceLinkId, lhs1->id(), rhs1->id(), topLevelRole()); componentLinkData = &resourceLinkData.value(resourceLinkId); + resourceRange = resourceLinks.equal_range(rhs1->id()); } else { @@ -407,7 +408,7 @@ std::pair Links::linkedObjectAndRole( const ResourceLinkData& resourceLinkData = lhs1->links().data(); if (!resourceLinkData.contains(key.first)) { - return std::make_pair(ResourcePtr(), invalidRoleType()); + return std::make_pair(ResourcePtr(), invalidRole()); } const auto& resourceLink = resourceLinkData.value(key.first); @@ -446,7 +447,7 @@ std::pair Links::linkedObjectIdAndRole( const ResourceLinkData& resourceLinkData = lhs1->links().data(); if (!resourceLinkData.contains(key.first)) { - return std::make_pair(smtk::common::UUID::null(), invalidRoleType()); + return std::make_pair(smtk::common::UUID::null(), invalidRole()); } const auto& resourceLink = resourceLinkData.value(key.first); diff --git a/smtk/resource/Links.h b/smtk/resource/Links.h index 7df2fe69c..f6a4e6d8e 100644 --- a/smtk/resource/Links.h +++ b/smtk/resource/Links.h @@ -31,13 +31,26 @@ struct LinkInformation; class SMTKCORE_EXPORT Links { public: + /// Force this class to be polymorphic. + virtual ~Links() = default; + /// A Key is a pair of UUIDs. the First UUID is the id of the resource link, /// and the second one is the id of the component link. typedef std::pair Key; typedef int RoleType; - /// Special RoleType indicating that the link is invalid - static RoleType invalidRoleType() { return std::numeric_limits::lowest(); } + /// A special role used to link pairs of resources together. + /// + /// All top-level links between resources use this role; + /// if you intend to link two resources in a specific role, + /// this is stored by a second Link in the bottom-level + /// Links container between null UUIDs. + static constexpr RoleType TopLevelRole = std::numeric_limits::lowest(); + static constexpr RoleType topLevelRole() { return Links::TopLevelRole; } + + /// A special Role indicating that a link is invalid. + static constexpr RoleType InvalidRole = std::numeric_limits::lowest() + 1; + static constexpr RoleType invalidRole() { return Links::InvalidRole; } /// Given a resource or component and a role, check if a link of this role type /// exists between us and the input object. @@ -96,6 +109,17 @@ class SMTKCORE_EXPORT Links virtual const smtk::common::UUID& leftHandSideComponentId() const; + /// This is protected so that ResourceLinks::copyLinks() can call it. + /// + /// In general, you should not call this method directly; instead, call + /// `addLinkTo(Component, RoleType)` or `addLinkTo(Resource, RoleType)`. + Key addLinkTo( + Resource* lhs1, + const smtk::common::UUID& lhs2, + const ResourcePtr& rhs1, + const smtk::common::UUID& rhs2, + const RoleType& role); + private: bool isLinkedTo( const Resource* lhs1, @@ -104,13 +128,6 @@ class SMTKCORE_EXPORT Links const smtk::common::UUID& rhs2, const RoleType& role) const; - Key addLinkTo( - Resource* lhs1, - const smtk::common::UUID& lhs2, - const ResourcePtr& rhs1, - const smtk::common::UUID& rhs2, - const RoleType& role); - PersistentObjectSet linkedTo(const Resource* lhs1, const smtk::common::UUID& lhs2, const RoleType& role) const; diff --git a/smtk/resource/Resource.cxx b/smtk/resource/Resource.cxx index 8b2323963..54a74c87a 100644 --- a/smtk/resource/Resource.cxx +++ b/smtk/resource/Resource.cxx @@ -302,44 +302,7 @@ void Resource::copyUnitSystem( void Resource::copyLinks(const std::shared_ptr& rsrc, const CopyOptions& options) { - if (!options.copyLinks()) - { - // Skip copying link data if told to. - return; - } - - // Only copy link-data if shouldExcludeLinksInRole(role) is false. - const auto& sourceLinks = rsrc->links().data(); - auto& targetLinks = this->links().data(); - const auto& sourceLinksById = sourceLinks.get(); - for (const auto& sourceLink : sourceLinksById) - { - if ( - options.shouldExcludeLinksInRole(sourceLink.role) || options.shouldOmitId(sourceLink.left) || - options.shouldOmitId(sourceLink.right) || options.shouldOmitId(sourceLink.id) || - // Ignore links with invalid roles - sourceLink.role == Links::invalidRoleType()) - { - continue; - } - // We need to copy this link. Transform its left- and right-hand side IDs. - smtk::common::Link< - smtk::common::UUID, - smtk::common::UUID, - smtk::common::UUID, - int, - smtk::resource::detail::ResourceLinkBase> - targetEntry(sourceLink); - if (auto* target = options.targetObjectFromSourceId(targetEntry.left)) - { - targetEntry.left = target->id(); - } - if (auto* target = options.targetObjectFromSourceId(targetEntry.right)) - { - targetEntry.right = target->id(); - } - targetLinks.insert(targetEntry); - } + this->links().copyFrom(rsrc, options); } void Resource::copyProperties(const std::shared_ptr& rsrc, CopyOptions& options) diff --git a/smtk/resource/Resource.h b/smtk/resource/Resource.h index a1de0f9a6..18b26adb2 100644 --- a/smtk/resource/Resource.h +++ b/smtk/resource/Resource.h @@ -93,6 +93,10 @@ class SMTKCORE_EXPORT Resource : public PersistentObject /// visibility-control badges for both A and B. This is indicated by a /// link from A to B. static constexpr smtk::resource::Links::RoleType VisuallyLinkedRole = -4; + static constexpr smtk::resource::Links::RoleType visuallyLinkedRole() + { + return Resource::VisuallyLinkedRole; + } smtkTypeMacro(smtk::resource::Resource); smtkSuperclassMacro(smtk::resource::PersistentObject); diff --git a/smtk/resource/ResourceLinks.cxx b/smtk/resource/ResourceLinks.cxx index 829e4089f..56cffe151 100644 --- a/smtk/resource/ResourceLinks.cxx +++ b/smtk/resource/ResourceLinks.cxx @@ -10,12 +10,19 @@ #include "smtk/resource/ResourceLinks.h" +#include "smtk/resource/ComponentLinks.h" +#include "smtk/resource/CopyOptions.h" +#include "smtk/resource/Manager.h" + #include "smtk/common/UUID.h" #include "smtk/resource/Resource.h" #include +// Uncomment to get debug printouts +// #define SMTK_DBG_COPYLINKS + namespace smtk { namespace resource @@ -59,6 +66,118 @@ bool ResourceLinks::removeAllLinksTo(const ResourcePtr& resource) { return this->data().erase_all(resource->id()); } + +void ResourceLinks::copyFrom( + const ConstResourcePtr& source, + const smtk::resource::CopyOptions& options) +{ + if (!options.copyLinks() || !source || !m_resource) + { + // Skip copying link data if told to. + return; + } + + smtk::resource::Manager::Ptr rsrcMgr = m_resource->manager(); + if (!rsrcMgr) + { + rsrcMgr = source->manager(); + } + + // First, copy top-level, resource-to-resource connections. + // These are always copied, even if no component/resource links + // underneath them are copied. We may add an option later to + // avoid this. + const auto& srcData = source->links().data(); + for (const auto& entry : srcData) + { + auto targetRightResourceId = entry.right; + if (options.shouldOmitId(targetRightResourceId)) + { + smtkInfoMacro(options.log(), "Skipping omitted resource " << targetRightResourceId << "."); + continue; + } + Resource::Ptr targetRightResource; + if (targetRightResourceId == source->id()) + { + targetRightResourceId = m_resource->id(); + targetRightResource = const_pointer_cast(m_resource->shared_from_this()); + } + else if (auto* mapped = options.targetObjectFromSourceId(targetRightResourceId)) + { + targetRightResourceId = mapped->id(); + targetRightResource = mapped->shared_from_this(); + } + else + { + // Look up resource in resource manager or omit the + // copied links; we need the resource to be loaded + // to copy the links properly since they include pointers. + if (rsrcMgr) + { + targetRightResource = rsrcMgr->get(targetRightResourceId); + } + // TODO: Handle Surrogates by fetching? Add a copy-option to avoid fetching? + if (!targetRightResource) + { + smtkWarningMacro(options.log(), "No resource found for " << targetRightResourceId << "."); + continue; + } + } + // Iterate over children of top-level link. +#ifdef SMTK_DBG_COPYLINKS + std::cerr << "Translate " << entry.left << " → " << entry.right << " (" << entry.role << ")\n" + << " into " << m_resource->id() << " → " << targetRightResourceId << "\n"; +#endif + for (const auto& subentry : entry.get()) + { + // Check whether to copy the sublink. + if ( + options.shouldExcludeLinksInRole(subentry.role) || options.shouldOmitId(subentry.left) || + options.shouldOmitId(subentry.right) || subentry.role == Links::invalidRole()) + { + smtkInfoMacro( + options.log(), + "Skipping link (" << subentry.left << " → " << subentry.right << " " << subentry.role + << ") due to component in 'omit' list " + << "or excluded/invalid role."); + continue; + } + auto idLf = subentry.left; + auto idRt = subentry.right; + auto* mappedLf = options.targetObjectFromSourceId(idLf); + auto* mappedRt = options.targetObjectFromSourceId(idRt); + if (mappedLf) + { + idLf = mappedLf->id(); + } + else if (!mappedLf && subentry.left) + { + // We should never copy a link whose left ("from") entry is not present in the + // target resource. Verify that idLf exists as a component in our resource. + if (!m_resource->component(idLf)) + { + smtkWarningMacro( + options.log(), + "Left-hand side UUID " << subentry.left + << " has no mapping and " + "is not in target resource. Skipping."); + continue; + } + } + if (mappedRt) + { + idRt = mappedRt->id(); + } +#ifdef SMTK_DBG_COPYLINKS + std::cerr << " Sublink " << subentry.left << " → " << subentry.right << " (" + << subentry.role << ")\n" + << " to " << idLf << " → " << idRt << " (" << subentry.role << ")\n"; +#endif + this->addLinkTo(m_resource, idLf, targetRightResource, idRt, subentry.role); + } + } +} + } // namespace detail } // namespace resource } // namespace smtk diff --git a/smtk/resource/ResourceLinks.h b/smtk/resource/ResourceLinks.h index fcd3d756b..5e6453a92 100644 --- a/smtk/resource/ResourceLinks.h +++ b/smtk/resource/ResourceLinks.h @@ -28,6 +28,7 @@ namespace smtk { namespace resource { +class CopyOptions; class Resource; namespace detail @@ -82,7 +83,7 @@ class SMTKCORE_EXPORT ResourceLinks : public Links typedef ResourceLinkData::Link Link; - ~ResourceLinks(); + ~ResourceLinks() override; /// Access the underlying link data. ResourceLinkData& data() { return m_data; } @@ -96,6 +97,14 @@ class SMTKCORE_EXPORT ResourceLinks : public Links /// when we know the resource parameter is being permanently deleted. bool removeAllLinksTo(const ResourcePtr&); + /// Copy links from \a source's links with the given \a options. + /// + /// This will copy both resource and component links as \a options indicates. + /// Both resources and components are mapped through the \a options' object-mapping, + /// so any links to objects that are also being copied will point to the copy + /// and not the original object in \a source. + void copyFrom(const ConstResourcePtr& source, const smtk::resource::CopyOptions& options); + private: ResourceLinks(Resource*); diff --git a/smtk/resource/pybind11/PybindComponent.h b/smtk/resource/pybind11/PybindComponent.h index 99422c678..9b9cc6f66 100644 --- a/smtk/resource/pybind11/PybindComponent.h +++ b/smtk/resource/pybind11/PybindComponent.h @@ -14,6 +14,7 @@ #include #include "smtk/resource/Component.h" +#include "smtk/resource/ComponentLinks.h" #include "smtk/resource/PersistentObject.h" #include "smtk/resource/Resource.h" @@ -29,6 +30,7 @@ inline PySharedPtrClass< smtk::resource::Component, smtk::resource::PyComponent, PySharedPtrClass< smtk::resource::Component, smtk::resource::PyComponent, smtk::resource::PersistentObject > instance(m, "Component"); instance .def(py::init<>()) + .def("links", (smtk::resource::Component::Links& (smtk::resource::Component::*)()) &smtk::resource::Component::links, py::return_value_policy::reference_internal) .def("deepcopy", (smtk::resource::Component & (smtk::resource::Component::*)(::smtk::resource::Component const &)) &smtk::resource::Component::operator=) .def("resource", &smtk::resource::Component::resource) .def("stringProperties", [](smtk::resource::Component& component) diff --git a/smtk/resource/pybind11/PybindComponentLinks.h b/smtk/resource/pybind11/PybindComponentLinks.h index e2d290ae8..87207d5bb 100644 --- a/smtk/resource/pybind11/PybindComponentLinks.h +++ b/smtk/resource/pybind11/PybindComponentLinks.h @@ -25,7 +25,6 @@ inline py::class_< smtk::resource::detail::ComponentLinkBase > pybind11_init_smt instance .def(py::init<>()) .def(py::init<::smtk::resource::detail::ComponentLinkBase const &>()) - .def("deepcopy", (smtk::resource::detail::ComponentLinkBase & (smtk::resource::detail::ComponentLinkBase::*)(::smtk::resource::detail::ComponentLinkBase const &)) &smtk::resource::detail::ComponentLinkBase::operator=) ; return instance; } @@ -35,7 +34,6 @@ inline py::class_< smtk::resource::detail::ComponentLinks, smtk::resource::Links py::class_< smtk::resource::detail::ComponentLinks, smtk::resource::Links > instance(m, "ComponentLinks"); instance .def(py::init<::smtk::resource::detail::ComponentLinks const &>()) - .def("deepcopy", (smtk::resource::detail::ComponentLinks & (smtk::resource::detail::ComponentLinks::*)(::smtk::resource::detail::ComponentLinks const &)) &smtk::resource::detail::ComponentLinks::operator=) ; return instance; } diff --git a/smtk/resource/pybind11/PybindCopyOptions.h b/smtk/resource/pybind11/PybindCopyOptions.h index 1d5cc4419..1533ee3c8 100644 --- a/smtk/resource/pybind11/PybindCopyOptions.h +++ b/smtk/resource/pybind11/PybindCopyOptions.h @@ -52,6 +52,24 @@ inline py::class_< smtk::resource::CopyOptions > pybind11_init_smtk_resource_Cop .def("shouldOmitId", &smtk::resource::CopyOptions::shouldOmitId) .def("omitComponents", &smtk::resource::CopyOptions::omitComponents, py::arg("resource")) .def("objectMapping", (smtk::resource::CopyOptions::ObjectMapType& (smtk::resource::CopyOptions::*)())&smtk::resource::CopyOptions::objectMapping) + .def("targetObjectFromSourceId", [](const smtk::resource::CopyOptions& self, const smtk::common::UUID& uid) + { + auto* obj = self.targetObjectFromSourceId(uid); + auto optr = obj ? obj->shared_from_this() : nullptr; + return optr; + }, py::arg("id")) + .def("targetComponentFromSourceId", [](const smtk::resource::CopyOptions& self, const smtk::common::UUID& uid) + { + auto* obj = self.targetObjectFromSourceId(uid); + auto optr = obj ? obj->shared_from_this() : nullptr; + return optr; + }, py::arg("id")) + .def("targetResourceFromSourceId", [](const smtk::resource::CopyOptions& self, const smtk::common::UUID& uid) + { + auto* obj = self.targetObjectFromSourceId(uid); + auto optr = obj ? obj->shared_from_this() : nullptr; + return optr; + }, py::arg("id")) .def("log", &smtk::resource::CopyOptions::log) ; return instance; diff --git a/smtk/resource/pybind11/PybindLinks.h b/smtk/resource/pybind11/PybindLinks.h new file mode 100644 index 000000000..552df2a0f --- /dev/null +++ b/smtk/resource/pybind11/PybindLinks.h @@ -0,0 +1,72 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#ifndef pybind_smtk_resource_Links_h +#define pybind_smtk_resource_Links_h + +#include + +#include "smtk/resource/Links.h" +#include "smtk/resource/Resource.h" +#include "smtk/common/UUID.h" +#include "smtk/common/pybind11/PybindUUIDTypeCaster.h" +#include "smtk/resource/Component.h" +#include "smtk/resource/Lock.h" +#include "smtk/resource/Manager.h" +#include "smtk/resource/PersistentObject.h" + +namespace py = pybind11; + +inline py::class_< smtk::resource::Links> pybind11_init_smtk_resource_Links(py::module &m) +{ + py::class_< smtk::resource::Links> instance(m, "Links"); + instance + .def_static("invalidRole", &smtk::resource::Links::invalidRole) + .def_static("topLevelRole", &smtk::resource::Links::topLevelRole) + .def("isLinkedTo", (bool (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&) const) &smtk::resource::Links::isLinkedTo) + .def("isLinkedTo", (bool (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&) const) &smtk::resource::Links::isLinkedTo) + .def("addLinkTo", (smtk::resource::Links::Key (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&)) &smtk::resource::Links::addLinkTo) + .def("addLinkTo", (smtk::resource::Links::Key (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&)) &smtk::resource::Links::addLinkTo) + .def("linkedTo", (smtk::resource::PersistentObjectSet (smtk::resource::Links::*)(const smtk::resource::Links::RoleType&) const) &smtk::resource::Links::linkedTo, py::arg("role")) + .def("linkedFrom", (smtk::resource::PersistentObjectSet (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&) const) &smtk::resource::Links::linkedFrom, py::arg("resource"), py::arg("role")) + .def("removeLink", [](smtk::resource::Links& self, const smtk::common::UUID& rr, const smtk::common::UUID& rc) + { + smtk::resource::Links::Key key(rr, rc); + return self.removeLink(key); + }, py::arg("rhs_resource"), py::arg("rhs_component")) + .def("removeLinksTo", (bool (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&)) &smtk::resource::Links::removeLinksTo) + .def("removeLinksTo", (bool (smtk::resource::Links::*)(const std::shared_ptr&, const smtk::resource::Links::RoleType&)) &smtk::resource::Links::removeLinksTo) + + .def("linkedObjectAndRole", [](const smtk::resource::Links& self, const smtk::common::UUID& rr, const smtk::common::UUID& rc) + { + smtk::resource::Links::Key key(rr, rc); + return self.linkedObjectAndRole(key); + }, py::arg("rhs_resource"), py::arg("rhs_component")) + .def("linkedObject", [](const smtk::resource::Links& self, const smtk::common::UUID& rr, const smtk::common::UUID& rc) + { + smtk::resource::Links::Key key(rr, rc); + return self.linkedObject(key); + }, py::arg("rhs_resource"), py::arg("rhs_component")) + + .def("linkedObjectIdAndRole", [](const smtk::resource::Links& self, const smtk::common::UUID& rr, const smtk::common::UUID& rc) + { + smtk::resource::Links::Key key(rr, rc); + return self.linkedObjectIdAndRole(key); + }, py::arg("rhs_resource"), py::arg("rhs_component")) + .def("linkedObjectId", [](const smtk::resource::Links& self, const smtk::common::UUID& rr, const smtk::common::UUID& rc) + { + smtk::resource::Links::Key key(rr, rc); + return self.linkedObjectId(key); + }, py::arg("rhs_resource"), py::arg("rhs_component")) + ; + return instance; +} + +#endif diff --git a/smtk/resource/pybind11/PybindResource.cxx b/smtk/resource/pybind11/PybindResource.cxx index c949e98eb..21bfb63e2 100644 --- a/smtk/resource/pybind11/PybindResource.cxx +++ b/smtk/resource/pybind11/PybindResource.cxx @@ -23,13 +23,16 @@ template using PySharedPtrClass = py::class_, Args...>; #include "PybindComponent.h" +#include "PybindComponentLinks.h" #include "PybindCopyOptions.h" +#include "PybindLinks.h" #include "PybindManager.h" #include "PybindObserver.h" #include "PybindPersistentObject.h" #include "PybindProperties.h" #include "PybindPropertyType.h" #include "PybindResource.h" +#include "PybindResourceLinks.h" #include "PybindRegistrar.h" PYBIND11_DECLARE_HOLDER_TYPE(T, std::shared_ptr); @@ -44,6 +47,11 @@ PYBIND11_MODULE(_smtkPybindResource, resource) // comparing the dependencies of each of the wrapped objects. auto smtk_resource_CopyOptions = pybind11_init_smtk_resource_CopyOptions(resource); auto smtk_resource_PersistentObject = pybind11_init_smtk_resource_PersistentObject(resource); + auto smtk_resource_Links = pybind11_init_smtk_resource_Links(resource); + auto smtk_resource_detail_ComponentLinkBase = pybind11_init_smtk_resource_detail_ComponentLinkBase(resource); + auto smtk_resource_detail_ResourceLinkBase = pybind11_init_smtk_resource_detail_ResourceLinkBase(resource); + auto smtk_resource_ComponentLinks = pybind11_init_smtk_resource_detail_ComponentLinks(resource); + auto smtk_resource_ResourceLinks = pybind11_init_smtk_resource_detail_ResourceLinks(resource); auto smtk_resource_Properties = pybind11_init_smtk_resource_Properties(resource); auto smtk_resource_Resource = pybind11_init_smtk_resource_Resource(resource); auto smtk_resource_Component = pybind11_init_smtk_resource_Component(resource); diff --git a/smtk/resource/pybind11/PybindResource.h b/smtk/resource/pybind11/PybindResource.h index 56d2c9c23..32a163ade 100644 --- a/smtk/resource/pybind11/PybindResource.h +++ b/smtk/resource/pybind11/PybindResource.h @@ -150,6 +150,8 @@ inline PySharedPtrClass< smtk::resource::Resource, smtk::resource::PyResource, s .def("setName", &smtk::resource::Resource::setName, py::arg("name")) .def("typeName", &smtk::resource::Resource::typeName) .def("visit", &smtk::resource::Resource::visit, py::arg("v")) + .def_static("visuallyLinkedRole", &smtk::resource::Resource::visuallyLinkedRole) + .def_readonly_static("VisuallyLinkedRole", &smtk::resource::Resource::VisuallyLinkedRole) .def_readonly_static("type_index", &smtk::resource::Resource::type_index) .def_readonly_static("type_name", &smtk::resource::Resource::type_name) ; diff --git a/smtk/resource/pybind11/PybindResourceLinks.h b/smtk/resource/pybind11/PybindResourceLinks.h new file mode 100644 index 000000000..2d928dd64 --- /dev/null +++ b/smtk/resource/pybind11/PybindResourceLinks.h @@ -0,0 +1,34 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#ifndef pybind_smtk_resource_ResourceLinks_h +#define pybind_smtk_resource_ResourceLinks_h + +#include + +#include "smtk/resource/ResourceLinks.h" + +#include "smtk/resource/Links.h" + +namespace py = pybind11; + +inline py::class_< smtk::resource::detail::ResourceLinkBase > pybind11_init_smtk_resource_detail_ResourceLinkBase(py::module &m) +{ + py::class_< smtk::resource::detail::ResourceLinkBase > instance(m, "ResourceLinkBase"); + return instance; +} + +inline py::class_< smtk::resource::detail::ResourceLinks, smtk::resource::Links > pybind11_init_smtk_resource_detail_ResourceLinks(py::module &m) +{ + py::class_< smtk::resource::detail::ResourceLinks, smtk::resource::Links > instance(m, "ResourceLinks"); + return instance; +} + +#endif diff --git a/smtk/resource/testing/python/testCloneResources.py b/smtk/resource/testing/python/testCloneResources.py index f51d200c5..f5c088faa 100644 --- a/smtk/resource/testing/python/testCloneResources.py +++ b/smtk/resource/testing/python/testCloneResources.py @@ -11,11 +11,12 @@ # ============================================================================= import os import smtk +import smtk.common +import smtk.resource import smtk.attribute import smtk.graph import smtk.string import smtk.io -import smtk.resource import smtk.testing import sys @@ -28,43 +29,65 @@ sys.exit(smtk.testing.SKIP_ENTIRE) +def printLogOnFail(ok, opts): + if not ok: + print('Log output:\n', opts.log().convertToString(True), '\n') + return ok + + class TestCloneResources(smtk.testing.TestCase): def setUp(self): + # Reset the smtk.common.Managers object used by smtk.read() to eliminate + # previously-read resources from memory: + if smtk.appContext: + rsrcMgr = smtk.appContext.get('smtk.resource.Manager') + for rsrc in rsrcMgr.resources(): + rsrcMgr.remove(rsrc) + # Read in the original attribute resource attFilename = os.path.join( smtk.testing.DATA_DIR, 'attribute', 'attribute_collection', 'cloneTest.smtk') - rr = smtk.read(attFilename) - self.origAttResource = rr[0] + self.origAttResource = smtk.read(attFilename)[0] self.origAttResource.setName('attResource') # Read in the original markup resource markupFilename = os.path.join( smtk.testing.DATA_DIR, 'model', '3d', 'smtk', 'coarse-knee.smtk') - rr = smtk.read(markupFilename) - self.origMarkupResource = rr[0] + self.origMarkupResource = smtk.read(markupFilename)[0] # add a resource property to xx to test that it gets copied # even when component properties are not. self.origMarkupResource.stringProperties().set('foo', 'bar') self.origAttResource.stringProperties().set('foo', 'bar') # Let's associate the markup resource to the attribute resource - self.origAttResource.associate(self.origMarkupResource) + self.assertTrue(self.origAttResource.associate(self.origMarkupResource), + 'Could not associatte attribute resource to markup resource.') compset = self.origMarkupResource.filter( 'smtk::markup::UnstructuredData') - att = self.origAttResource.findAttribute("Test Attribute") + att = self.origAttResource.findAttribute('Test Attribute') refitem = att.associations() refitem.setValue(0, compset.pop()) # The original attribute did not have any active categories or analyses so let's add some - self.origAttResource.setActiveCategories({"A", "B"}) + self.origAttResource.setActiveCategories({'A', 'B'}) self.origAttResource.setActiveCategoriesEnabled(True) - analysis = self.origAttResource.analyses().create("foo") - analysis.setLocalCategories({"A"}) + analysis = self.origAttResource.analyses().create('foo') + analysis.setLocalCategories({'A'}) + + # Create a "visual link" between an attribute and a markup component. + self.originalMarkupComp = compset.pop() + mcomp = self.originalMarkupComp + att.links().addLinkTo(mcomp, smtk.resource.Resource.visuallyLinkedRole()) + self.assertTrue(att.links().isLinkedTo(mcomp, smtk.resource.Resource.visuallyLinkedRole()), + 'Could not visually link attribute to markup component.') + print('------- Setup Results --------') - print(' +++++ Original Attribute Resource ++++++') - self.printResource(self.origAttResource) - print(' +++++ Original Markup Resource ++++++') - self.printResource(self.origMarkupResource) + print(self.origMarkupResource.id(), self.origMarkupResource.name()) + print(self.originalMarkupComp.id(), self.originalMarkupComp.name()) + print(self.origAttResource.id(), self.origAttResource.name()) + print(att.id(), att.name()) + print('Visually linked', mcomp.name(), 'to', att.name()) + self.printAttInfo(self.origAttResource) def printResource(self, resource): print(resource, resource.id()) @@ -92,17 +115,17 @@ def printAttInfo(self, resource): print(' active categories enabled:', resource.activeCategoriesEnabled()) # Find the test attribute - att = resource.findAttribute("Test Attribute") + att = resource.findAttribute('Test Attribute') refitem = att.associations() print(' association: ', refitem.value( 0).name(), refitem.value(0).id(), '\n') - d0 = att.findDouble("d0") - d1 = att.findDouble("d1") + d0 = att.findDouble('d0') + d1 = att.findDouble('d1') print(' d0 = ', d0.value(0), ' d1 = {', d1.expression().name(), d1.expression().id(), '}\n') - view = resource.findView("AttributeResourceCloneTest") - val = view.details().child(0).child(0).attributeAsString("ID") - if view.details().child(0).child(0).attribute("ID"): + view = resource.findView('AttributeResourceCloneTest') + val = view.details().child(0).child(0).attributeAsString('ID') + if view.details().child(0).child(0).attribute('ID'): print(' View: ', view.name(), ' has Attribute ID: ', val) else: print(' View: ', view.name(), ' does not have an ID Attribute') @@ -114,23 +137,19 @@ def testCloneAttributeResourceOnly(self): clonedAtts = self.origAttResource.clone(opts) - if not clonedAtts.copyInitialize(self.origAttResource, opts): - print('Logger Output:\n') - print(smtk.io.Logger.instance().convertToString(True), '\n') - raise RuntimeError( - 'Copy Initialized Failed for Attribute Resource') - - print('Copy Initialized for Attributes Worked.') - if not clonedAtts.copyFinalize(self.origAttResource, opts): - print('Logger Output:\n') - print(smtk.io.Logger.instance().convertToString(True), '\n') - raise RuntimeError( - 'Copy Finalized Failed for Attribute Resource') + self.assertTrue( + printLogOnFail(clonedAtts.copyInitialize( + self.origAttResource, opts), opts), + 'copyInitialize Failed for attribute resource') + self.assertTrue( + printLogOnFail(clonedAtts.copyFinalize( + self.origAttResource, opts), opts), + 'copyFinalize Failed for attribute resource') - print('Copy Finalized for Attributes Worked.') - print(' +++++ Cloned Attribute Resource ++++++') - self.printResource(clonedAtts) - self.compareClonedAttResource(clonedAtts, 1) + # print(' +++++ Cloned Attribute Resource ++++++') + # self.printResource(clonedAtts) + self.printAttInfo(clonedAtts) + self.compareClonedAttResource(clonedAtts, True, opts) def testCloneAllResource(self): print('-------- Testing Copying All Resources --------') @@ -139,107 +158,109 @@ def testCloneAllResource(self): clonedAtts = self.origAttResource.clone(opts) clonedMarkup = self.origMarkupResource.clone(opts) - if not clonedAtts.copyInitialize(self.origAttResource, opts): - print('Logger Output:\n') - print(smtk.io.Logger.instance().convertToString(True), '\n') - raise RuntimeError( - 'Copy Initialized Failed for Attribute Resource') - - if not clonedMarkup.copyInitialize(self.origMarkupResource, opts): - print('Logger Output:\n') - print(smtk.io.Logger.instance().convertToString(True), '\n') - raise RuntimeError('Copy Initialized Failed for Markup Resource') - - print('Copy Initialized for Resources Worked.') - - if not clonedAtts.copyFinalize(self.origAttResource, opts): - print('Logger Output:\n') - print(smtk.io.Logger.instance().convertToString(True), '\n') - raise RuntimeError('Copy Finalized Failed for Attribute Resource') - - if not clonedMarkup.copyFinalize(self.origMarkupResource, opts): - print('Logger Output:\n') - print(smtk.io.Logger.instance().convertToString(True), '\n') - raise RuntimeError('Copy Finalized Failed for Markup Resource') - - print('Copy Finalized for Resources Worked.') - print(' +++++ Cloned Attribute Resource ++++++') - self.printResource(clonedAtts) - print(' +++++ Cloned Markup Resource ++++++') - self.printResource(clonedMarkup) - self.compareClonedAttResource(clonedAtts, 0) - - def compareClonedAttResource(self, attRes, sameMarkup): - if attRes.stringProperties().contains('foo'): - if attRes.stringProperties().at('foo') != 'bar': - raise RuntimeError( - 'Copied Attribute Resource do not have a string property foo equal to bar') - else: - raise RuntimeError( - 'Copied Attribute Resource do not have a string property foo') - if attRes.activeCategoriesEnabled() != self.origAttResource.activeCategoriesEnabled(): - raise RuntimeError( - 'Attribute Resources do not have the same active categories enabled option') - if attRes.activeCategories() != self.origAttResource.activeCategories(): - raise RuntimeError( - 'Attribute Resources do not have the same active categories') - if attRes.analyses().size() != self.origAttResource.analyses().size(): - raise RuntimeError( - 'Attribute Resources do not have the same number of analyses') - if attRes.id() == self.origAttResource.id(): - raise RuntimeError('Attribute Resources have same ID') - - origAtt = self.origAttResource.findAttribute("Test Attribute") - att = attRes.findAttribute("Test Attribute") - if att.id() == origAtt.id(): - raise RuntimeError('Attributes have same ID') - - d0 = att.findDouble("d0") - d1 = att.findDouble("d1") - origD0 = origAtt.findDouble("d0") - origD1 = origAtt.findDouble("d1") - if d0.value() != origD0.value(): - raise RuntimeError('Items d0 do not have the same value') - - if d1.expression().id() == origD1.expression().id(): - raise RuntimeError('Items d1 do the same expression attribute') + + self.assertTrue( + printLogOnFail(clonedAtts.copyInitialize( + self.origAttResource, opts), opts), + 'copyInitialize failed for attribute resource') + self.assertTrue( + printLogOnFail(clonedMarkup.copyInitialize( + self.origMarkupResource, opts), opts), + 'copyInitialize failed for markup resource') + + self.assertTrue( + printLogOnFail(clonedAtts.copyFinalize( + self.origAttResource, opts), opts), + 'copyFinalize failed for attribute resource') + self.assertTrue( + printLogOnFail(clonedMarkup.copyFinalize( + self.origMarkupResource, opts), opts), + 'copyFinalize failed for markup resource') + + # print(' +++++ Cloned Attribute Resource ++++++') + # self.printResource(clonedAtts) + self.printAttInfo(clonedAtts) + # print(' +++++ Cloned Markup Resource ++++++') + # self.printResource(clonedMarkup) + self.compareClonedAttResource(clonedAtts, False, opts) + + def compareClonedAttResource(self, attRes, sameMarkup, opts): + self.assertTrue(attRes.stringProperties().contains( + 'foo'), 'Missing "foo" property.') + self.assertEqual(attRes.stringProperties().at('foo'), 'bar', + 'Copied attribute resource does not have a string property foo equal to bar.') + + self.assertEqual(attRes.activeCategoriesEnabled(), self.origAttResource.activeCategoriesEnabled(), + 'Attribute resources do not have the same active categories enabled.') + self.assertEqual(attRes.activeCategories(), self.origAttResource.activeCategories(), + 'Attribute Resources do not have the same active categories.') + self.assertEqual(attRes.analyses().size(), self.origAttResource.analyses().size(), + 'Attribute Resources do not have the same number of analyses.') + self.assertNotEqual(attRes.id(), self.origAttResource.id(), + 'Attribute Resources have same ID') + + origAtt = self.origAttResource.findAttribute('Test Attribute') + att = attRes.findAttribute('Test Attribute') + self.assertNotEqual(att.id(), origAtt.id(), 'Attributes have same ID.') + + d0 = att.findDouble('d0') + d1 = att.findDouble('d1') + origD0 = origAtt.findDouble('d0') + origD1 = origAtt.findDouble('d1') + self.assertEqual(d0.value(), origD0.value(), + 'Items d0 do not have the same value.') + + self.assertNotEqual(d1.expression().id(), origD1.expression().id(), + 'Items d1 have the same expression attribute, but it should be distinct.') attResAsso = attRes.associations() origAttResAsso = self.origAttResource.associations() + print('attRes assocs:') + for ar in attResAsso: + print(' ', ar.name(), ar.id()) + print('origAttRes assocs:') + for ar in origAttResAsso: + print(' ', ar.name(), ar.id()) + print('--') # The copy should have the same number of associates resources as the source - if len(attResAsso) != len(origAttResAsso): - raise RuntimeError( - 'Copied Attribute Resources has incorrect number of associated resources') + self.assertEqual(len(attResAsso), len(origAttResAsso), + 'Copied Attribute Resources has incorrect number of associated resources.') attAsso = att.associations() origAttAsso = origAtt.associations() - if bool(sameMarkup): + if sameMarkup: if len(attResAsso): - if not (self.origMarkupResource in attResAsso): - raise RuntimeError( - 'Copied Attribute Resource is not associated to the origin MarkUp Resource and should be') - if attAsso.value(0).id() != origAttAsso.value(0).id(): - raise RuntimeError( - 'Attributes are not associated with the same component and should be') + self.assertTrue(self.origMarkupResource in attResAsso, + 'Copied attribute resource is not associated to the original markup resource and should be.') + self.assertEqual(attAsso.value(0).id(), origAttAsso.value(0).id(), + 'Attributes are not associated with the same component and should be.') else: if len(attResAsso): - if self.origMarkupResource in attResAsso: - raise RuntimeError( - 'Copied Attribute Resource is associated to the origin MarkUp Resource and not should be') - if attAsso.value(0).id() == origAttAsso.value(0).id(): - raise RuntimeError( - 'Attributes are associated with the same component and should not be') + self.assertFalse(self.origMarkupResource in attResAsso, + 'Copied attribute resource is associated to the original markup resource and should not be.') + self.assertNotEqual(attAsso.value(0).id(), origAttAsso.value(0).id(), + 'Attributes are associated with the same component and should not be.') # Lets compare the views - the Attribute IDs should be different if they exist - view = attRes.findView("AttributeResourceCloneTest") - val = view.details().child(0).child(0).attributeAsString("ID") - - origView = self.origAttResource.findView("AttributeResourceCloneTest") - origVal = origView.details().child(0).child(0).attributeAsString("ID") - if val == origVal: - raise RuntimeError( - 'View contains IDs that point to the same attribute which is wrong') + view = attRes.findView('AttributeResourceCloneTest') + val = view.details().child(0).child(0).attributeAsString('ID') + + origView = self.origAttResource.findView('AttributeResourceCloneTest') + origVal = origView.details().child(0).child(0).attributeAsString('ID') + self.assertNotEqual( + val, origVal, 'View contains IDs that point to the same attribute which is wrong.') + + mcomp = self.originalMarkupComp + if sameMarkup: + self.assertTrue(att.links().isLinkedTo(mcomp, smtk.resource.Resource.visuallyLinkedRole()), + 'Visual link not copied.') + else: + self.assertFalse(att.links().isLinkedTo(mcomp, smtk.resource.Resource.visuallyLinkedRole()), + 'Visually linked to original (not new) component.') + ncomp = opts.targetComponentFromSourceId(mcomp.id()) + self.assertTrue(att.links().isLinkedTo(ncomp, smtk.resource.Resource.VisuallyLinkedRole), + 'Visual link not preserved in cloned resource.') print('Comparing Attribute Resources Succeeded!') From d8a4da4a2287166a69ec3e948e067c582f5f9a29 Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Mon, 8 Jul 2024 14:37:12 -0500 Subject: [PATCH 26/42] Changes to "CallObserversOnMain" behaviors. Fixes sequencing issue with observers when operations are launched asynchronously. --- ...qSMTKCallObserversOnMainThreadBehavior.cxx | 34 +++++++++++++------ ...tSMTKCallObserversOnMainThreadBehavior.cxx | 34 +++++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/smtk/extension/paraview/appcomponents/pqSMTKCallObserversOnMainThreadBehavior.cxx b/smtk/extension/paraview/appcomponents/pqSMTKCallObserversOnMainThreadBehavior.cxx index d6d19d13a..7dd042315 100644 --- a/smtk/extension/paraview/appcomponents/pqSMTKCallObserversOnMainThreadBehavior.cxx +++ b/smtk/extension/paraview/appcomponents/pqSMTKCallObserversOnMainThreadBehavior.cxx @@ -17,6 +17,9 @@ #include "smtk/extension/paraview/appcomponents/pqSMTKBehavior.h" #include "smtk/extension/paraview/appcomponents/pqSMTKWrapper.h" +#include +#include + static pqSMTKCallObserversOnMainThreadBehavior* g_instance = nullptr; pqSMTKCallObserversOnMainThreadBehavior::pqSMTKCallObserversOnMainThreadBehavior(QObject* parent) @@ -104,19 +107,27 @@ void pqSMTKCallObserversOnMainThreadBehavior::forceObserversToBeCalledOnMainThre // Override the operation Observers' call method to emit a private signal // instead of calling its Observer functors directly. wrapper->smtkOperationManager()->observers().overrideWith( - [this]( + [this, wrapper]( const smtk::operation::Operation& oper, smtk::operation::EventType event, smtk::operation::Operation::Result result) -> int { - auto id = smtk::common::UUID::random(); - m_activeOperationMutex.lock(); - m_activeOperations[id] = const_cast(oper).shared_from_this(); - m_activeOperationMutex.unlock(); - Q_EMIT operationEvent( - QString::fromStdString(id.toString()), - static_cast(event), - result ? QString::fromStdString(result->name()) : QString(), - QPrivateSignal()); + if (QThread::currentThread() != qApp->thread()) + { + auto id = smtk::common::UUID::random(); + m_activeOperationMutex.lock(); + m_activeOperations[id] = const_cast(oper).shared_from_this(); + m_activeOperationMutex.unlock(); + Q_EMIT operationEvent( + QString::fromStdString(id.toString()), + static_cast(event), + result ? QString::fromStdString(result->name()) : QString(), + QPrivateSignal()); + } + else + { + // Directly invoke the observers. + wrapper->smtkOperationManager()->observers().callObserversDirectly(oper, event, result); + } return 0; }); @@ -143,7 +154,8 @@ void pqSMTKCallObserversOnMainThreadBehavior::forceObserversToBeCalledOnMainThre } m_activeOperations.erase(id); m_activeOperationMutex.unlock(); - }); + }, + Qt::BlockingQueuedConnection); // Override the selection Observers' call method to emit a private signal // instead of calling its Observer functors directly. diff --git a/smtk/extension/qt/qtSMTKCallObserversOnMainThreadBehavior.cxx b/smtk/extension/qt/qtSMTKCallObserversOnMainThreadBehavior.cxx index e46cd6b16..c2571f27e 100644 --- a/smtk/extension/qt/qtSMTKCallObserversOnMainThreadBehavior.cxx +++ b/smtk/extension/qt/qtSMTKCallObserversOnMainThreadBehavior.cxx @@ -12,6 +12,9 @@ #include "smtk/attribute/Attribute.h" #include "smtk/attribute/Resource.h" +#include +#include + static qtSMTKCallObserversOnMainThreadBehavior* g_instance = nullptr; qtSMTKCallObserversOnMainThreadBehavior::qtSMTKCallObserversOnMainThreadBehavior(QObject* parent) @@ -93,19 +96,27 @@ void qtSMTKCallObserversOnMainThreadBehavior::forceObserversToBeCalledOnMainThre if (operationManager) { operationManager->observers().overrideWith( - [this]( + [this, operationManager]( const smtk::operation::Operation& oper, smtk::operation::EventType event, smtk::operation::Operation::Result result) -> int { - auto id = smtk::common::UUID::random(); - m_activeOperationMutex.lock(); - m_activeOperations[id] = const_cast(oper).shared_from_this(); - m_activeOperationMutex.unlock(); - Q_EMIT operationEvent( - QString::fromStdString(id.toString()), - static_cast(event), - result ? QString::fromStdString(result->name()) : QString(), - QPrivateSignal()); + if (QThread::currentThread() != qApp->thread()) + { + auto id = smtk::common::UUID::random(); + m_activeOperationMutex.lock(); + m_activeOperations[id] = const_cast(oper).shared_from_this(); + m_activeOperationMutex.unlock(); + Q_EMIT operationEvent( + QString::fromStdString(id.toString()), + static_cast(event), + result ? QString::fromStdString(result->name()) : QString(), + QPrivateSignal()); + } + else + { + // Directly invoke the observers. + operationManager->observers().callObserversDirectly(oper, event, result); + } return 0; }); @@ -132,7 +143,8 @@ void qtSMTKCallObserversOnMainThreadBehavior::forceObserversToBeCalledOnMainThre } m_activeOperations.erase(id); m_activeOperationMutex.unlock(); - }); + }, + Qt::BlockingQueuedConnection); } // Override the selection Observers' call method to emit a private signal From fb786454e9d33120037a6abed3ff9dbfca08f510 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Mon, 15 Jul 2024 17:32:44 -0400 Subject: [PATCH 27/42] ENH: Attribute Definitions are now Resource Components :smtk:`smtk::attribute::Definition` now derives from :smtk:`smtk::resource::Component`. This means that Definitions now have UUIDs and may also have properties. **Note** You do not need to specify UUIDs in either the XML or JSON file formats. If an ID is not specified, one will be assigned to it. **Note** All previous JSON and XML files are supported by this change. Version 8 XML and 7 JSON files and later will support storing the IDs. These formats will also support properties defined on Definitions. As in the case for properties on the Resource and Attributes, the XML format only supports reading properties. Developer changes ~~~~~~~~~~~~~~~~~~ ** API Breakage: ** Since classes derived from the resource component class must provide a method to return a shared pointer to a :smtk:`smtk::resource::Resource` instance via a member functions called resource() and since the Definition class already had a method called resource() that returned a shared pointer to its owning :smtk:`smtk::attribute::Resource`, this resulted in breaking API. A new method called attributeResource() was added to Definition that provides that same functionality as its original resource() method. A simple name replacement is all that is needed to resolve compilation errors resulting from this change. :smtk:`smtk::attribute::Attribute::setId()` method was not being properly supported and now generates an error message if called. The code used to parse property information in XML files has been relocated from the XMLV5Parser to its own file so it can be reused. --- .../TemplateEditor/AttDefDataModel.cxx | 2 +- applications/TemplateEditor/AttDefDialog.cxx | 2 +- .../TemplateEditor/ItemDefDataModel.cxx | 2 +- .../notes/makingDefinitionsComponents.rst | 29 +++ smtk/attribute/Attribute.cxx | 24 +-- smtk/attribute/Attribute.h | 19 +- smtk/attribute/Definition.cxx | 35 +++- smtk/attribute/Definition.h | 40 +++- smtk/attribute/ReferenceItem.cxx | 4 +- smtk/attribute/ReferenceItemDefinition.cxx | 4 - smtk/attribute/Resource.cxx | 195 +++++++++--------- smtk/attribute/Resource.h | 34 ++- smtk/attribute/json/jsonDefinition.cxx | 22 +- smtk/attribute/json/jsonResource.cxx | 25 ++- smtk/attribute/pybind11/PybindAttribute.h | 1 - smtk/attribute/pybind11/PybindResource.h | 13 +- .../qt/qtAssociation2ColumnWidget.cxx | 2 +- smtk/extension/qt/qtAssociationView.cxx | 2 +- smtk/extension/qt/qtAttributeView.cxx | 12 +- smtk/extension/qt/qtBaseAttributeView.cxx | 2 +- smtk/extension/qt/qtReferenceItemEditor.cxx | 2 +- smtk/extension/qt/qtSimpleExpressionView.cxx | 4 +- smtk/io/XmlDocV1Parser.cxx | 14 +- smtk/io/XmlDocV1Parser.h | 1 + smtk/io/XmlDocV5Parser.cxx | 105 +--------- smtk/io/XmlDocV8Parser.cxx | 28 +++ smtk/io/XmlDocV8Parser.h | 5 +- smtk/io/XmlPropertyParsingHelper.txx | 118 +++++++++++ smtk/io/XmlV8StringWriter.cxx | 2 + smtk/model/utility/InterpolateField.cxx | 2 +- 30 files changed, 447 insertions(+), 303 deletions(-) create mode 100644 doc/release/notes/makingDefinitionsComponents.rst create mode 100644 smtk/io/XmlPropertyParsingHelper.txx diff --git a/applications/TemplateEditor/AttDefDataModel.cxx b/applications/TemplateEditor/AttDefDataModel.cxx index 32a238659..d34c331bf 100644 --- a/applications/TemplateEditor/AttDefDataModel.cxx +++ b/applications/TemplateEditor/AttDefDataModel.cxx @@ -82,7 +82,7 @@ bool AttDefDataModel::hasDerivedTypes(const QModelIndex& index) const auto def = this->get(index); DefinitionPtrVec defVec; - def->resource()->derivedDefinitions(def, defVec); + def->attributeResource()->derivedDefinitions(def, defVec); return !defVec.empty(); } diff --git a/applications/TemplateEditor/AttDefDialog.cxx b/applications/TemplateEditor/AttDefDialog.cxx index e233545d1..ea9973cbd 100644 --- a/applications/TemplateEditor/AttDefDialog.cxx +++ b/applications/TemplateEditor/AttDefDialog.cxx @@ -60,7 +60,7 @@ bool AttDefDialog::validate_impl() const QString type = this->Ui->leType->text(); valid &= !type.isEmpty(); - auto def = this->BaseDef->resource()->findDefinition(type.toStdString()); + auto def = this->BaseDef->attributeResource()->findDefinition(type.toStdString()); valid &= !def; return valid; diff --git a/applications/TemplateEditor/ItemDefDataModel.cxx b/applications/TemplateEditor/ItemDefDataModel.cxx index 35791d028..77815d1ee 100644 --- a/applications/TemplateEditor/ItemDefDataModel.cxx +++ b/applications/TemplateEditor/ItemDefDataModel.cxx @@ -157,7 +157,7 @@ void ItemDefDataModel::remove(const QModelIndex& itemIndex, smtk::attribute::Def void ItemDefDataModel::clearAttributes(smtk::attribute::DefinitionPtr def) { std::vector atts; - auto sys = def->resource(); + auto sys = def->attributeResource(); sys->findAttributes(def->type(), atts); for (const auto& att : atts) { diff --git a/doc/release/notes/makingDefinitionsComponents.rst b/doc/release/notes/makingDefinitionsComponents.rst new file mode 100644 index 000000000..beb0eca53 --- /dev/null +++ b/doc/release/notes/makingDefinitionsComponents.rst @@ -0,0 +1,29 @@ +Changes to Attribute Resource +============================= +Attribute Definitions are now Resource Components +------------------------------------------------- + +:smtk:`smtk::attribute::Definition` now derives from :smtk:`smtk::resource::Component`. +This means that Definitions now have UUIDs and may also have properties. + +**Note** You do not need to specify UUIDs in either the XML or JSON file formats. If an +ID is not specified, one will be assigned to it. + +**Note** All previous JSON and XML files are supported by this change. Version 8 XML and 7 JSON files and later +will support storing the IDs. These formats will also support properties defined on Definitions. +As in the case for properties on the Resource and Attributes, the XML format only supports reading properties. + +Developer changes +~~~~~~~~~~~~~~~~~~ + +** API Breakage: ** Since classes derived from the resource component class must provide a +method to return a shared pointer to a :smtk:`smtk::resource::Resource` instance via a member functions called +resource() and since the Definition class already had a method called resource() that returned a shared pointer +to its owning :smtk:`smtk::attribute::Resource`, this resulted in breaking API. A new method called attributeResource() +was added to Definition that provides that same functionality as its original resource() method. A simple name replacement is +all that is needed to resolve compilation errors resulting from this change. + +:smtk:`smtk::attribute::Attribute::setId()` method was not being properly supported and now generates an error message if called. + +The code used to parse property information in XML files has been relocated from the XMLV5Parser to its own file so it +can be reused. diff --git a/smtk/attribute/Attribute.cxx b/smtk/attribute/Attribute.cxx index 55c593057..e686e44b3 100644 --- a/smtk/attribute/Attribute.cxx +++ b/smtk/attribute/Attribute.cxx @@ -58,20 +58,6 @@ Attribute::Attribute( , m_isColorSet(false) , m_aboutToBeDeleted(false) , m_id(myId) -{ - m_hasLocalAdvanceLevelInfo[0] = false; - m_hasLocalAdvanceLevelInfo[1] = false; - m_localAdvanceLevel[0] = m_localAdvanceLevel[1] = 0; -} - -Attribute::Attribute(const std::string& myName, const smtk::attribute::DefinitionPtr& myDefinition) - : m_name(myName) - , m_definition(myDefinition) - , m_appliesToBoundaryNodes(false) - , m_appliesToInteriorNodes(false) - , m_isColorSet(false) - , m_aboutToBeDeleted(false) - , m_id(smtk::common::UUIDGenerator::instance().random()) , m_includeIndex(0) { m_hasLocalAdvanceLevelInfo[0] = false; @@ -113,6 +99,14 @@ void Attribute::removeAllItems() m_items.clear(); } +bool Attribute::setId(const common::UUID&) +{ + smtkErrorMacro( + smtk::io::Logger::instance(), + "Changing the ID for Attribute: " << m_name << " is not supported\n"); + return false; +} + const double* Attribute::color() const { if (m_isColorSet) @@ -386,7 +380,7 @@ bool Attribute::isRelevant( ResourcePtr Attribute::attributeResource() const { - return m_definition->resource(); + return m_definition->attributeResource(); } const smtk::resource::ResourcePtr Attribute::resource() const diff --git a/smtk/attribute/Attribute.h b/smtk/attribute/Attribute.h index cd99c0d1c..d6102b7ff 100644 --- a/smtk/attribute/Attribute.h +++ b/smtk/attribute/Attribute.h @@ -83,13 +83,6 @@ class SMTKCORE_EXPORT Attribute : public resource::Component } }; - static smtk::attribute::AttributePtr New( - const std::string& myName, - const smtk::attribute::DefinitionPtr& myDefinition) - { - return smtk::attribute::AttributePtr(new Attribute(myName, myDefinition)); - } - static smtk::attribute::AttributePtr New( const std::string& myName, const smtk::attribute::DefinitionPtr& myDefinition, @@ -376,11 +369,12 @@ class SMTKCORE_EXPORT Attribute : public resource::Component bool isAboutToBeDeleted() const { return m_aboutToBeDeleted; } const common::UUID& id() const override { return m_id; } - bool setId(const common::UUID& uid) override - { - m_id = uid; - return true; - } + + /// Assign an ID to this attribute. + /// + /// This not supported for attributes. The Id is set in the constructor and should + /// never be changed. + bool setId(const common::UUID& uid) override; // These methods are use primarily by I/O operations. The include ID corresponds to // the include directory information store in the attribute resource and is used @@ -461,7 +455,6 @@ class SMTKCORE_EXPORT Attribute : public resource::Component const std::string& myName, const smtk::attribute::DefinitionPtr& myDefinition, const smtk::common::UUID& myId); - Attribute(const std::string& myName, const smtk::attribute::DefinitionPtr& myDefinition); /// Constructs the attribute from its definition void build(); diff --git a/smtk/attribute/Definition.cxx b/smtk/attribute/Definition.cxx index 6426fbfb1..4b936c193 100644 --- a/smtk/attribute/Definition.cxx +++ b/smtk/attribute/Definition.cxx @@ -17,6 +17,8 @@ #include "smtk/attribute/ReferenceItemDefinition.h" #include "smtk/attribute/Resource.h" +#include "smtk/io/Logger.h" + #include #include #include @@ -30,8 +32,10 @@ double Definition::s_defaultBaseColor[4] = { 1.0, 1.0, 1.0, 1.0 }; Definition::Definition( const std::string& myType, smtk::attribute::DefinitionPtr myBaseDef, - ResourcePtr myResource) + ResourcePtr myResource, + const smtk::common::UUID& myId) { + m_id = myId; m_resource = myResource; m_baseDefinition = myBaseDef; m_type = myType; @@ -75,6 +79,25 @@ const Tag* Definition::tag(const std::string& name) const return tag; } +bool Definition::setId(const common::UUID&) +{ + smtkErrorMacro( + smtk::io::Logger::instance(), + "Changing the ID for Attribute Definition " << m_type << " is not supported\n"); + return false; +} + +ResourcePtr Definition::attributeResource() const +{ + return m_resource.lock(); + ; +} + +const smtk::resource::ResourcePtr Definition::resource() const +{ + return this->attributeResource(); +} + Tag* Definition::tag(const std::string& name) { const Tag* tag = nullptr; @@ -186,7 +209,7 @@ bool Definition::isRelevant( { if (includeCategoryCheck && (!m_ignoreCategories)) { - auto aResource = this->resource(); + auto aResource = this->attributeResource(); if (aResource && aResource->activeCategoriesEnabled()) { if (!this->categories().passes(aResource->activeCategories())) @@ -673,7 +696,7 @@ void Definition::setItemDefinitionUnitsSystem( const smtk::attribute::ItemDefinitionPtr& itemDef) const { const auto& defUnitsSystem = this->unitsSystem(); - auto attRes = this->resource(); + auto attRes = this->attributeResource(); if (defUnitsSystem) { itemDef->setUnitsSystem(defUnitsSystem); @@ -685,7 +708,7 @@ void Definition::updateDerivedDefinitions() DefinitionPtr def = this->shared_from_this(); if (def) { - auto attresource = this->resource(); + auto attresource = this->attributeResource(); if (attresource != nullptr) { attresource->updateDerivedDefinitionIndexOffsets(def); @@ -750,7 +773,7 @@ std::set Definition::attributes( { // Get all attributes that are associated with the object and then remove all attributes whose definitions // are not derived from this one. - auto atts = this->resource()->attributes(object); + auto atts = this->attributeResource()->attributes(object); auto sharedDef = this->shared_from_this(); for (auto it = atts.begin(); it != atts.end();) { @@ -889,7 +912,7 @@ void Definition::applyAdvanceLevels( const std::shared_ptr& Definition::unitsSystem() const { static std::shared_ptr nullUnitsSystem; - auto attRes = this->resource(); + auto attRes = this->attributeResource(); if (attRes) { return attRes->unitsSystem(); diff --git a/smtk/attribute/Definition.h b/smtk/attribute/Definition.h index 7c42c27dc..49f046b7e 100644 --- a/smtk/attribute/Definition.h +++ b/smtk/attribute/Definition.h @@ -11,6 +11,8 @@ #ifndef smtk_attribute_Definition_h #define smtk_attribute_Definition_h +#include "smtk/resource/Component.h" + #include "smtk/CoreExports.h" #include "smtk/PublicPointerDefs.h" #include "smtk/SharedFromThis.h" // For smtkTypeMacroBase. @@ -42,7 +44,7 @@ class Resource; * single attribute. Instances of a definition should be created * through Resource::createAttribute(). */ -class SMTKCORE_EXPORT Definition : public smtk::enable_shared_from_this +class SMTKCORE_EXPORT Definition : public resource::Component { public: /// Return types for canBeAssociated method @@ -54,7 +56,10 @@ class SMTKCORE_EXPORT Definition : public smtk::enable_shared_from_this WeakDefinitionSet; - virtual ~Definition(); + ~Definition() override; - // Description: - // The type is the identifier that is used to access the - // attribute definition through the Resource. It should never change. + ///\brief Returns Definition's type + /// + /// The type is an additional identifier that can be used to access the + /// attribute definition through the Resource. It should never change. const std::string& type() const { return m_type; } - smtk::attribute::ResourcePtr resource() const { return m_resource.lock(); } + /// Return the name of the definition - this is the same as its type + std::string name() const override { return this->type(); } + + ///\brief Returns the attribute resource that owns the definition + smtk::attribute::ResourcePtr attributeResource() const; + + ///\brief Implementation of resource::Component::resource() method + const smtk::resource::ResourcePtr resource() const override; + + /// Return a unique identifier for the definition which will be persistent across sessions. + const common::UUID& id() const override { return m_id; } + + /// Assign an ID to this definition. + /// + /// This not supported for definitions. The Id is set in the constructor and should + /// never be changed. + bool setId(const common::UUID& myID) override; ///\brief return the smtk::attribute::Tags associated with the Definition const Tags& tags() const { return m_tags; } @@ -446,7 +468,8 @@ class SMTKCORE_EXPORT Definition : public smtk::enable_shared_from_thisname() << " to ReferenceItem: " << sourceItem->name() - << " and allowPartialValues options was not specified."); + << val->name() << " of type: " << val->typeName() << " to ReferenceItem: " + << sourceItem->name() << " and allowPartialValues options was not specified."); return result; } } diff --git a/smtk/attribute/ReferenceItemDefinition.cxx b/smtk/attribute/ReferenceItemDefinition.cxx index d27c9818c..d06fc5dfb 100644 --- a/smtk/attribute/ReferenceItemDefinition.cxx +++ b/smtk/attribute/ReferenceItemDefinition.cxx @@ -7,9 +7,6 @@ // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. See the above copyright notice for more information. //========================================================================= -#ifndef smtk_attribute_ReferenceItemDefinition_txx -#define smtk_attribute_ReferenceItemDefinition_txx - #include "smtk/attribute/ReferenceItemDefinition.h" #include "smtk/attribute/Attribute.h" @@ -517,4 +514,3 @@ void ReferenceItemDefinition::setUnitsSystem(const shared_ptr& un } } // namespace attribute } // namespace smtk -#endif diff --git a/smtk/attribute/Resource.cxx b/smtk/attribute/Resource.cxx index fd625c113..ad337c1c4 100644 --- a/smtk/attribute/Resource.cxx +++ b/smtk/attribute/Resource.cxx @@ -29,6 +29,7 @@ #include "smtk/resource/filter/Filter.h" #include "smtk/common/UUID.h" +#include "smtk/common/UUIDGenerator.h" #include "units/System.h" @@ -93,7 +94,8 @@ bool Resource::setUnitsSystem(const shared_ptr& unitsSystem) smtk::attribute::DefinitionPtr Resource::createDefinition( const std::string& typeName, - const std::string& baseTypeName) + const std::string& baseTypeName, + const smtk::common::UUID& id) { smtk::attribute::DefinitionPtr def = this->findDefinition(typeName); // Does this definition already exist @@ -111,8 +113,18 @@ smtk::attribute::DefinitionPtr Resource::createDefinition( return smtk::attribute::DefinitionPtr(); } } - smtk::attribute::DefinitionPtr newDef(new Definition(typeName, def, shared_from_this())); + + auto newDefId = id; + if (newDefId.isNull()) + { + // We need to create a new id + newDefId = smtk::common::UUIDGenerator::instance().random(); + } + + smtk::attribute::DefinitionPtr newDef( + new Definition(typeName, def, shared_from_this(), newDefId)); m_definitions[typeName] = newDef; + m_definitionIdMap[newDefId] = newDef; if (def) { // Need to add this new definition to the list of derived defs @@ -124,18 +136,28 @@ smtk::attribute::DefinitionPtr Resource::createDefinition( smtk::attribute::DefinitionPtr Resource::createDefinition( const std::string& typeName, - smtk::attribute::DefinitionPtr baseDef) + smtk::attribute::DefinitionPtr baseDef, + const smtk::common::UUID& id) { smtk::attribute::DefinitionPtr def = this->findDefinition(typeName); // Does this definition already exist or if the base def is not part - // of this manger - if (!(!def && (!baseDef || ((baseDef->resource() == shared_from_this()))))) + // of this resource + if (def || (baseDef && (baseDef->attributeResource().get() != this))) { return smtk::attribute::DefinitionPtr(); } - smtk::attribute::DefinitionPtr newDef(new Definition(typeName, baseDef, shared_from_this())); + auto newDefId = id; + if (newDefId.isNull()) + { + // We need to create a new id + newDefId = smtk::common::UUIDGenerator::instance().random(); + } + + smtk::attribute::DefinitionPtr newDef( + new Definition(typeName, baseDef, shared_from_this(), newDefId)); m_definitions[typeName] = newDef; + m_definitionIdMap[newDefId] = newDef; if (baseDef) { // Need to add this new definition to the list of derived defs @@ -147,7 +169,7 @@ smtk::attribute::DefinitionPtr Resource::createDefinition( bool Resource::removeDefinition(DefinitionPtr def) { - if (!def || def->resource() != shared_from_this()) + if (!def || def->attributeResource() != shared_from_this()) { smtkErrorMacro( smtk::io::Logger::instance(), @@ -179,31 +201,46 @@ bool Resource::removeDefinition(DefinitionPtr def) } m_definitions.erase(def->type()); + m_definitionIdMap.erase(def->id()); this->setClean(false); return true; } smtk::attribute::AttributePtr Resource::createAttribute( const std::string& name, - smtk::attribute::DefinitionPtr def) + smtk::attribute::DefinitionPtr def, + const smtk::common::UUID& id) { - // Make sure the definition belongs to this Resource or if the definition - // is abstract - if ((def->resource() != shared_from_this()) || def->isAbstract()) + auto newAttId = id; + // if the id was provided then we need to check to see if it is in use + // else we need to generate a new one + if (newAttId.isNull()) + { + newAttId = smtk::common::UUIDGenerator::instance().random(); + } + else if (this->findAttribute(newAttId) != nullptr) + { + return smtk::attribute::AttributePtr(); + } + + // Lets make sure the definition is valid + if ((def == nullptr) || (def->attributeResource() != shared_from_this()) || def->isAbstract()) { return smtk::attribute::AttributePtr(); } - // Next we need to check to see if an attribute exists by the same name + + // We need to check to see if an attribute exists by the same name smtk::attribute::AttributePtr a = this->findAttribute(name); if (a) { return smtk::attribute::AttributePtr(); } - a = Attribute::New(name, def); + + a = Attribute::New(name, def, newAttId); a->build(); m_attributeClusters[def->type()].insert(a); m_attributes[name] = a; - m_attributeIdMap[a->id()] = a; + m_attributeIdMap[newAttId] = a; this->setClean(false); return a; } @@ -230,15 +267,42 @@ smtk::attribute::AttributePtr Resource::createAttribute(const std::string& typeN smtk::attribute::AttributePtr Resource::createAttribute( const std::string& name, - const std::string& typeName) + const std::string& typeName, + const smtk::common::UUID& id) { smtk::attribute::DefinitionPtr def = this->findDefinition(typeName); - if ((def == nullptr) || def->isAbstract()) + if (def == nullptr) { return smtk::attribute::AttributePtr(); } - smtk::attribute::AttributePtr att = this->createAttribute(name, def); - return att; + return this->createAttribute(name, def, id); +} + +bool Resource::removeAttribute(smtk::attribute::AttributePtr att) +{ + // Make sure that this Resource is managing this attribute + if (!att || att->attributeResource() != shared_from_this()) + { + return false; + } + if (!att->removeAllAssociations(false)) + { + return false; + } + + att->detachItemsFromOwningResource(); + + if (m_attributes.find(att->name()) == m_attributes.end()) + { + smtkErrorMacro( + smtk::io::Logger::instance(), "Resource doesn't have attribute named: " << att->name()); + return false; + } + m_attributes.erase(att->name()); + m_attributeIdMap.erase(att->id()); + m_attributeClusters[att->type()].erase(att); + this->setClean(false); + return true; } void Resource::definitions(std::vector& result, bool sortList) const @@ -277,80 +341,6 @@ void Resource::attributes(std::vector& result) co } } -// For Reader classes - Note that since these methods are restoring an attribute -// into the resource it does not call setClean(false) -smtk::attribute::AttributePtr Resource::createAttribute( - const std::string& name, - smtk::attribute::DefinitionPtr def, - const smtk::common::UUID& id) -{ - // Lets verify that the id is not already in use - if (this->findAttribute(id) != nullptr) - { - return smtk::attribute::AttributePtr(); - } - - // Lets make sure the definition is valid - if ((def == nullptr) || (def->resource() != shared_from_this()) || def->isAbstract()) - { - return smtk::attribute::AttributePtr(); - } - - // We need to check to see if an attribute exists by the same name - smtk::attribute::AttributePtr a = this->findAttribute(name); - if (a) - { - return smtk::attribute::AttributePtr(); - } - - a = Attribute::New(name, def, id); - a->build(); - m_attributeClusters[def->type()].insert(a); - m_attributes[name] = a; - m_attributeIdMap[id] = a; - return a; -} - -smtk::attribute::AttributePtr Resource::createAttribute( - const std::string& name, - const std::string& typeName, - const smtk::common::UUID& id) -{ - smtk::attribute::DefinitionPtr def = this->findDefinition(typeName); - if (def == nullptr) - { - return smtk::attribute::AttributePtr(); - } - return this->createAttribute(name, def, id); -} - -bool Resource::removeAttribute(smtk::attribute::AttributePtr att) -{ - // Make sure that this Resource is managing this attribute - if (!att || att->attributeResource() != shared_from_this()) - { - return false; - } - if (!att->removeAllAssociations(false)) - { - return false; - } - - att->detachItemsFromOwningResource(); - - if (m_attributes.find(att->name()) == m_attributes.end()) - { - smtkErrorMacro( - smtk::io::Logger::instance(), "Resource doesn't have attribute named: " << att->name()); - return false; - } - m_attributes.erase(att->name()); - m_attributeIdMap.erase(att->id()); - m_attributeClusters[att->type()].erase(att); - this->setClean(false); - return true; -} - /**\brief Find the attribute definitions that can be associated with \a mask. * */ @@ -390,7 +380,7 @@ void Resource::findAttributes( std::vector& result) const { result.clear(); - if (def && (def->resource() == shared_from_this())) + if (def && (def->attributeResource() == shared_from_this())) { this->internalFindAttributes(def, result); } @@ -425,7 +415,7 @@ void Resource::findAllDerivedDefinitions( std::vector& result) const { result.clear(); - if (def && (def->resource() == shared_from_this())) + if (def && (def->attributeResource() == shared_from_this())) { this->internalFindAllDerivedDefinitions(def, onlyConcrete, result); } @@ -709,7 +699,8 @@ smtk::attribute::DefinitionPtr Resource::copyDefinition( if (!expDef) { // Lets see if we can copy the Definition from the source to this Resource - smtk::attribute::DefinitionPtr sourceExpDef = sourceDef->resource()->findDefinition(type); + smtk::attribute::DefinitionPtr sourceExpDef = + sourceDef->attributeResource()->findDefinition(type); // Definition missing only if source Resource is invalid, but check anyway if (sourceExpDef) @@ -746,6 +737,7 @@ smtk::attribute::DefinitionPtr Resource::copyDefinition( } // Copies attribute definition into this Resource, returning true if successful +// When copying definitions, their IDs will also be copied. bool Resource::copyDefinitionImpl( smtk::attribute::DefinitionPtr sourceDef, smtk::attribute::ItemDefinition::CopyInfo& info) @@ -775,14 +767,14 @@ bool Resource::copyDefinitionImpl( } } - // Retrieve base definition and create new def + // Retrieve base definition and create new def with the same uuid smtk::attribute::DefinitionPtr newBaseDef = this->findDefinition(baseTypeName); - newDef = this->createDefinition(typeName, newBaseDef); + newDef = this->createDefinition(typeName, newBaseDef, sourceDef->id()); } else { // No base definition - newDef = this->createDefinition(typeName); + newDef = this->createDefinition(typeName, "", sourceDef->id()); } // Set contents of new definition (defer categories) @@ -1050,9 +1042,12 @@ const std::map& Resource::fin return dummy; } -smtk::resource::ComponentPtr Resource::find(const smtk::common::UUID& attId) const +smtk::resource::ComponentPtr Resource::find(const smtk::common::UUID& compId) const { - return this->findAttribute(attId); + // First check to see if we have an attribute by that ID, if not then look for + // a definition + smtk::resource::ComponentPtr comp = this->findAttribute(compId); + return (comp ? comp : this->findDefinition(compId)); } std::string Resource::createAttributeQuery(const smtk::attribute::DefinitionPtr& def) diff --git a/smtk/attribute/Resource.h b/smtk/attribute/Resource.h index 76960614a..92308365c 100644 --- a/smtk/attribute/Resource.h +++ b/smtk/attribute/Resource.h @@ -106,10 +106,14 @@ class SMTKCORE_EXPORT Resource smtk::attribute::DefinitionPtr createDefinition( const std::string& typeName, - const std::string& baseTypeName = ""); + const std::string& baseTypeName = "", + const smtk::common::UUID& id = smtk::common::UUID::null()); + smtk::attribute::DefinitionPtr createDefinition( const std::string& name, - attribute::DefinitionPtr baseDefiniiton); + attribute::DefinitionPtr baseDefiniiton, + const smtk::common::UUID& id = smtk::common::UUID::null()); + // Description: // For simplicity, only Definitions without any children can be currently // removed (external nodes). @@ -131,12 +135,17 @@ class SMTKCORE_EXPORT Resource */ bool setDefaultNameSeparator(const std::string& separator); - smtk::attribute::AttributePtr createAttribute(const std::string& name, const std::string& type); smtk::attribute::AttributePtr createAttribute(attribute::DefinitionPtr def); smtk::attribute::AttributePtr createAttribute(const std::string& type); smtk::attribute::AttributePtr createAttribute( const std::string& name, - attribute::DefinitionPtr def); + const std::string& type, + const smtk::common::UUID& id = smtk::common::UUID::null()); + smtk::attribute::AttributePtr createAttribute( + const std::string& name, + attribute::DefinitionPtr def, + const smtk::common::UUID& id = smtk::common::UUID::null()); + bool removeAttribute(smtk::attribute::AttributePtr att); smtk::attribute::AttributePtr findAttribute(const std::string& name) const; smtk::attribute::AttributePtr findAttribute(const smtk::common::UUID& id) const; @@ -181,6 +190,7 @@ class SMTKCORE_EXPORT Resource std::vector& result) const; smtk::attribute::DefinitionPtr findDefinition(const std::string& type) const; + smtk::attribute::DefinitionPtr findDefinition(const smtk::common::UUID& id) const; /// Return true if the Resource has a Definition with the requested type bool hasDefinition(const std::string& type) const; @@ -218,13 +228,6 @@ class SMTKCORE_EXPORT Resource const double* advanceLevelColor(int level) const; void setAdvanceLevelColor(int level, const double* l_color); - // For Reader classes - smtk::attribute::AttributePtr - createAttribute(const std::string& name, const std::string& type, const smtk::common::UUID& id); - smtk::attribute::AttributePtr createAttribute( - const std::string& name, - attribute::DefinitionPtr def, - const smtk::common::UUID& id); std::string createUniqueName(const std::string& type) const; void finalizeDefinitions(); @@ -490,6 +493,7 @@ class SMTKCORE_EXPORT Resource m_attributeClusters; std::map m_attributes; std::map m_attributeIdMap; + std::map m_definitionIdMap; std::map< smtk::attribute::DefinitionPtr, @@ -554,6 +558,14 @@ inline smtk::attribute::DefinitionPtr Resource::findDefinition(const std::string return (it == m_definitions.end()) ? smtk::attribute::DefinitionPtr() : it->second; } +inline smtk::attribute::DefinitionPtr Resource::findDefinition( + const smtk::common::UUID& defId) const +{ + std::map::const_iterator it; + it = m_definitionIdMap.find(defId); + return (it == m_definitionIdMap.end()) ? smtk::attribute::DefinitionPtr() : it->second; +} + inline bool Resource::hasDefinition(const std::string& typeName) const { auto it = m_definitions.find(typeName); diff --git a/smtk/attribute/json/jsonDefinition.cxx b/smtk/attribute/json/jsonDefinition.cxx index 5a7ae7338..e41f43159 100644 --- a/smtk/attribute/json/jsonDefinition.cxx +++ b/smtk/attribute/json/jsonDefinition.cxx @@ -32,6 +32,7 @@ namespace attribute SMTKCORE_EXPORT void to_json(nlohmann::json& j, const smtk::attribute::DefinitionPtr& defPtr) { j["Type"] = defPtr->type(); + j["ID"] = defPtr->id().toString(); if (!defPtr->label().empty()) { j["Label"] = defPtr->label(); @@ -164,20 +165,28 @@ SMTKCORE_EXPORT void to_json(nlohmann::json& j, const smtk::attribute::Definitio j["Tags"] = tagInfo; } + smtk::attribute::ResourcePtr attResource = defPtr->attributeResource(); + if (!attResource) + { + smtkErrorMacro( + smtk::io::Logger::instance(), + "When converting to json, definition " << defPtr->label() << " has an invalid resourcePtr"); + return; + } + auto associationRuleForDef = - defPtr->resource()->associationRules().associationRulesForDefinitions().find(defPtr->type()); + attResource->associationRules().associationRulesForDefinitions().find(defPtr->type()); if ( - associationRuleForDef != - defPtr->resource()->associationRules().associationRulesForDefinitions().end()) + associationRuleForDef != attResource->associationRules().associationRulesForDefinitions().end()) { j["AssociationRule"] = associationRuleForDef->second; } auto dissociationRuleForDef = - defPtr->resource()->associationRules().dissociationRulesForDefinitions().find(defPtr->type()); + attResource->associationRules().dissociationRulesForDefinitions().find(defPtr->type()); if ( dissociationRuleForDef != - defPtr->resource()->associationRules().dissociationRulesForDefinitions().end()) + attResource->associationRules().dissociationRulesForDefinitions().end()) { j["DissociationRule"] = dissociationRuleForDef->second; } @@ -193,8 +202,7 @@ SMTKCORE_EXPORT void from_json( { return; } - smtk::attribute::ResourcePtr attResource = - std::dynamic_pointer_cast(defPtr->resource()); + smtk::attribute::ResourcePtr attResource = defPtr->attributeResource(); if (attResource == nullptr) { smtkErrorMacro( diff --git a/smtk/attribute/json/jsonResource.cxx b/smtk/attribute/json/jsonResource.cxx index d569f2c52..7e3e2078a 100644 --- a/smtk/attribute/json/jsonResource.cxx +++ b/smtk/attribute/json/jsonResource.cxx @@ -555,7 +555,7 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) } else { - baseDef = res->findDefinition(*baseType); + baseDef = res->findDefinition(baseType->get()); if (!baseDef) { smtkErrorMacro( @@ -565,7 +565,18 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) continue; } } - def = res->createDefinition(*type, baseDef); + // Definitions were made into Components after SMTK 24.01 so see if there is + // an id and if so use it. + auto id = currentDef.find("ID"); + if (id != currentDef.end()) + { + smtk::common::UUID uuid(id->get()); + def = res->createDefinition(*type, baseDef, uuid); + } + else + { + def = res->createDefinition(*type, baseDef); + } if (!def) { smtkWarningMacro( @@ -591,7 +602,7 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) smtk::attribute::DefinitionPtr def; for (const auto& defName : exclusion) { - def = res->findDefinition(defName); + def = res->findDefinition(defName.get()); if (def) { defs.push_back(def); @@ -628,7 +639,7 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) continue; } // Lets find the target definition - auto tdef = res->findDefinition(*type); + auto tdef = res->findDefinition(type->get()); if (!tdef) { smtkErrorMacro( @@ -648,7 +659,7 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) smtk::attribute::DefinitionPtr def; for (const auto& defName : *prerequisite) { - def = res->findDefinition(defName); + def = res->findDefinition(defName.get()); if (def) { tdef->addPrerequisite(def); @@ -740,7 +751,7 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) continue; } - if (!res->findDefinition(def)) + if (!res->findDefinition(def.get())) { smtkErrorMacro( smtk::io::Logger::instance(), @@ -783,7 +794,7 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ResourcePtr& res) "Invalid Attribute! - Missing Type for attribute:" << *name); continue; } - smtk::attribute::DefinitionPtr def = res->findDefinition(*type); + smtk::attribute::DefinitionPtr def = res->findDefinition(type->get()); if (def == nullptr) { smtkErrorMacro( diff --git a/smtk/attribute/pybind11/PybindAttribute.h b/smtk/attribute/pybind11/PybindAttribute.h index e6db02320..3a87b9359 100644 --- a/smtk/attribute/pybind11/PybindAttribute.h +++ b/smtk/attribute/pybind11/PybindAttribute.h @@ -45,7 +45,6 @@ inline PySharedPtrClass< smtk::attribute::Attribute > pybind11_init_smtk_attribu instance .def(py::init<::smtk::attribute::Attribute const &>()) .def("deepcopy", (smtk::attribute::Attribute & (smtk::attribute::Attribute::*)(::smtk::attribute::Attribute const &)) &smtk::attribute::Attribute::operator=) - .def_static("New", (smtk::attribute::AttributePtr (*)(::std::string const &, const ::smtk::attribute::DefinitionPtr&)) &smtk::attribute::Attribute::New, py::arg("myName"), py::arg("myDefinition")) .def_static("New", (smtk::attribute::AttributePtr (*)(::std::string const &, const ::smtk::attribute::DefinitionPtr&, ::smtk::common::UUID const &)) &smtk::attribute::Attribute::New, py::arg("myName"), py::arg("myDefinition"), py::arg("myId")) .def("appliesToBoundaryNodes", &smtk::attribute::Attribute::appliesToBoundaryNodes) .def("appliesToInteriorNodes", &smtk::attribute::Attribute::appliesToInteriorNodes) diff --git a/smtk/attribute/pybind11/PybindResource.h b/smtk/attribute/pybind11/PybindResource.h index ca7292936..9edfec1f9 100644 --- a/smtk/attribute/pybind11/PybindResource.h +++ b/smtk/attribute/pybind11/PybindResource.h @@ -47,14 +47,12 @@ inline PySharedPtrClass< smtk::attribute::Resource> pybind11_init_smtk_attribute .def("attributes", [](const smtk::attribute::Resource& sys){ std::vector result; sys.attributes(result); return result; }) .def("categories", &smtk::attribute::Resource::categories) .def("copyDefinition", &smtk::attribute::Resource::copyDefinition, py::arg("def"), py::arg("options") = 0) - .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &, ::std::string const &)) &smtk::attribute::Resource::createAttribute, py::arg("name"), py::arg("type")) + .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &, ::std::string const &, ::smtk::common::UUID const &)) &smtk::attribute::Resource::createAttribute, py::arg("name"), py::arg("type"), py::arg("id") = smtk::common::UUID::null()) .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::smtk::attribute::DefinitionPtr)) &smtk::attribute::Resource::createAttribute, py::arg("def")) .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &)) &smtk::attribute::Resource::createAttribute, py::arg("type")) - .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &, ::smtk::attribute::DefinitionPtr)) &smtk::attribute::Resource::createAttribute, py::arg("name"), py::arg("def")) - .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &, ::std::string const &, ::smtk::common::UUID const &)) &smtk::attribute::Resource::createAttribute, py::arg("name"), py::arg("type"), py::arg("id")) - .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &, ::smtk::attribute::DefinitionPtr, ::smtk::common::UUID const &)) &smtk::attribute::Resource::createAttribute, py::arg("name"), py::arg("def"), py::arg("id")) - .def("createDefinition", (smtk::attribute::DefinitionPtr (smtk::attribute::Resource::*)(::std::string const &, ::std::string const &)) &smtk::attribute::Resource::createDefinition, py::arg("typeName"), py::arg("baseTypeName") = "") - .def("createDefinition", (smtk::attribute::DefinitionPtr (smtk::attribute::Resource::*)(::std::string const &, ::smtk::attribute::DefinitionPtr)) &smtk::attribute::Resource::createDefinition, py::arg("name"), py::arg("baseDefiniiton")) + .def("createAttribute", (smtk::attribute::AttributePtr (smtk::attribute::Resource::*)(::std::string const &, ::smtk::attribute::DefinitionPtr, ::smtk::common::UUID const &)) &smtk::attribute::Resource::createAttribute, py::arg("name"), py::arg("def"), py::arg("id") = smtk::common::UUID::null()) + .def("createDefinition", (smtk::attribute::DefinitionPtr (smtk::attribute::Resource::*)(::std::string const &, ::std::string const &, ::smtk::common::UUID const &)) &smtk::attribute::Resource::createDefinition, py::arg("typeName"), py::arg("baseTypeName") = "", py::arg("id") = smtk::common::UUID::null()) + .def("createDefinition", (smtk::attribute::DefinitionPtr (smtk::attribute::Resource::*)(::std::string const &, ::smtk::attribute::DefinitionPtr, ::smtk::common::UUID const &)) &smtk::attribute::Resource::createDefinition, py::arg("name"), py::arg("baseDefiniiton"), py::arg("id") = smtk::common::UUID::null()) .def("createUniqueName", &smtk::attribute::Resource::createUniqueName, py::arg("type")) .def("defaultNameSeparator", &smtk::attribute::Resource::defaultNameSeparator) .def("definitions", [](const smtk::attribute::Resource& sys){ std::vector result; sys.definitions(result); return result; }) @@ -66,7 +64,8 @@ inline PySharedPtrClass< smtk::attribute::Resource> pybind11_init_smtk_attribute .def("findAttributes", (std::vector, std::allocator > > (smtk::attribute::Resource::*)(::std::string const &) const) &smtk::attribute::Resource::findAttributes, py::arg("type")) .def("findAttributes", [](const smtk::attribute::Resource& res, smtk::attribute::DefinitionPtr defn) { std::vector result; res.findAttributes(defn, result); return result; }) .def("findBaseDefinitions", [](const smtk::attribute::Resource& res) { std::vector result; res.findBaseDefinitions(result); return result; }) - .def("findDefinition", &smtk::attribute::Resource::findDefinition, py::arg("type")) + .def("findDefinition", (smtk::attribute::DefinitionPtr (smtk::attribute::Resource::*)(::std::string const &) const) &smtk::attribute::Resource::findDefinition, py::arg("type")) + .def("findDefinition", (smtk::attribute::DefinitionPtr (smtk::attribute::Resource::*)(::smtk::common::UUID const &) const) &smtk::attribute::Resource::findDefinition, py::arg("id")) .def("findDefinitionAttributes", [](const smtk::attribute::Resource& res, const std::string& type) { std::vector result; res.findDefinitionAttributes(type, result); return result; }) .def("findDefinitions", [](const smtk::attribute::Resource& res, unsigned long mask) { std::vector result; res.findDefinitions(mask, result); return result; }) .def("findIsUniqueBaseClass", &smtk::attribute::Resource::findIsUniqueBaseClass, py::arg("attDef")) diff --git a/smtk/extension/qt/qtAssociation2ColumnWidget.cxx b/smtk/extension/qt/qtAssociation2ColumnWidget.cxx index 6fdd6f1de..3f5274bca 100644 --- a/smtk/extension/qt/qtAssociation2ColumnWidget.cxx +++ b/smtk/extension/qt/qtAssociation2ColumnWidget.cxx @@ -354,7 +354,7 @@ void qtAssociation2ColumnWidget::refreshAssociations(const smtk::common::UUID& i return; } - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); // If this resource is marked for removal there is nothing to be done if (attResource->isMarkedForRemoval()) { diff --git a/smtk/extension/qt/qtAssociationView.cxx b/smtk/extension/qt/qtAssociationView.cxx index 54f0ffa06..8eb8088bb 100644 --- a/smtk/extension/qt/qtAssociationView.cxx +++ b/smtk/extension/qt/qtAssociationView.cxx @@ -222,7 +222,7 @@ void qtAssociationView::updateUI() // Get all of the attributes that match the list of definitions Q_FOREACH (attribute::DefinitionPtr attDef, currentDefs) { - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); std::vector result; attResource->findAttributes(attDef, result); if (!result.empty()) diff --git a/smtk/extension/qt/qtAttributeView.cxx b/smtk/extension/qt/qtAttributeView.cxx index cbc15905d..7590d1686 100644 --- a/smtk/extension/qt/qtAttributeView.cxx +++ b/smtk/extension/qt/qtAttributeView.cxx @@ -686,7 +686,7 @@ void qtAttributeView::onAttributeNameChanged(QStandardItem* item) smtk::attribute::AttributePtr aAttribute = this->getAttributeFromItem(item); if (aAttribute && item->text().toStdString() != aAttribute->name()) { - ResourcePtr attResource = aAttribute->definition()->resource(); + ResourcePtr attResource = aAttribute->definition()->attributeResource(); // Lets see if the name is in use auto att = attResource->findAttribute(item->text().toStdString()); if (att != nullptr) @@ -852,7 +852,7 @@ void qtAttributeView::createNewAttribute(smtk::attribute::DefinitionPtr attDef) return; } - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); smtk::attribute::AttributePtr newAtt = attResource->createAttribute(attDef->type()); QStandardItem* item = this->addAttributeListItem(newAtt); @@ -936,7 +936,7 @@ bool smtk::extension::qtAttributeView::deleteAttribute(smtk::attribute::Attribut if (att != nullptr) { attribute::DefinitionPtr attDef = att->definition(); - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); status = attResource->removeAttribute(att); } @@ -1110,7 +1110,7 @@ void qtAttributeView::onViewByWithDefinition(smtk::attribute::DefinitionPtr attD { smtk::attribute::AttributePtr currentAtt = m_internals->selectedAttribute.lock(); std::vector result; - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); attResource->findAttributes(attDef, result); if (!result.empty()) { @@ -1203,7 +1203,7 @@ void qtAttributeView::initSelectAttCombo(smtk::attribute::DefinitionPtr attDef) } std::vector result; - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); attResource->findAttributes(attDef, result); std::vector::iterator it; int row = 1; @@ -1521,7 +1521,7 @@ int qtAttributeView::handleOperationEvent( // If there is no definition or it's attribute resource is mark for removal // then we don't need to update anything - if ((currentDef == nullptr) || currentDef->resource()->isMarkedForRemoval()) + if ((currentDef == nullptr) || currentDef->attributeResource()->isMarkedForRemoval()) { return 0; } diff --git a/smtk/extension/qt/qtBaseAttributeView.cxx b/smtk/extension/qt/qtBaseAttributeView.cxx index e7205b2bb..74127c822 100644 --- a/smtk/extension/qt/qtBaseAttributeView.cxx +++ b/smtk/extension/qt/qtBaseAttributeView.cxx @@ -140,7 +140,7 @@ void qtBaseAttributeView::getDefinitions( QList& defs) { std::vector newdefs; - attribute::ResourcePtr attResource = attDef->resource(); + attribute::ResourcePtr attResource = attDef->attributeResource(); attResource->findAllDerivedDefinitions(attDef, true, newdefs); if (!attDef->isAbstract() && !defs.contains(attDef)) { diff --git a/smtk/extension/qt/qtReferenceItemEditor.cxx b/smtk/extension/qt/qtReferenceItemEditor.cxx index 046760284..0f4002fe9 100644 --- a/smtk/extension/qt/qtReferenceItemEditor.cxx +++ b/smtk/extension/qt/qtReferenceItemEditor.cxx @@ -329,7 +329,7 @@ void qtReferenceItemEditor::updateChoices(const smtk::common::UUID& ignoreResour attribute::DefinitionPtr attDef = theAttribute->definition(); - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); // Lets get a set of possible candidates that could be assigned to the item auto resManager = this->uiManager()->resourceManager(); diff --git a/smtk/extension/qt/qtSimpleExpressionView.cxx b/smtk/extension/qt/qtSimpleExpressionView.cxx index 3e80e5d9c..957cb1f38 100644 --- a/smtk/extension/qt/qtSimpleExpressionView.cxx +++ b/smtk/extension/qt/qtSimpleExpressionView.cxx @@ -370,7 +370,7 @@ void qtSimpleExpressionView::onFuncNameChanged(QListWidgetItem* item) smtk::attribute::AttributePtr func = this->getFunctionFromItem(item); if (func) { - ResourcePtr attResource = func->definition()->resource(); + ResourcePtr attResource = func->definition()->attributeResource(); attResource->rename(func, item->text().toLatin1().constData()); } } @@ -464,7 +464,7 @@ void qtSimpleExpressionView::createNewFunction(smtk::attribute::DefinitionPtr at return; } this->Internals->FuncList->blockSignals(true); - ResourcePtr attResource = attDef->resource(); + ResourcePtr attResource = attDef->attributeResource(); smtk::attribute::AttributePtr newFunc = attResource->createAttribute(attDef->type()); this->attributeCreated(newFunc); diff --git a/smtk/io/XmlDocV1Parser.cxx b/smtk/io/XmlDocV1Parser.cxx index 1155a80aa..011ffcd07 100644 --- a/smtk/io/XmlDocV1Parser.cxx +++ b/smtk/io/XmlDocV1Parser.cxx @@ -989,6 +989,8 @@ void XmlDocV1Parser::createDefinition(xml_node& defNode) smtkErrorMacro(m_logger, "Definition missing Type XML Attribute"); return; } + auto id = this->getDefinitionID(defNode); + baseType = defNode.attribute("BaseType").value(); if (!baseType.empty()) { @@ -1000,12 +1002,14 @@ void XmlDocV1Parser::createDefinition(xml_node& defNode) "Could not find Base Definition: " << baseType << " needed to create Definition: " << type); return; } - def = m_resource->createDefinition(type, baseDef); + + def = m_resource->createDefinition(type, baseDef, id); } else { - def = m_resource->createDefinition(type); + def = m_resource->createDefinition(type, "", id); } + if (!def) { if (m_reportAsError) @@ -2011,6 +2015,12 @@ smtk::common::UUID XmlDocV1Parser::getAttributeID(xml_node& attNode) return smtk::common::UUID::null(); } +smtk::common::UUID XmlDocV1Parser::getDefinitionID(xml_node&) +{ + // Prior to V8 - Definition IDs were not stored in XML format + return smtk::common::UUID::null(); +} + smtk::attribute::AttributePtr XmlDocV1Parser::processAttribute(xml_node& attNode) { xml_node itemsNode, assocsNode, iNode, node; diff --git a/smtk/io/XmlDocV1Parser.h b/smtk/io/XmlDocV1Parser.h index 681d88391..ee6a55edd 100644 --- a/smtk/io/XmlDocV1Parser.h +++ b/smtk/io/XmlDocV1Parser.h @@ -242,6 +242,7 @@ class SMTKCORE_EXPORT XmlDocV1Parser bool getColor(pugi::xml_node& node, double color[3], const std::string& colorName); virtual smtk::common::UUID getAttributeID(pugi::xml_node& attNode); + virtual smtk::common::UUID getDefinitionID(pugi::xml_node& defNode); /// For processing item definition blocks void processItemDefinitionBlocks( diff --git a/smtk/io/XmlDocV5Parser.cxx b/smtk/io/XmlDocV5Parser.cxx index a9f6da1f3..28975be21 100644 --- a/smtk/io/XmlDocV5Parser.cxx +++ b/smtk/io/XmlDocV5Parser.cxx @@ -10,114 +10,15 @@ #include "smtk/io/XmlDocV5Parser.h" -#include "smtk/common/StringUtil.h" - #define PUGIXML_HEADER_ONLY // NOLINTNEXTLINE(bugprone-suspicious-include) #include "pugixml/src/pugixml.cpp" - using namespace pugi; -using namespace smtk::io; -using namespace smtk; - -namespace -{ - -template -ValueType node_as(const xml_node& node); - -template<> -double node_as(const xml_node& node) -{ - return node.text().as_double(); -} - -template<> -std::string node_as(const xml_node& node) -{ - std::string s = node.text().as_string(); - return common::StringUtil::trim(s); -} - -template -void nodeToData(const xml_node& node, Container& container) -{ - for (const xml_node& child : node.children("Value")) - { - container.insert(container.end(), node_as(child)); - } -} - -template -void processProperties(T& object, xml_node& propertiesNode, Logger& logger) -{ - for (const xml_node& propNode : propertiesNode.children("Property")) - { - const xml_attribute propNameAtt = propNode.attribute("Name"); - if (!propNameAtt) - { - smtkWarningMacro(logger, "Missing Name xml attribute in Property xml node."); - continue; - } - const xml_attribute propTypeAtt = propNode.attribute("Type"); - if (!propTypeAtt) - { - smtkWarningMacro(logger, "Missing Type xml attribute in Property xml node."); - continue; - } - - // Convert the type to lower case - std::string attVal = propTypeAtt.value(); - std::string propType = smtk::common::StringUtil::lower(attVal); - std::string propName = propNameAtt.value(); +#include "smtk/io/XmlPropertyParsingHelper.txx" - if (propType == "int") - { - object->properties().template get()[propName] = propNode.text().as_int(); - } - else if (propType == "double") - { - object->properties().template get()[propName] = propNode.text().as_double(); - } - else if (propType == "string") - { - object->properties().template get()[propName] = propNode.text().as_string(); - } - else if (propType == "vector[string]") - { - nodeToData(propNode, object->properties().template get>()[propName]); - } - else if (propType == "vector[double]") - { - nodeToData(propNode, object->properties().template get>()[propName]); - } - else if (propType == "bool") - { - std::string sval = propNode.text().as_string(); - bool bval; - if (smtk::common::StringUtil::toBoolean(sval, bval)) - { - object->properties().template get()[propName] = bval; - } - else - { - smtkWarningMacro( - logger, - "Invalid Boolean Property Value:" << propNode.text().as_string() << " for Property: " - << propName << " of Object: " << object->name() << "."); - } - } - else - { - smtkWarningMacro( - logger, - "Unsupported Type:" << propTypeAtt.value() - << " in Property xml node for Object: " << object->name() << "."); - } - } -} -}; // namespace +using namespace smtk::io; +using namespace smtk; XmlDocV5Parser::XmlDocV5Parser(smtk::attribute::ResourcePtr myResource, smtk::io::Logger& logger) : XmlDocV4Parser(myResource, logger) diff --git a/smtk/io/XmlDocV8Parser.cxx b/smtk/io/XmlDocV8Parser.cxx index 62b08c0c1..d8d1a3fcb 100644 --- a/smtk/io/XmlDocV8Parser.cxx +++ b/smtk/io/XmlDocV8Parser.cxx @@ -17,6 +17,9 @@ #include "pugixml/src/pugixml.cpp" using namespace pugi; + +#include "smtk/io/XmlPropertyParsingHelper.txx" + using namespace smtk::attribute; using namespace smtk::io; using namespace smtk; @@ -135,6 +138,13 @@ void XmlDocV8Parser::processDefinitionChildNode(xml_node& node, DefinitionPtr& d { std::string nodeName = node.name(); + // Are we dealing with Properties + if (nodeName == "Properties") + { + processProperties(def, node, m_logger); + return; + } + // Are we dealing with Category Expressions if (nodeName == "CategoryExpression") { @@ -174,3 +184,21 @@ smtk::attribute::AttributePtr XmlDocV8Parser::processAttribute(pugi::xml_node& a return att; } + +smtk::common::UUID XmlDocV8Parser::getDefinitionID(xml_node& attNode) +{ + xml_attribute xatt; + smtk::common::UUID id; + + xatt = attNode.attribute("ID"); + if (!xatt) + { + id = smtk::common::UUID::null(); + } + else + { + id = smtk::common::UUID(xatt.value()); + } + + return id; +} diff --git a/smtk/io/XmlDocV8Parser.h b/smtk/io/XmlDocV8Parser.h index ee4883ae7..50a5d8bbf 100644 --- a/smtk/io/XmlDocV8Parser.h +++ b/smtk/io/XmlDocV8Parser.h @@ -38,14 +38,15 @@ class SMTKCORE_EXPORT XmlDocV8Parser : public XmlDocV7Parser attribute::Categories::Expression& catExp, attribute::Categories::CombinationMode& inheritanceMode); void processDefinitionAtts(pugi::xml_node& node, smtk::attribute::DefinitionPtr& def) override; + void processDefinitionChildNode(pugi::xml_node& defNode, smtk::attribute::DefinitionPtr& def) + override; void processItemDefChildNode(pugi::xml_node& node, const smtk::attribute::ItemDefinitionPtr& idef) override; void processItemDefCategoryExpressionNode( pugi::xml_node& node, smtk::attribute::ItemDefinitionPtr idef); - void processDefinitionChildNode(pugi::xml_node& node, smtk::attribute::DefinitionPtr& def) - override; void processDefCategoryExpressionNode(pugi::xml_node& node, smtk::attribute::DefinitionPtr& def); + smtk::common::UUID getDefinitionID(pugi::xml_node& defNode) override; }; } // namespace io } // namespace smtk diff --git a/smtk/io/XmlPropertyParsingHelper.txx b/smtk/io/XmlPropertyParsingHelper.txx new file mode 100644 index 000000000..a95bc482e --- /dev/null +++ b/smtk/io/XmlPropertyParsingHelper.txx @@ -0,0 +1,118 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#ifndef smtk_io_XmlPropertyParsingHelper_txx +#define smtk_io_XmlPropertyParsingHelper_txx + +#include "smtk/common/StringUtil.h" +#include "smtk/io/Logger.h" + +/// These templates are for aiding XML classes that need to parse nodes that +/// represent SMTK Properties + +namespace +{ + +template +ValueType node_as(const xml_node& node); + +template<> +double node_as(const xml_node& node) +{ + return node.text().as_double(); +} + +template<> +std::string node_as(const xml_node& node) +{ + std::string s = node.text().as_string(); + return smtk::common::StringUtil::trim(s); +} + +template +void nodeToData(const xml_node& node, Container& container) +{ + for (const xml_node& child : node.children("Value")) + { + container.insert(container.end(), node_as(child)); + } +} + +template +void processProperties(T& object, xml_node& propertiesNode, smtk::io::Logger& logger) +{ + for (const xml_node& propNode : propertiesNode.children("Property")) + { + const xml_attribute propNameAtt = propNode.attribute("Name"); + if (!propNameAtt) + { + smtkWarningMacro(logger, "Missing Name xml attribute in Property xml node."); + continue; + } + const xml_attribute propTypeAtt = propNode.attribute("Type"); + if (!propTypeAtt) + { + smtkWarningMacro(logger, "Missing Type xml attribute in Property xml node."); + continue; + } + + // Convert the type to lower case + std::string attVal = propTypeAtt.value(); + std::string propType = smtk::common::StringUtil::lower(attVal); + + std::string propName = propNameAtt.value(); + + if (propType == "int") + { + object->properties().template get()[propName] = propNode.text().as_int(); + } + else if (propType == "double") + { + object->properties().template get()[propName] = propNode.text().as_double(); + } + else if (propType == "string") + { + object->properties().template get()[propName] = propNode.text().as_string(); + } + else if (propType == "vector[string]") + { + nodeToData(propNode, object->properties().template get>()[propName]); + } + else if (propType == "vector[double]") + { + nodeToData(propNode, object->properties().template get>()[propName]); + } + else if (propType == "bool") + { + std::string sval = propNode.text().as_string(); + bool bval; + if (smtk::common::StringUtil::toBoolean(sval, bval)) + { + object->properties().template get()[propName] = bval; + } + else + { + smtkWarningMacro( + logger, + "Invalid Boolean Property Value:" << propNode.text().as_string() << " for Property: " + << propName << " of Object: " << object->name() << "."); + } + } + else + { + smtkWarningMacro( + logger, + "Unsupported Type:" << propTypeAtt.value() + << " in Property xml node for Object: " << object->name() << "."); + } + } +} +}; // namespace +#endif diff --git a/smtk/io/XmlV8StringWriter.cxx b/smtk/io/XmlV8StringWriter.cxx index b6cbaaf37..d80173d29 100644 --- a/smtk/io/XmlV8StringWriter.cxx +++ b/smtk/io/XmlV8StringWriter.cxx @@ -52,6 +52,8 @@ void XmlV8StringWriter::processDefinitionInternal( { definition.append_attribute("Units").set_value(def->units().c_str()); } + // Add its ID + definition.append_attribute("ID").set_value(def->id().toString().c_str()); } } // namespace io diff --git a/smtk/model/utility/InterpolateField.cxx b/smtk/model/utility/InterpolateField.cxx index a960f6ff3..1fe4b45e7 100644 --- a/smtk/model/utility/InterpolateField.cxx +++ b/smtk/model/utility/InterpolateField.cxx @@ -43,7 +43,7 @@ std::vector computeWeights( { // Access the attribute resource containing the annotations smtk::attribute::ResourcePtr attributeResource = - definition != nullptr ? definition->resource() : nullptr; + definition != nullptr ? definition->attributeResource() : nullptr; // Construct a vector of point profiles std::vector pointProfiles(samplePoints.size() / 3); From 1d01633431d5ea05bc2cccfa9b4c9bfc25364536 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 17 Jul 2024 09:55:18 -0400 Subject: [PATCH 28/42] TESTING: Modifying Test for XML Support for Properties on Definitions --- .../attribute_collection/propertiesExample.sbt | 6 +++++- smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx | 10 +++++++++- smtk/resource/testing/python/testCloneResources.py | 2 +- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/data/attribute/attribute_collection/propertiesExample.sbt b/data/attribute/attribute_collection/propertiesExample.sbt index d27ea1e3c..dcd34b0be 100644 --- a/data/attribute/attribute_collection/propertiesExample.sbt +++ b/data/attribute/attribute_collection/propertiesExample.sbt @@ -11,7 +11,11 @@ - + + + 100 + + diff --git a/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx b/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx index b55e234d9..c6982be01 100644 --- a/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx +++ b/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx @@ -64,9 +64,17 @@ int unitXmlReaderProperties(int /*unused*/, char* /*unused*/[]) smtkTest( attRes->properties().at("pb"), "Resource Bool Property pb was false but should be true"); - // Now lets test the properties on the attribute + // Now lets test the properties on the attribute and definition auto att = attRes->findAttribute("foo"); smtkTest(att != nullptr, "Could not find attribute foo"); + auto def = att->definition(); + smtkTest(def != nullptr, "Could not find attribute foo's definition"); + smtkTest( + def->properties().contains("alpha"), "Definition does not contains Int Property alpha"); + smtkTest( + def->properties().at("alpha") == 100, + "Attribute Definition Int Property alpha is " << def->properties().at("alpha") + << " but should be 100."); smtkTest(att->properties().contains("pi"), "Attribute does not contains Int Property pi"); smtkTest( att->properties().contains("pd"), "Attribute does not contains Double Property pd"); diff --git a/smtk/resource/testing/python/testCloneResources.py b/smtk/resource/testing/python/testCloneResources.py index f5c088faa..00bc9c2f8 100644 --- a/smtk/resource/testing/python/testCloneResources.py +++ b/smtk/resource/testing/python/testCloneResources.py @@ -61,7 +61,7 @@ def setUp(self): # Let's associate the markup resource to the attribute resource self.assertTrue(self.origAttResource.associate(self.origMarkupResource), - 'Could not associatte attribute resource to markup resource.') + 'Could not associate attribute resource to markup resource.') compset = self.origMarkupResource.filter( 'smtk::markup::UnstructuredData') From d7f0c3e35e567d38e15b2e5fa056f0ef7d304456 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 17 Jul 2024 14:03:45 -0400 Subject: [PATCH 29/42] BUG: Fixing Issue with Copying Items using Expressions Closes #538 --- smtk/attribute/ValueItem.cxx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smtk/attribute/ValueItem.cxx b/smtk/attribute/ValueItem.cxx index 13dd79360..0b89ad945 100644 --- a/smtk/attribute/ValueItem.cxx +++ b/smtk/attribute/ValueItem.cxx @@ -527,8 +527,8 @@ Item::Status ValueItem::assign( else { // Do we have the expression in the options' mapping information? - Attribute* rawDestExpAtt = - options.itemOptions.targetObjectFromSourceId(sourceValueItem->attribute()->id()); + Attribute* rawDestExpAtt = options.itemOptions.targetObjectFromSourceId( + sourceValueItem->expression()->id()); if (rawDestExpAtt) { status = this->setExpression(rawDestExpAtt->shared_from_this()); From 883db2362d67ea9c9d6599509dfd7d101c69c93e Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Mon, 22 Jul 2024 14:21:06 -0400 Subject: [PATCH 30/42] ENH: Adding Support for vector of int and long Properties for XML Parsers Also modified unitXmlReaderProperties.cxx and propertiesExample.sbt to test these enhancements. --- .../propertiesExample.sbt | 6 +++++ .../testing/cxx/unitXmlReaderProperties.cxx | 22 +++++++++++++++++++ smtk/io/XmlPropertyParsingHelper.txx | 21 ++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/data/attribute/attribute_collection/propertiesExample.sbt b/data/attribute/attribute_collection/propertiesExample.sbt index dcd34b0be..b07fe14f4 100644 --- a/data/attribute/attribute_collection/propertiesExample.sbt +++ b/data/attribute/attribute_collection/propertiesExample.sbt @@ -5,6 +5,12 @@ 3.141 Test string YES + + 10 + + + 1000 + the dog a cat diff --git a/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx b/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx index c6982be01..cae20cdfb 100644 --- a/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx +++ b/smtk/attribute/testing/cxx/unitXmlReaderProperties.cxx @@ -64,6 +64,28 @@ int unitXmlReaderProperties(int /*unused*/, char* /*unused*/[]) smtkTest( attRes->properties().at("pb"), "Resource Bool Property pb was false but should be true"); + smtkTest( + attRes->properties().contains>("pvi"), + "Resource does not contains vector Property pvi"); + smtkTest( + attRes->properties().at>("pvi")[0] == 10, + "Resource's vector Property pvi [0] = " + << attRes->properties().at>("pvi")[0] << " but should be 10"); + smtkTest( + attRes->properties().contains>("pvl"), + "Resource does not contains vector Property pvl"); + smtkTest( + attRes->properties().at>("pvl")[0] == 1000, + "Resource's vector Property pvi [0] = " + << attRes->properties().at>("pvl")[0] << " but should be 1000"); + smtkTest( + attRes->properties().contains>("animals"), + "Resource does not contains vector Property animals"); + smtkTest( + attRes->properties().at>("animals")[0] == "the dog", + "Resource's vector Property animals [0] = \"" + << attRes->properties().at>("animals")[0] + << "\" but should be \"the dog\""); // Now lets test the properties on the attribute and definition auto att = attRes->findAttribute("foo"); smtkTest(att != nullptr, "Could not find attribute foo"); diff --git a/smtk/io/XmlPropertyParsingHelper.txx b/smtk/io/XmlPropertyParsingHelper.txx index a95bc482e..12c3b5cdb 100644 --- a/smtk/io/XmlPropertyParsingHelper.txx +++ b/smtk/io/XmlPropertyParsingHelper.txx @@ -29,6 +29,19 @@ double node_as(const xml_node& node) return node.text().as_double(); } +template<> +int node_as(const xml_node& node) +{ + return node.text().as_int(); +} + +template<> +long node_as(const xml_node& node) +{ + std::string v = node.text().as_string(); + return std::stol(v); +} + template<> std::string node_as(const xml_node& node) { @@ -89,6 +102,14 @@ void processProperties(T& object, xml_node& propertiesNode, smtk::io::Logger& lo { nodeToData(propNode, object->properties().template get>()[propName]); } + else if (propType == "vector[int]") + { + nodeToData(propNode, object->properties().template get>()[propName]); + } + else if (propType == "vector[long]") + { + nodeToData(propNode, object->properties().template get>()[propName]); + } else if (propType == "bool") { std::string sval = propNode.text().as_string(); From 6480331bbce4b8b346a942a9628d8aecb4ca8f88 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Mon, 22 Jul 2024 16:53:57 -0400 Subject: [PATCH 31/42] ENH: Displaying Attribute with Units Using the new ``qtUnitsLineEdit`` class, end-users can now set units on an Attribute (assuming that its Definition supports units). This required changes to ``qtAttribute`` to not consider an Attribute *empty* if it had no items to display but did have specified units. New XML Attribute for controlling how Units are displayed ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can use UnitsMode to indicate how an Attribute's units should be displayed. * none - do not display the attribute's units * viewOnly - display but do not allow the units to be changed * editable - allow the user to edit the attribute's units **Note** that these values are case insensitive See DoubleItemExample.sbt as an example that demonstrates this new functionality. Line Edit widget with unit completion ------------------------------------- The ``qtUnitsLineEdit.cxx`` class is for entering strings that represent units. The widget provides a units aware completer as well as color coding its background based on the entry being a valid unit or if the unit is the default. as in the case of ``qtDoubleUnitsLineEdit.cxx`` class, it can make use of the unit-library's ``PreferredUnits`` class to suggest unit completions. This allows workflow developers to provide a tailored list of suggested completions rather than listing every available compatible unit. Changes to ``qtBaseAttributeView`` and Derived View Classes ------------------------------------------------------------ * Added displayAttribute method that returns true if the attribute should be displayed based on its relevance. * Changed displayItem to take in a ``const smtk::attribute::ItemDefinitionPtr&`` instead of a ``smtk::attribute::ItemDefinitionPtr`` **Note** that this does break API though it is very simple to update to the new API Also added the ability to print out a reference item's acceptance/rejection criteria - for debugging purposes --- .../DoubleItemExample.sbt | 18 +- doc/release/notes/qt-unit-suggestions.rst | 35 +++ doc/userguide/attribute/file-syntax.rst | 13 +- smtk/attribute/ReferenceItem.cxx | 5 +- smtk/attribute/ReferenceItemDefinition.cxx | 16 ++ smtk/attribute/ReferenceItemDefinition.h | 3 + .../operators/smtkAssignColorsView.cxx | 2 +- .../paraview/operators/smtkAssignColorsView.h | 2 +- .../smtkDataSetInfoInspectorView.cxx | 2 +- .../operators/smtkDataSetInfoInspectorView.h | 2 +- .../operators/smtkEditPropertiesView.cxx | 2 +- .../operators/smtkEditPropertiesView.h | 2 +- .../operators/smtkMeshInspectorView.cxx | 2 +- .../operators/smtkMeshInspectorView.h | 2 +- smtk/extension/qt/CMakeLists.txt | 2 + smtk/extension/qt/qtAttribute.cxx | 134 +++++++++-- smtk/extension/qt/qtAttribute.h | 1 + smtk/extension/qt/qtAttributeEditorDialog.cxx | 6 + smtk/extension/qt/qtBaseAttributeView.cxx | 12 +- smtk/extension/qt/qtBaseAttributeView.h | 6 +- smtk/extension/qt/qtInstancedView.cxx | 2 +- smtk/extension/qt/qtUnitsLineEdit.cxx | 213 ++++++++++++++++++ smtk/extension/qt/qtUnitsLineEdit.h | 82 +++++++ 23 files changed, 527 insertions(+), 37 deletions(-) create mode 100644 smtk/extension/qt/qtUnitsLineEdit.cxx create mode 100644 smtk/extension/qt/qtUnitsLineEdit.h diff --git a/data/attribute/attribute_collection/DoubleItemExample.sbt b/data/attribute/attribute_collection/DoubleItemExample.sbt index 62975ebdb..1e4d7c4f6 100644 --- a/data/attribute/attribute_collection/DoubleItemExample.sbt +++ b/data/attribute/attribute_collection/DoubleItemExample.sbt @@ -1,11 +1,11 @@ - + - - + + @@ -19,7 +19,17 @@ - + + + + + + + + + + + diff --git a/doc/release/notes/qt-unit-suggestions.rst b/doc/release/notes/qt-unit-suggestions.rst index 6d03706d5..44d14ed17 100644 --- a/doc/release/notes/qt-unit-suggestions.rst +++ b/doc/release/notes/qt-unit-suggestions.rst @@ -8,3 +8,38 @@ The ``qtDoubleUnitsLineEdit.cxx`` class now uses the unit-library's ``PreferredUnits`` class to suggest unit completions. This allows workflow developers to provide a tailored list of suggested completions rather than listing every available compatible unit. + +Line Edit widget with unit completion +------------------------------------- + +The ``qtUnitsLineEdit.cxx`` class is for entering strings that represent units. +The widget provides a units aware completer as well as color coding its background +based on the entry being a valid unit or if the unit is the default. +as in the case of ``qtDoubleUnitsLineEdit.cxx`` class, it can make use of the unit-library's +``PreferredUnits`` class to suggest unit completions. This allows +workflow developers to provide a tailored list of suggested completions +rather than listing every available compatible unit. + +Displaying Attribute with Units +------------------------------- +Using the new ``qtUnitsLineEdit`` class, end-users can now set units on an Attribute (assuming that its Definition supports units). +This required changes to ``qtAttribute`` to not consider an Attribute *empty* if it had no items to display but did have specified units. + +New XML Attribute for controlling how Units are displayed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can use UnitsMode to indicate how an Attribute's units should be displayed. + +* none - do not display the attribute's units +* viewOnly - display but do not allow the units to be changed +* editable - allow the user to edit the attribute's units + +**Note** that these values are case insensitive + +See DoubleItemExample.sbt as an example that demonstrates this new functionality. + +Changes to ``qtBaseAttributeView`` and Derived View Classes +------------------------------------------------------------ + +* Added displayAttribute method that returns true if the attribute should be displayed based on its relevance. +* Changed displayItem to take in a ``const smtk::attribute::ItemDefinitionPtr&`` instead of a ``smtk::attribute::ItemDefinitionPtr`` **Note** that this does break API though it is very simple to update to the new API diff --git a/doc/userguide/attribute/file-syntax.rst b/doc/userguide/attribute/file-syntax.rst index 27c475b26..99943479c 100644 --- a/doc/userguide/attribute/file-syntax.rst +++ b/doc/userguide/attribute/file-syntax.rst @@ -47,7 +47,7 @@ This element can contain the following optional children XML Elements: - Templates : used to define reusable parameterized blocks of Attribute/Item Definitions Information(see `Templates Section`_) - Definitions : used to define attribute definitions (see `Definitions Section`_) - Attributes : used to define attributes -- Views : used to define various Views (used to create GUIs) +- Views : used to define various Views (used to create GUIs) (see `Views Section`_) Includes Section -------------------- @@ -2286,6 +2286,17 @@ The XML Element should have one child element called . The (Optional) – default is false + * - UnitsMode + - A string that indicates how to handle the case where the attribute has units assigned to it. Supported value include: + + * "None" (do not display the attribute's units) + * "Editable" (allow the user to edit the attribute's units) + * "ViewOnly" (display the attribute's units but do not allow them to be edited) + + **Note**: these values are case insensitive + + (Optional) - default is editable + Here in an example UI of an Associations View. .. findfigure:: instanceViewExample.* diff --git a/smtk/attribute/ReferenceItem.cxx b/smtk/attribute/ReferenceItem.cxx index bc1fa1a8e..4a1e6a2d7 100644 --- a/smtk/attribute/ReferenceItem.cxx +++ b/smtk/attribute/ReferenceItem.cxx @@ -896,12 +896,15 @@ Item::Status ReferenceItem::assign( } else { + auto def = this->definitionAs(); result.markFailed(); smtkErrorMacro( logger, "ReferenceItem: " << name() << "'s number of values (" << myNumVals << ") can not hold source ReferenceItem's number of values (" - << sourceNumVals << ") and Partial Copying was not permitted"); + << sourceNumVals << ") and Partial Copying was not permitted." + << " Reference Criteria:\n" + << def->criteriaAsString()); return result; } } diff --git a/smtk/attribute/ReferenceItemDefinition.cxx b/smtk/attribute/ReferenceItemDefinition.cxx index d06fc5dfb..72f89680f 100644 --- a/smtk/attribute/ReferenceItemDefinition.cxx +++ b/smtk/attribute/ReferenceItemDefinition.cxx @@ -259,6 +259,22 @@ void ReferenceItemDefinition::copyTo(Ptr dest, smtk::attribute::ItemDefinition:: dest->m_conditionalItemNames = m_conditionalItemNames; } +std::string ReferenceItemDefinition::criteriaAsString() const +{ + std::string s; + s.append("Rejection Criteria\n"); + for (const auto& rejected : m_rejected) + { + s.append("\t(").append(rejected.first).append(", ").append(rejected.second).append(")\n"); + } + s.append("Acceptance Criteria\n"); + for (const auto& accepted : m_acceptable) + { + s.append("\t(").append(accepted.first).append(", ").append(accepted.second).append(")\n"); + } + return s; +} + bool ReferenceItemDefinition::checkResource(const smtk::resource::Resource& rsrc) const { // TODO: diff --git a/smtk/attribute/ReferenceItemDefinition.h b/smtk/attribute/ReferenceItemDefinition.h index 7630dff57..e8e64c07e 100644 --- a/smtk/attribute/ReferenceItemDefinition.h +++ b/smtk/attribute/ReferenceItemDefinition.h @@ -240,6 +240,9 @@ class SMTKCORE_EXPORT ReferenceItemDefinition : public ItemDefinition // Returns the conditional that matches object std::size_t testConditionals(PersistentObjectPtr& objet) const; + // Returns the criteria information as a string + std::string criteriaAsString() const; + protected: ReferenceItemDefinition(const std::string& myName); diff --git a/smtk/extension/paraview/operators/smtkAssignColorsView.cxx b/smtk/extension/paraview/operators/smtkAssignColorsView.cxx index 2e41f8c1e..b14dcdc48 100644 --- a/smtk/extension/paraview/operators/smtkAssignColorsView.cxx +++ b/smtk/extension/paraview/operators/smtkAssignColorsView.cxx @@ -137,7 +137,7 @@ smtkAssignColorsView::~smtkAssignColorsView() delete this->Internals; } -bool smtkAssignColorsView::displayItem(smtk::attribute::ItemPtr item) const +bool smtkAssignColorsView::displayItem(const smtk::attribute::ItemPtr& item) const { if (item && item->name() == "colors") { diff --git a/smtk/extension/paraview/operators/smtkAssignColorsView.h b/smtk/extension/paraview/operators/smtkAssignColorsView.h index db475e5a2..1d2b897f4 100644 --- a/smtk/extension/paraview/operators/smtkAssignColorsView.h +++ b/smtk/extension/paraview/operators/smtkAssignColorsView.h @@ -36,7 +36,7 @@ class SMTKPQOPERATIONVIEWSEXT_EXPORT smtkAssignColorsView static QIcon renderPaletteSwatch(const QList& color, int width, int radius); static QIcon renderInvalidSwatch(int radius); - bool displayItem(smtk::attribute::ItemPtr) const override; + bool displayItem(const smtk::attribute::ItemPtr&) const override; public Q_SLOTS: void updateUI() override; diff --git a/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.cxx b/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.cxx index 81dbbbea4..4728cdd41 100644 --- a/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.cxx +++ b/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.cxx @@ -158,7 +158,7 @@ smtkDataSetInfoInspectorView::~smtkDataSetInfoInspectorView() delete m_p; } -bool smtkDataSetInfoInspectorView::displayItem(smtk::attribute::ItemPtr item) const +bool smtkDataSetInfoInspectorView::displayItem(const smtk::attribute::ItemPtr& item) const { // Here is where item visibility can be overridden for the custom view. return this->qtBaseAttributeView::displayItem(item); diff --git a/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.h b/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.h index 9b1222925..952ac2b34 100644 --- a/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.h +++ b/smtk/extension/paraview/operators/smtkDataSetInfoInspectorView.h @@ -33,7 +33,7 @@ class SMTKPQOPERATIONVIEWSEXT_EXPORT smtkDataSetInfoInspectorView static smtk::extension::qtBaseView* createViewWidget(const smtk::view::Information& info); - bool displayItem(smtk::attribute::ItemPtr) const override; + bool displayItem(const smtk::attribute::ItemPtr&) const override; public Q_SLOTS: // NOLINT(readability-redundant-access-specifiers) void updateUI() override; diff --git a/smtk/extension/paraview/operators/smtkEditPropertiesView.cxx b/smtk/extension/paraview/operators/smtkEditPropertiesView.cxx index 876df1041..c12271db8 100644 --- a/smtk/extension/paraview/operators/smtkEditPropertiesView.cxx +++ b/smtk/extension/paraview/operators/smtkEditPropertiesView.cxx @@ -512,7 +512,7 @@ smtkEditPropertiesView::~smtkEditPropertiesView() delete m_p; } -bool smtkEditPropertiesView::displayItem(smtk::attribute::ItemPtr item) const +bool smtkEditPropertiesView::displayItem(const smtk::attribute::ItemPtr& item) const { if (item && item->name() == "colors") { diff --git a/smtk/extension/paraview/operators/smtkEditPropertiesView.h b/smtk/extension/paraview/operators/smtkEditPropertiesView.h index 79dadfb97..5524f187e 100644 --- a/smtk/extension/paraview/operators/smtkEditPropertiesView.h +++ b/smtk/extension/paraview/operators/smtkEditPropertiesView.h @@ -29,7 +29,7 @@ class SMTKPQOPERATIONVIEWSEXT_EXPORT smtkEditPropertiesView static bool validateInformation(const smtk::view::Information& info); static smtk::extension::qtBaseView* createViewWidget(const smtk::view::Information& info); - bool displayItem(smtk::attribute::ItemPtr item) const override; + bool displayItem(const smtk::attribute::ItemPtr& item) const override; public Q_SLOTS: // NOLINT(readability-redundant-access-specifiers) void updateUI() override; diff --git a/smtk/extension/paraview/operators/smtkMeshInspectorView.cxx b/smtk/extension/paraview/operators/smtkMeshInspectorView.cxx index ce878ecc6..51ae9a398 100644 --- a/smtk/extension/paraview/operators/smtkMeshInspectorView.cxx +++ b/smtk/extension/paraview/operators/smtkMeshInspectorView.cxx @@ -240,7 +240,7 @@ smtkMeshInspectorView::~smtkMeshInspectorView() delete m_p; } -bool smtkMeshInspectorView::displayItem(smtk::attribute::ItemPtr item) const +bool smtkMeshInspectorView::displayItem(const smtk::attribute::ItemPtr& item) const { // Here is where item visibility can be overridden for the custom view. return this->qtBaseAttributeView::displayItem(item); diff --git a/smtk/extension/paraview/operators/smtkMeshInspectorView.h b/smtk/extension/paraview/operators/smtkMeshInspectorView.h index f7df11be9..c340ae884 100644 --- a/smtk/extension/paraview/operators/smtkMeshInspectorView.h +++ b/smtk/extension/paraview/operators/smtkMeshInspectorView.h @@ -31,7 +31,7 @@ class SMTKPQOPERATIONVIEWSEXT_EXPORT smtkMeshInspectorView : public smtk::extens static smtk::extension::qtBaseView* createViewWidget(const smtk::view::Information& info); - bool displayItem(smtk::attribute::ItemPtr) const override; + bool displayItem(const smtk::attribute::ItemPtr&) const override; public Q_SLOTS: void updateUI() override; diff --git a/smtk/extension/qt/CMakeLists.txt b/smtk/extension/qt/CMakeLists.txt index 0920d0e64..191cd3541 100644 --- a/smtk/extension/qt/CMakeLists.txt +++ b/smtk/extension/qt/CMakeLists.txt @@ -50,6 +50,7 @@ set(QAttrLibSrcs qtLineEdit.cxx qtDoubleLineEdit.cxx qtDoubleUnitsLineEdit.cxx + qtUnitsLineEdit.cxx qtViewRegistrar.cxx qtWorkletModel.cxx qtWorkletPalette.cxx @@ -170,6 +171,7 @@ set(QAttrLibMocHeaders qtLineEdit.h qtDoubleLineEdit.h qtDoubleUnitsLineEdit.h + qtUnitsLineEdit.cxx qtItem.h qtDiscreteValueEditor.h diff --git a/smtk/extension/qt/qtAttribute.cxx b/smtk/extension/qt/qtAttribute.cxx index d7f79c0b1..4c716eb05 100644 --- a/smtk/extension/qt/qtAttribute.cxx +++ b/smtk/extension/qt/qtAttribute.cxx @@ -11,6 +11,7 @@ #include "smtk/extension/qt/qtBaseAttributeView.h" #include "smtk/extension/qt/qtUIManager.h" +#include "smtk/extension/qt/qtUnitsLineEdit.h" #include "smtk/attribute/Attribute.h" #include "smtk/attribute/ComponentItem.h" @@ -25,11 +26,14 @@ #include "smtk/attribute/ValueItem.h" #include "smtk/attribute/VoidItem.h" +#include "smtk/common/StringUtil.h" + #include "smtk/io/Logger.h" #include #include #include +#include #include #include // for atexit() @@ -42,6 +46,12 @@ using namespace smtk::extension; class qtAttributeInternals { public: + enum UnitsMode + { + Editable, + ViewOnly, + None + }; qtAttributeInternals( smtk::attribute::AttributePtr myAttribute, const smtk::view::Configuration::Component& comp, @@ -61,6 +71,23 @@ class qtAttributeInternals } auto iviews = m_attComp.child(sindex); qtAttributeItemInfo::buildFromComponent(iviews, m_view, m_itemViewMap); + std::string unitsStr; + if (m_attComp.attribute("UnitsMode", unitsStr)) + { + smtk::common::StringUtil::lower(unitsStr); + if (unitsStr == "none") + { + m_unitsMode = UnitsMode::None; + } + else if (unitsStr == "viewonly") + { + m_unitsMode = UnitsMode::ViewOnly; + } + else if (unitsStr == "editable") + { + m_unitsMode = UnitsMode::Editable; + } + } } ~qtAttributeInternals() = default; @@ -70,6 +97,7 @@ class qtAttributeInternals QPointer m_view; smtk::view::Configuration::Component m_attComp; std::map m_itemViewMap; + UnitsMode m_unitsMode = UnitsMode::Editable; }; qtAttribute::qtAttribute( @@ -84,7 +112,6 @@ qtAttribute::qtAttribute( m_widget = nullptr; m_useSelectionManager = false; this->createWidget(createWidgetWhenEmpty); - m_isEmpty = true; } qtAttribute::~qtAttribute() @@ -108,39 +135,57 @@ void qtAttribute::removeItems() void qtAttribute::createWidget(bool createWidgetWhenEmpty) { - // Initially there are no items being displayed + // A qtAttribute is empty if it has no items to display and if it + // does not need to display its units. Assume that it is empty and then + // updated it if that is not the case. m_isEmpty = true; + auto att = this->attribute(); - if ((att == nullptr) || ((att->numberOfItems() == 0) && (att->associations() == nullptr))) + if ( + (att == nullptr) || + ((att->numberOfItems() == 0) && (att->associations() == nullptr) && + (att->units().empty() || (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None)))) { return; } - int numShowItems = 0; - std::size_t i, n = att->numberOfItems(); - if (m_internals->m_view) + // Based on the View, see if the attribute should be displayed + if (!m_internals->m_view->displayAttribute(att)) + { + return; + } + + // We will always display units if there are any associated with the attribute and we were told to display them else + // we need to see if any items would be displayed + if (att->units().empty() || (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None)) { - for (i = 0; i < n; i++) + int numShowItems = 0; + std::size_t i, n = att->numberOfItems(); + if (m_internals->m_view) { - if (m_internals->m_view->displayItem(att->item(static_cast(i)))) + for (i = 0; i < n; i++) + { + if (m_internals->m_view->displayItem(att->item(static_cast(i)))) + { + numShowItems++; + } + } + // also check associations + if (m_internals->m_view->displayItem(att->associations())) { numShowItems++; } } - // also check associations - if (m_internals->m_view->displayItem(att->associations())) + else // show everything { - numShowItems++; + numShowItems = static_cast(att->associations() ? n + 1 : n); + } + if ((numShowItems == 0) && !createWidgetWhenEmpty) + { + return; } } - else // show everything - { - numShowItems = static_cast(att->associations() ? n + 1 : n); - } - if ((numShowItems == 0) && !createWidgetWhenEmpty) - { - return; - } + m_isEmpty = false; QFrame* attFrame = new QFrame(this->parentWidget()); attFrame->setObjectName(att->name().c_str()); @@ -195,6 +240,48 @@ void qtAttribute::createBasicLayout(bool includeAssociations) qtItem* qItem = nullptr; smtk::attribute::AttributePtr att = this->attribute(); auto* uiManager = m_internals->m_view->uiManager(); + // If the attribute has units - display them + if (!(att->units().empty() || + (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None))) + { + QFrame* unitFrame = new QFrame(this->m_widget); + std::string unitFrameName = att->name() + "UnitsFrame"; + unitFrame->setObjectName(unitFrameName.c_str()); + unitFrame->setFrameShape(QFrame::NoFrame); + + QHBoxLayout* unitsLayout = new QHBoxLayout(unitFrame); + std::string unitsLayoutName = att->name() + "UnitsLayout"; + unitsLayout->setObjectName(unitsLayoutName.c_str()); + unitsLayout->setMargin(0); + unitFrame->setLayout(unitsLayout); + std::string unitsLabel = att->name() + "'s units:"; + + if (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::Editable) + { + QLabel* uLabel = new QLabel(unitsLabel.c_str(), unitFrame); + unitsLayout->addWidget(uLabel); + auto* unitsWidget = new qtUnitsLineEdit( + att->definition()->units().c_str(), att->definition()->unitsSystem(), uiManager, unitFrame); + std::string unitsWidgetName = att->name() + "UnitsLineWidget"; + unitsWidget->setObjectName(unitsWidgetName.c_str()); + unitsLayout->addWidget(unitsWidget); + unitsWidget->setAndClassifyText(att->units().c_str()); + // Create a regular expression validator to prevent leading spaces + unitsWidget->setValidator( + new QRegularExpressionValidator(QRegularExpression("^\\S*$"), this)); + QObject::connect( + unitsWidget, &qtUnitsLineEdit::editingCompleted, this, &qtAttribute::onUnitsChanged); + } + else + { + // if we are here, units are to be displayed as read only so just add the units to the label + unitsLabel.append(att->units()); + QLabel* uLabel = new QLabel(unitsLabel.c_str(), unitFrame); + unitsLayout->addWidget(uLabel); + } + layout->addWidget(unitFrame); + m_isEmpty = false; + } // If there are model associations for the attribute, create UI for them if requested. // This will be the same widget used for ModelEntityItem. if (includeAssociations && att->associations()) @@ -297,3 +384,12 @@ bool qtAttribute::isValid() const } return true; } + +void qtAttribute::onUnitsChanged(QString newUnits) +{ + auto att = this->attribute(); + if (att && att->setLocalUnits(newUnits.toStdString())) + { + Q_EMIT this->modified(); + } +} diff --git a/smtk/extension/qt/qtAttribute.h b/smtk/extension/qt/qtAttribute.h index a959fee5f..c28718f89 100644 --- a/smtk/extension/qt/qtAttribute.h +++ b/smtk/extension/qt/qtAttribute.h @@ -88,6 +88,7 @@ class SMTKQTEXT_EXPORT qtAttribute : public QObject protected Q_SLOTS: void onItemModified(); + void onUnitsChanged(QString); private: qtAttributeInternals* m_internals; diff --git a/smtk/extension/qt/qtAttributeEditorDialog.cxx b/smtk/extension/qt/qtAttributeEditorDialog.cxx index 8e41427e4..b526bcbb9 100644 --- a/smtk/extension/qt/qtAttributeEditorDialog.cxx +++ b/smtk/extension/qt/qtAttributeEditorDialog.cxx @@ -27,6 +27,7 @@ #include #include +#include using namespace smtk::extension; @@ -106,4 +107,9 @@ void qtAttributeEditorDialog::attributeNameChanged() return; } attResource->rename(m_attribute, newName); + // This method can be triggered by a change of focus event. Since + // updating the instanceView's contents causes all of its internal widgets + // to be destroyed we need to use a timer so that if a focus event was involved, + // it will be processed before the view is updated. + QTimer::singleShot(0, [=]() { m_instancedView->updateUI(); }); } diff --git a/smtk/extension/qt/qtBaseAttributeView.cxx b/smtk/extension/qt/qtBaseAttributeView.cxx index 74127c822..0f10e36b5 100644 --- a/smtk/extension/qt/qtBaseAttributeView.cxx +++ b/smtk/extension/qt/qtBaseAttributeView.cxx @@ -156,16 +156,24 @@ void qtBaseAttributeView::getDefinitions( } } -bool qtBaseAttributeView::displayItem(smtk::attribute::ItemPtr item) const +bool qtBaseAttributeView::displayItem(const smtk::attribute::ItemPtr& item) const { if (!item) { return false; } - auto idef = item->definition(); return this->advanceLevelTest(item) && this->categoryTest(item); } +bool qtBaseAttributeView::displayAttribute(const smtk::attribute::AttributePtr& att) const +{ + if (!att) + { + return false; + } + return m_ignoreCategories || att->isRelevant(); +} + bool qtBaseAttributeView::displayItemDefinition( const smtk::attribute::ItemDefinitionPtr& idef) const { diff --git a/smtk/extension/qt/qtBaseAttributeView.h b/smtk/extension/qt/qtBaseAttributeView.h index 0f410a04e..25de25491 100644 --- a/smtk/extension/qt/qtBaseAttributeView.h +++ b/smtk/extension/qt/qtBaseAttributeView.h @@ -44,7 +44,11 @@ class SMTKQTEXT_EXPORT qtBaseAttributeView : public qtBaseView /// If you override this method, you may wish to call this from /// your override, since it checks the advance-level and category /// settings for the item. - virtual bool displayItem(smtk::attribute::ItemPtr) const; + virtual bool displayItem(const smtk::attribute::ItemPtr&) const; + /// Determines if an Attribute should be displayed solely based on categories + /// + /// This method is used by qtAttribute to deal with Attributes with units. + virtual bool displayAttribute(const smtk::attribute::AttributePtr&) const; virtual bool displayItemDefinition(const smtk::attribute::ItemDefinitionPtr&) const; /// Determines if an item can be modified virtual bool isItemWriteable(const smtk::attribute::ItemPtr&) const; diff --git a/smtk/extension/qt/qtInstancedView.cxx b/smtk/extension/qt/qtInstancedView.cxx index 76581a035..b214308e8 100644 --- a/smtk/extension/qt/qtInstancedView.cxx +++ b/smtk/extension/qt/qtInstancedView.cxx @@ -265,7 +265,7 @@ void qtInstancedView::updateUI() this->Internals->m_isEmpty = true; for (i = 0; i < n; i++) { - if (atts[i]->numberOfItems() > 0) + if ((atts[i]->numberOfItems() > 0) || !atts[i]->units().empty()) { qtAttribute* attInstance = new qtAttribute(atts[i], comps[i], this->widget(), this); if (attInstance) diff --git a/smtk/extension/qt/qtUnitsLineEdit.cxx b/smtk/extension/qt/qtUnitsLineEdit.cxx new file mode 100644 index 000000000..fa6e96767 --- /dev/null +++ b/smtk/extension/qt/qtUnitsLineEdit.cxx @@ -0,0 +1,213 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= + +#include "smtk/extension/qt/qtUnitsLineEdit.h" + +#include "smtk/common/StringUtil.h" +#include "smtk/extension/qt/qtUIManager.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "units/Converter.h" +#include "units/PreferredUnits.h" + +#include // std::sort et al +#include + +using namespace smtk::attribute; +using namespace smtk::extension; + +namespace +{ + +/** \brief Subclass QStringListModel to highlight first item */ +class qtCompleterStringModel : public QStringListModel +{ +public: + qtCompleterStringModel(QObject* parent = nullptr) + : QStringListModel(parent) + { + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + if (role == Qt::FontRole && index.row() == 0) + { + QVariant var = QStringListModel::data(index, role); + QFont font = qvariant_cast(var); + font.setBold(true); + return font; + } + // (else) + return QStringListModel::data(index, role); + } +}; + +} // anonymous namespace + +namespace smtk +{ +namespace extension +{ + +qtUnitsLineEdit::qtUnitsLineEdit( + const QString& baseUnit, + const std::shared_ptr& unitSys, + qtUIManager* uiManager, + QWidget* parent) + : QLineEdit(parent) + , m_baseUnit(baseUnit) + , m_unitSys(unitSys) + , m_uiManager(uiManager) +{ + + // Parsing the Item's unit string + bool parsedOK = false; + auto unit = m_unitSys->unit(m_baseUnit.toStdString(), &parsedOK); + + // If the units are supported then setup a completer + if (parsedOK) + { + + std::shared_ptr preferred; + auto cit = m_unitSys->m_unitContexts.find(m_unitSys->m_activeUnitContext); + if (cit != m_unitSys->m_unitContexts.end()) + { + preferred = cit->second; + } + + // Get the completer strings + QStringList unitChoices; + + // Create a list of possible units names: + if (preferred) + { + // We have a context; this provides preferred units as a + // developer-ordered list of units, placing dUnits at + // position 0 of the suggestions. + units::CompatibleUnitOptions opts; + opts.m_inputUnitPriority = 0; + for (const auto& suggestion : preferred->suggestedUnits(m_baseUnit.toStdString(), opts)) + { + unitChoices.push_back(suggestion.c_str()); + } + } + else + { + // We don't have a unit-system context; just + // find compatible units. This may present + // duplicates and is definitely not sorted. + + // Get list of compatible units + auto compatibleUnits = m_unitSys->compatibleUnits(unit); + for (const auto& unit : compatibleUnits) + { + unitChoices.push_back(unit.name().c_str()); + } + // Lets remove duplicates and sort the list + unitChoices.removeDuplicates(); + unitChoices.sort(); + // Now make the Item's units appear at the top + //unitChoices.push_front(dUnits.c_str()); + } + + auto* model = new qtCompleterStringModel(this); + model->setStringList(unitChoices); + m_completer = new QCompleter(model, parent); + m_completer->setCompletionMode(QCompleter::PopupCompletion); + this->setCompleter(m_completer); + } + + // Connect up the signals + QObject::connect(this, &QLineEdit::textEdited, this, &qtUnitsLineEdit::onTextEdited); + QObject::connect(this, &QLineEdit::editingFinished, this, &qtUnitsLineEdit::onEditFinished); +} + +bool qtUnitsLineEdit::isCurrentTextValid() const +{ + auto utext = this->text().toStdString(); + // Remove all surrounding white space + smtk::common::StringUtil::trim(utext); + if (utext.empty()) + { + return false; + } + bool parsedOK = false; + auto baseU = m_unitSys->unit(m_baseUnit.toStdString(), &parsedOK); + + // Is the current value supported by the unit system? + parsedOK = false; + auto newU = m_unitSys->unit(utext, &parsedOK); + if (!parsedOK) + { + return false; + } + // Can you convert between the units? + return (m_unitSys->convert(baseU, newU) != nullptr); +} + +void qtUnitsLineEdit::setAndClassifyText(const QString& newVal) +{ + this->setText(newVal); + this->onTextEdited(); +} + +void qtUnitsLineEdit::onTextEdited() +{ + if (!this->isCurrentTextValid()) + { + m_uiManager->setWidgetColorToInvalid(this); + return; + } + if (this->text() == m_baseUnit) + { + m_uiManager->setWidgetColorToDefault(this); + return; + } + m_uiManager->setWidgetColorToNormal(this); +} + +void qtUnitsLineEdit::onEditFinished() +{ + // This works around unexpected QLineEdit behavior. When certain keys + // are pressed (backspace, e.g.), a QLineEdit::editingFinished signal + // is emitted. The cause is currently unknown. + // This code ignores the editingFinished signal if the editor is still + // in focus and the last key pressed was not or . + bool finished = (m_lastKey == Qt::Key_Enter) || (m_lastKey == Qt::Key_Return); + if (!finished && this->hasFocus()) + { + return; + } + + // Don't emit completion if the current string is invalid + if (this->isCurrentTextValid()) + { + Q_EMIT this->editingCompleted(this->text()); + } +} + +void qtUnitsLineEdit::keyPressEvent(QKeyEvent* event) +{ + // Save last key pressed + m_lastKey = event->key(); + QLineEdit::keyPressEvent(event); +} + +} // namespace extension +} // namespace smtk diff --git a/smtk/extension/qt/qtUnitsLineEdit.h b/smtk/extension/qt/qtUnitsLineEdit.h new file mode 100644 index 000000000..318ccf7f8 --- /dev/null +++ b/smtk/extension/qt/qtUnitsLineEdit.h @@ -0,0 +1,82 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_extension_qt_qtUnitsLineEdit_h +#define smtk_extension_qt_qtUnitsLineEdit_h + +#include + +#include "smtk/extension/qt/Exports.h" + +#include "smtk/PublicPointerDefs.h" + +#include +#include +#include +#include // for formatDouble + +#include "units/System.h" +#include "units/Unit.h" + +#include + +class QCompleter; +class QKeyEvent; + +namespace smtk +{ +namespace extension +{ + +class qtUIManager; + +/**\brief qtUnitsLineEdit provides units-aware line edit */ +class SMTKQTEXT_EXPORT qtUnitsLineEdit : public QLineEdit +{ + Q_OBJECT + +public: + using Superclass = QLineEdit; + + qtUnitsLineEdit( + const QString& baseUnit, + const std::shared_ptr& unitSys, + qtUIManager* uiManager, + QWidget* parent = nullptr); + + bool isCurrentTextValid() const; + void setAndClassifyText(const QString&); +Q_SIGNALS: + /** + * Indicates that the user has finished editing and the appropriate units have been added to + * the value + */ + void editingCompleted(QString); + +public Q_SLOTS: + void onEditFinished(); + +protected Q_SLOTS: + void onTextEdited(); + +protected: + void keyPressEvent(QKeyEvent* event) override; + + int m_lastKey = -1; + QString m_baseUnit; + const std::shared_ptr& m_unitSys; + qtUIManager* m_uiManager; + + QCompleter* m_completer = nullptr; +}; + +} // namespace extension +} // namespace smtk + +#endif // smtk_extension_qt_qtUnitsLineEdit_h From 1861c0d1c9968dbfa7c7448041bcf2bf53911630 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Wed, 24 Jul 2024 14:03:58 -0400 Subject: [PATCH 32/42] ENH: Updated CI for SMTK Needed to use the latest version of Units Library that supports "*" units. --- .gitlab/ci/download_superbuild.cmake | 14 +++++++------- .gitlab/os-linux.yml | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitlab/ci/download_superbuild.cmake b/.gitlab/ci/download_superbuild.cmake index 3dcf53f9a..75af0ae6c 100644 --- a/.gitlab/ci/download_superbuild.cmake +++ b/.gitlab/ci/download_superbuild.cmake @@ -10,16 +10,16 @@ cmake_minimum_required(VERSION 3.12) set(data_host "https://data.kitware.com") # Determine the tarball to download. ci-smtk-ci-developer-{date}-{git-sha}.tar.gz -# 20240503: Refresh for new SDK usage (Xcode 14.2) +# 20240724: Refresh for Units Library supporting "*" units if ("$ENV{CMAKE_CONFIGURATION}" MATCHES "vs2022") - set(file_item "6636158ec6586ba79a5656e3") - set(file_hash "d25862f76bbe4256498396cc5c0526ae74cdb6ed66eaf48bd9444edad4ece01f778d20db364a172abcea8a6b43573ead951052419b123d63ec1730b25e3a4ca7") + set(file_item "66a13fdd5d2551c516b1d5c7") + set(file_hash "e248699a4b7926b2bb5fc19880a460ac36c4ebe0896dab77d8232ce96fe934e539ec77ce7602d60490499b8edf756a1ad3a68c18365606e2d8b3e45c6488a240") elseif ("$ENV{CMAKE_CONFIGURATION}" MATCHES "macos_x86_64") - set(file_item "6636158ec6586ba79a5656df") - set(file_hash "811570d6bed9a808fd00c98ed7d435c98408034fcc2bd2705878014a88a66717af566ef549d579052a7d8582a0c7c7393164e4a22ad8abbdb2460d915de4887e") + set(file_item "66a13fdd5d2551c516b1d5c5") + set(file_hash "807ceb6fdfad77103b849659fa033f242d8211895ebd21d9492809cae96e16fb17050daffc7f71a7fc65b8746d34c66e8d05e479fbb523b8a1b4ce0ef9af7cb5") elseif ("$ENV{CMAKE_CONFIGURATION}" MATCHES "macos_arm64") - set(file_item "6636158ec6586ba79a5656dd") - set(file_hash "cc89f4ab4b71ef1946ed9199b9d6216827cd3c9a92a1520718f448aed3a4f9afb334d516cbe06a32f9d01f1dec8232915b4baa6c42632997e37fdc5565ee9a35") + set(file_item "66a13fdd5d2551c516b1d5c3") + set(file_hash "a4697df02417badf76f28ab51d51db4815f13cb861a3e5f0a0d224b5dde0a389c230d385a8f03f3959b32434cadfb375ebe105c041400481d93e70711e9c8389") else () message(FATAL_ERROR "Unknown build to use for the superbuild") diff --git a/.gitlab/os-linux.yml b/.gitlab/os-linux.yml index 1b50e80fe..cbc8ef254 100644 --- a/.gitlab/os-linux.yml +++ b/.gitlab/os-linux.yml @@ -6,8 +6,8 @@ ### Fedora .fedora33: - # Update to include Python support in containers. - image: "kitware/cmb:ci-smtk-fedora33-20231127" + # Update to use Units Library that supports "*" units. + image: "kitware/cmb:ci-smtk-fedora33-20240724" variables: GIT_SUBMODULE_STRATEGY: recursive @@ -67,21 +67,21 @@ .fedora33_vtk_python3: extends: .fedora33 - image: "kitware/cmb:ci-smtk-fedora33-vtk-20231127" + image: "kitware/cmb:ci-smtk-fedora33-vtk-20240724" variables: CMAKE_CONFIGURATION: fedora33_vtk_python3 .fedora33_paraview: extends: .fedora33 - image: "kitware/cmb:ci-smtk-fedora33-paraview-20231127" + image: "kitware/cmb:ci-smtk-fedora33-paraview-20240724" variables: CMAKE_CONFIGURATION: fedora33_paraview .fedora33_paraview59_compat: extends: .fedora33 - image: "kitware/cmb:ci-smtk-fedora33-paraview59-20231127" + image: "kitware/cmb:ci-smtk-fedora33-paraview59-20240724" variables: CMAKE_CONFIGURATION: fedora33_paraview59_compat From ee7d27cec9ca5b7b3302f07217c8aaf20b617b2c Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Tue, 23 Jul 2024 17:00:25 -0400 Subject: [PATCH 33/42] ENH: Supporting Definitions that can take any units Definitions can now be assigned "*" for their units. This indicates that Attributes being defined by the Definition or Definitions derived from it can be assigned any unit that is supported by its units system. Definitions can now inherit the units from their base Definition. When overriding the units being inherited, by default a Definition's units must be compatible with the units coming from its base Definition, though the method provides an option to force units to be set even if they are not compatible. Attributes whose Definition's units are "*" are initially considered unit-less. Please see `unitAttributeUnits `_ for a simple example of using units with Attributes and Definitions. **Note** The method Definition::setUnits has been renamed Definition::setLocalUnits to be in line with Attribute's API since as in the case of Attributes, Definitions can now inherit units from their base Definitions. Also moved the validator for qtUnitsLineEdit out of qtAttribute and into qtUnitsLineEdit --- .../DoubleItemExample.sbt | 2 + doc/release/notes/attributeChanges.rst | 4 + doc/userguide/attribute/concepts.rst | 58 ++++++++--- smtk/attribute/Attribute.cxx | 44 +++++++-- smtk/attribute/Attribute.h | 34 +++++-- smtk/attribute/Definition.cxx | 63 +++++++++++- smtk/attribute/Definition.h | 32 ++++-- smtk/attribute/ReferenceItem.cxx | 5 +- smtk/attribute/Resource.cxx | 2 +- smtk/attribute/json/jsonDefinition.cxx | 8 +- smtk/attribute/pybind11/PybindAttribute.h | 4 + smtk/attribute/pybind11/PybindDefinition.h | 3 +- .../testing/cxx/unitAttributeUnits.cxx | 92 ++++++++++++++++- smtk/extension/qt/qtAttribute.cxx | 89 +++++++++-------- smtk/extension/qt/qtInstancedView.cxx | 2 +- smtk/extension/qt/qtUnitsLineEdit.cxx | 98 ++++++++++++++----- smtk/io/XmlDocV8Parser.cxx | 2 +- smtk/io/XmlV8StringWriter.cxx | 4 +- .../testing/cxx/TestProjectPortability.cxx | 2 +- .../testing/cxx/TestProjectReadWrite2.cxx | 2 +- 20 files changed, 429 insertions(+), 121 deletions(-) diff --git a/data/attribute/attribute_collection/DoubleItemExample.sbt b/data/attribute/attribute_collection/DoubleItemExample.sbt index 1e4d7c4f6..2fca83f56 100644 --- a/data/attribute/attribute_collection/DoubleItemExample.sbt +++ b/data/attribute/attribute_collection/DoubleItemExample.sbt @@ -5,6 +5,7 @@ + @@ -34,6 +35,7 @@ + diff --git a/doc/release/notes/attributeChanges.rst b/doc/release/notes/attributeChanges.rst index d8c1faf30..d7b6f8385 100644 --- a/doc/release/notes/attributeChanges.rst +++ b/doc/release/notes/attributeChanges.rst @@ -24,3 +24,7 @@ Attribute's units to Celsius. The rules for assigning local units to an Attribute that override those inherited through its definition are the same as the case of assigning a value with units to a ValueItem. + +Derived Definitions inherit units associated with their base Definition. When overriding the units being inherited, by default a Definition's units must be compatible with the units coming from its base Definition, though the method provides an option to force units to be set even if they are not compatible. + +Definitions whose units are "*" indicate that Definitions derived and Attributes that are create from can be assigned any supported units. diff --git a/doc/userguide/attribute/concepts.rst b/doc/userguide/attribute/concepts.rst index 0205e6735..98c8ff7a9 100644 --- a/doc/userguide/attribute/concepts.rst +++ b/doc/userguide/attribute/concepts.rst @@ -274,7 +274,7 @@ Custom Relevance for Items SMTK now supports the ability to assign a function to an attribute item that would be used to determine if an item is currently relevant. For example, it is now possible to have the relevance of one item depend on the value of another. -**Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call ``Item::setCustomIsRelevant()`` each time the item is loaded or created. +**Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call :smtk:`Item::setCustomIsRelevant()` each time the item is loaded or created. Custom Relevance for Enumerated Values ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -282,21 +282,21 @@ Custom Relevance for Enumerated Values SMTK now supports the ability to assign a function to a value item's definition (consisting of discrete enumerated values). Similar to relevance function for item, this function would be used to determine if an item's enum value is currently relevant. For example, it is now possible to restrict the possible relevant values an item can based on the value of another item. -**Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call ``Item::setCustomIsRelevant()`` each time the item is loaded or created. +**Note** that no effort is made to serialize or deserialize the provided function; your application that uses SMTK must call :smtk:`Item::setCustomIsRelevant()` each time the item is loaded or created. Related API ~~~~~~~~~~~ -* ``Attribute::isRelevant`` - method to call to see if an attribute is relevant -* ``Item::isRelevant`` - method to call to see if an item is relevant -* ``Item::defaultIsRelevant`` - default relevance method used by ``Attribute::isRelevant`` when no custom relevance function has been specified -* ``Item::setCustomIsRelevant`` - method used to set a custom relevance function -* ``Item::customIsRelevant - method used to return the custom relevance function if one has been set -* ``ValueItem::relevantEnums`` - returns a list of enum strings representing the current relevant discrete values -* ``ValueItemDefinition::relevantEnums`` - the method called by ``ValueItem::relevantEnums`` when no custom relevance function has been specified -* ``ValueItemDefinition::defaultIsEnumRelevant`` - the default relevance method for enum values based solely on categories and read access level -* ``ValueItemDefinition::setCustomEnumIsRelevant - method used to set a custom enum relevance function -* ``ValueItemDefinition::customEnumIsRelevant - method used to return the custom enum relevance function if one has been set +* :smtk:`Attribute::isRelevant` - method to call to see if an attribute is relevant +* :smtk:`Item::isRelevant` - method to call to see if an item is relevant +* :smtk:`Item::defaultIsRelevant` - default relevance method used by :smtk:`Attribute::isRelevant` when no custom relevance function has been specified +* :smtk:`Item::setCustomIsRelevant` - method used to set a custom relevance function +* :smtk:`Item::customIsRelevant` - method used to return the custom relevance function if one has been set +* :smtk:`ValueItem::relevantEnums` - returns a list of enum strings representing the current relevant discrete values +* :smtk:`ValueItemDefinition::relevantEnums` - the method called by :smtk:`ValueItem::relevantEnums` when no custom relevance function has been specified +* :smtk:`ValueItemDefinition::defaultIsEnumRelevant` - the default relevance method for enum values based solely on categories and read access level +* :smtk:`ValueItemDefinition::setCustomEnumIsRelevant` - method used to set a custom enum relevance function +* :smtk:`ValueItemDefinition::customEnumIsRelevant` - method used to return the custom enum relevance function if one has been set Please see `customIsRelevantTest `_ for an example of how to use this functionality. @@ -312,6 +312,9 @@ user-interface components accept values from users in any compatible units. This way, when you export a simulation attribute, all the unit conversions are automatically performed by SMTK for you. +Units and Value Items +~~~~~~~~~~~~~~~~~~~~~ + The value-based Items (DoubleItem, StringItem, IntegerItem, etc..) can have their *native* units (the units that the Item returns its value in) defined in their :smtk:`ItemDefinition `. In the @@ -324,7 +327,36 @@ For example, a DoubleItem **foo** has its units specify by its Definition as "me Changing an Item's explicit units can affect its current values. For example if a DoubleItem **bar** is explicitly assigned "cm" as its units and its value is set to "100 m", its return value will be 10000 (cm). If its explicit units are then changed to "meters", its return value is now 100 (meters). However, if an Item is assigned new units that not compatible to its input values, the input values are replaced with the new units. For example, it we were to later change **bar**'s units to "K" (Kelvin), it return value will be 100 (Kelvin). -Please see `unitDoubleItem `_ for a simple example of using units. +Please see `unitDoubleItem `_ for a simple example of using units with Items. + +Units and Attributes +~~~~~~~~~~~~~~~~~~~~ + +There are times when the attribute itself represents information that has units associated with it. For example, an attribute could represent a temperature field that will be computed by a solver and the user needs to indicate the units they would like the field calculated in. In this case there are no *values* but just the units themselves. Attribute definitions can now have units set on them. These units are inherited by definitions that are derived from it as well as the attributes that produced from it. A derived definition can override these units, through by default as in the case of value items, the derived definition's units must be compatible with its base definition units. So for example if the base definition's units are meters, you could set the derived definition's units to miles but not to feet/sec. You can however explicitly force a unit change. + +Attributes inherit units from the definition they are based on and as with derived , can have different units locally set on them. However, in this case the new units must be compatible with those of its definition (there is now way of forcing incompatible units). + +Definitions with Units "*" +++++++++++++++++++++++++++ + +There are cases where attributes coming from the same definition need to have different/incompatible units. For example, consider associating units on an attribute representing an expression. The definition may not want to restrict what units its attributes can have. +In this case, workflow designers can set the definition's units to be "*" indicating any units are allowed. Note that this also pertains to any definition derived from it. + +An attribute whose definition has "*" units will by default be unit-less (meaning that the units it returns are the empty string). The only restriction in terms of setting the attribute's local units is that the new units must either be one supported by the units system or an empty string meaning that the attribute is unit-less. + +Related API +~~~~~~~~~~~ + +* :smtk:`Attribute::units` - method to return the units associated with the attribute (either locally set or inherited from its definition) +* :smtk:`Attribute::localUnits` - method to return the local units explicitly associated with the attribute +* :smtk:`Attribute::setLocalUnits` - method to set the local units explicitly associated with the attribute +* :smtk:`Attribute::supportUnits` - method to used to determine if the attribute supports units +* :smtk:`Definition::units` - method to return the units associated with the definition (either locally set or inherited from its derived definition) +* :smtk:`Definition::localUnits` - method to return the local units explicitly associated with the definition +* :smtk:`Definition::setLocalUnits` - method to set the units explicitly associated with the definition +* :smtk:`Definition::unitsSystem` - method to return the units system associated with the definition + +Please see `unitAttributeUnits `_ for a simple example of using units with Attributes and Definitions. Migration to newer schema diff --git a/smtk/attribute/Attribute.cxx b/smtk/attribute/Attribute.cxx index e686e44b3..ca16efabf 100644 --- a/smtk/attribute/Attribute.cxx +++ b/smtk/attribute/Attribute.cxx @@ -1182,7 +1182,12 @@ bool Attribute::assign( const std::string& Attribute::units() const { - if (!(m_localUnits.empty() && m_definition)) + // Return the attribute's local units if any of the following is true: + // 1. They have been set + // 2. The attribute has no definition + // 3. The attribute's definition's units are * which means the attribute must have its + // units set locally but can be set to any valid unit + if ((!(m_localUnits.empty() && m_definition)) || (m_definition->units() == "*")) { return m_localUnits; } @@ -1190,6 +1195,11 @@ const std::string& Attribute::units() const return m_definition->units(); } +bool Attribute::supportsUnits() const +{ + return (m_definition && !m_definition->units().empty()); +} + bool Attribute::setLocalUnits(const std::string& newUnits) { const std::string& currentUnits = this->units(); @@ -1198,7 +1208,7 @@ bool Attribute::setLocalUnits(const std::string& newUnits) return true; } - if (currentUnits.empty() || !m_definition) + if (!(this->supportsUnits() && m_definition)) { return false; } @@ -1210,14 +1220,16 @@ bool Attribute::setLocalUnits(const std::string& newUnits) return false; } - // Can we convert from the current units to the proposed new units? - bool unitsSupported; - auto cUnits = unitSys->unit(currentUnits, &unitsSupported); - if (!unitsSupported) + // If the definitions units = "*" then the attribute can be unit-less (empty string) + if (newUnits.empty() && m_definition->units() == "*") { - // the current units are not supported by the units system - return false; + m_localUnits = newUnits; + return true; } + + bool unitsSupported; + + // Are the requested units supported by the units system auto nUnits = unitSys->unit(newUnits, &unitsSupported); if (!unitsSupported) { @@ -1225,6 +1237,22 @@ bool Attribute::setLocalUnits(const std::string& newUnits) return false; } + // If the definitions units = "*", which means that the attribute's + // definition can support any units, then as long as the requested units + // are supported we will accept them. + if (m_definition->units() == "*") + { + m_localUnits = newUnits; + return true; + } + + // Can we convert from the current units to the proposed new units? + auto cUnits = unitSys->unit(currentUnits, &unitsSupported); + if (!unitsSupported) + { + // the current units are not supported by the units system + return false; + } if (unitSys->convert(cUnits, nUnits)) { m_localUnits = newUnits; diff --git a/smtk/attribute/Attribute.h b/smtk/attribute/Attribute.h index d6102b7ff..9c9154c69 100644 --- a/smtk/attribute/Attribute.h +++ b/smtk/attribute/Attribute.h @@ -339,6 +339,12 @@ class SMTKCORE_EXPORT Attribute : public resource::Component /// a category will be a simulation type like heat transfer, fluid flow, etc. const smtk::attribute::Categories& categories() const; + ///@{ + ///\brief Returns true if the attribute is valid. + /// + /// An irrelevant attribute is considered valid since for all intents and purposes it is treated + /// as though it does not exist. A relevant attribute if valid if all of its relevant children items + /// are valid bool isValid(bool useActiveCategories = true) const; bool isValid(const std::set& categories) const; @@ -396,29 +402,37 @@ class SMTKCORE_EXPORT Attribute : public resource::Component // registered with an Evaluator, else returns nullptr. std::unique_ptr createEvaluator() const; - /// @{ - ///\brief Set/Get the units string associated with the attribute + ///\brief Return the units associated to the attribute + /// + /// Returns the units that have been locally set on the attribute, else it will return those + /// returned from its definition's units method /// /// This means that the information that the attributes represents /// conceptually has units but unlike attribute Items, does not have a numerical value /// associated with it. For example an attribute may be used to indicate that a numerical /// field being output by a simulation represents a temperature whose units should be in Kelvin. /// - ///\brief Return the units associated to the attribute - /// - /// Returns the units that have been explicitly on the attribute, else it will return those - /// set on its definition + /// If the attribute has no local units set and its definition's units method returns "*", then + /// this will return an empty string indicating that the attribute is currently unit-less const std::string& units() const; - ///\brief Return the units explicitly set on the attribute + + ///\brief Return the units locally set on the attribute const std::string& localUnits() const { return m_localUnits; } - ///\brief Explicitly sets the units on the attribute + + ///\brief Locally set the units on the attribute /// /// This will fail and return false under the following circumstances: /// 1. There are no units associated with its definition and the newUnits string is not empty /// 2. There is no units system associated with its definition and newUnits is not the same as those on the definition - /// 3. There is no way to convert between the units associated with its definition and newUnits + /// 3. newUnits are not supported by the associated units system + /// 4. Its definition's units are not "*" or there is no way to convert between the units associated with its definition + /// and newUnits bool setLocalUnits(const std::string& newUnits); - /// @} + + ///\brief Returns true if the attribute supports units. + /// + /// An attribute supports units if its definition's units() does not return an empty string. + bool supportsUnits() const; class GuardedLinks { diff --git a/smtk/attribute/Definition.cxx b/smtk/attribute/Definition.cxx index 4b936c193..93136f337 100644 --- a/smtk/attribute/Definition.cxx +++ b/smtk/attribute/Definition.cxx @@ -19,6 +19,9 @@ #include "smtk/io/Logger.h" +#include "units/Converter.h" +#include "units/System.h" + #include #include #include @@ -920,8 +923,62 @@ const std::shared_ptr& Definition::unitsSystem() const return nullUnitsSystem; } -bool Definition::setUnits(const std::string& newUnits) +bool Definition::setLocalUnits(const std::string& newUnits, bool force) { - m_units = newUnits; - return true; + const auto& currentUnits = this->units(); + if (currentUnits.empty() || force) + { + m_localUnits = newUnits; + return true; + } + const auto& unitSys = this->unitsSystem(); + if (!unitSys) + { + return false; // There is no unit system + } + + // Are the requested units supported by the units system + bool unitsSupported; + auto nUnits = unitSys->unit(newUnits, &unitsSupported); + if (!unitsSupported) + { + // the new units are not supported by the units system + return false; + } + + if (currentUnits == "*") + { + // This definition can accept any support units + m_localUnits = newUnits; + return true; + } + + // Can we convert from the current units to the proposed new units? + auto cUnits = unitSys->unit(currentUnits, &unitsSupported); + if (!unitsSupported) + { + // the current units are not supported by the units system so + // there is no conversion + return false; + } + + if (unitSys->convert(cUnits, nUnits)) + { + m_localUnits = newUnits; + return true; + } + // No known conversion + return false; +} + +const std::string& Definition::units() const +{ + // If it has units set, return them. Else if there is + // a base definition return those + if (m_localUnits.empty() && m_baseDefinition) + { + return m_baseDefinition->units(); + } + + return m_localUnits; } diff --git a/smtk/attribute/Definition.h b/smtk/attribute/Definition.h index 49f046b7e..46e9f5d18 100644 --- a/smtk/attribute/Definition.h +++ b/smtk/attribute/Definition.h @@ -448,16 +448,32 @@ class SMTKCORE_EXPORT Definition : public resource::Component bool ignoreCategories() const { return m_ignoreCategories; } void setIgnoreCategories(bool val) { m_ignoreCategories = val; } - /// @{ - ///\brief Set/Get the units string associated with the definition + ///\brief Return the units associated to the definition /// - /// This means that the information that the definition's attributes represents + /// Returns the units that have been locally set on the definition, else if the definition has + /// a base definition, then its base definition's units are returned. + /// + /// This means that the information that the attributes, generated from the definition, represents /// conceptually has units but unlike attribute Items, does not have a numerical value /// associated with it. For example an attribute may be used to indicate that a numerical - /// field being output by a simulation represents a temperature whose units should be in Kelvin - const std::string& units() const { return m_units; } - bool setUnits(const std::string& newUnits); - /// @} + /// field being output by a simulation represents a temperature whose units should be in Kelvin. + /// + /// **Note** A definition's units method returning "*" indicates that its attributes or derived definitions can be assigned + /// any supported units + const std::string& units() const; + + ///\brief Return the units locally set on the definition + const std::string& localUnits() const { return m_localUnits; } + + ///\brief Locally set the units on the definition + /// + /// This will fail and return false under the following circumstances: + /// 1. There is no units system associated with the definition, the definition has a base definition whose units() are + /// not empty, and newUnits is not the same as those returned from its base definition + /// 2. newUnits are not supported by the associated units system + /// 3. Its base definition's units are not "*" or there is no way to convert between the units associated with its base definition + /// and newUnits + bool setLocalUnits(const std::string& newUnits, bool force = false); /// \brief Gets the system of units used by this definition. const std::shared_ptr& unitsSystem() const; @@ -528,7 +544,7 @@ class SMTKCORE_EXPORT Definition : public resource::Component Tags m_tags; std::size_t m_includeIndex; Categories::CombinationMode m_combinationMode; - std::string m_units; + std::string m_localUnits; private: /// These colors are returned for base definitions w/o set colors diff --git a/smtk/attribute/ReferenceItem.cxx b/smtk/attribute/ReferenceItem.cxx index 4a1e6a2d7..d3ead6dc1 100644 --- a/smtk/attribute/ReferenceItem.cxx +++ b/smtk/attribute/ReferenceItem.cxx @@ -999,11 +999,14 @@ Item::Status ReferenceItem::assign( else { result.markFailed(); + auto def = smtk::dynamic_pointer_cast(this->definition()); smtkErrorMacro( logger, "Could not assign PersistentObject:" << val->name() << " of type: " << val->typeName() << " to ReferenceItem: " - << sourceItem->name() << " and allowPartialValues options was not specified."); + << sourceItem->name() << " and allowPartialValues options was not specified." + << " Reference Criteria:\n" + << def->criteriaAsString()); return result; } } diff --git a/smtk/attribute/Resource.cxx b/smtk/attribute/Resource.cxx index ad337c1c4..db7827dcf 100644 --- a/smtk/attribute/Resource.cxx +++ b/smtk/attribute/Resource.cxx @@ -781,7 +781,7 @@ bool Resource::copyDefinitionImpl( newDef->setLabel(sourceDef->label()); newDef->setVersion(sourceDef->version()); newDef->setIsAbstract(sourceDef->isAbstract()); - newDef->setUnits(sourceDef->units()); + newDef->setLocalUnits(sourceDef->localUnits()); if (sourceDef->hasLocalAdvanceLevelInfo(0)) { newDef->setLocalAdvanceLevel(0, sourceDef->localAdvanceLevel(0)); diff --git a/smtk/attribute/json/jsonDefinition.cxx b/smtk/attribute/json/jsonDefinition.cxx index e41f43159..45db2ceb8 100644 --- a/smtk/attribute/json/jsonDefinition.cxx +++ b/smtk/attribute/json/jsonDefinition.cxx @@ -80,10 +80,10 @@ SMTKCORE_EXPORT void to_json(nlohmann::json& j, const smtk::attribute::Definitio j["Nodal"] = true; } - // Does the Definition have units? - if (!defPtr->units().empty()) + // Does the Definition have local units? + if (!defPtr->localUnits().empty()) { - j["Units"] = defPtr->units(); + j["Units"] = defPtr->localUnits(); } // Process Local Category Expression @@ -263,7 +263,7 @@ SMTKCORE_EXPORT void from_json( result = j.find("Units"); if (result != j.end()) { - defPtr->setUnits(*result); + defPtr->setLocalUnits(*result); } // Process Category Info () diff --git a/smtk/attribute/pybind11/PybindAttribute.h b/smtk/attribute/pybind11/PybindAttribute.h index 3a87b9359..8f85b2241 100644 --- a/smtk/attribute/pybind11/PybindAttribute.h +++ b/smtk/attribute/pybind11/PybindAttribute.h @@ -139,6 +139,10 @@ inline PySharedPtrClass< smtk::attribute::Attribute > pybind11_init_smtk_attribu .def("setlocalAdvanceLevel", &smtk::attribute::Attribute::setLocalAdvanceLevel, py::arg("mode"), py::arg("level")) .def("unsetLocalAdvanceLevel", &smtk::attribute::Attribute::unsetLocalAdvanceLevel, py::arg("mode") = 0) .def("hasLocalAdvanceLevelInfo", &smtk::attribute::Attribute::hasLocalAdvanceLevelInfo, py::arg("mode") = 0) + .def("units", &smtk::attribute::Attribute::units) + .def("localUnits", &smtk::attribute::Attribute::localUnits) + .def("setLocalUnits", &smtk::attribute::Attribute::setLocalUnits, py::arg("newUnits")) + .def("supportsUnits", &smtk::attribute::Attribute::supportsUnits) .def_static("CastTo", [](const std::shared_ptr i) { return std::dynamic_pointer_cast(i); }) diff --git a/smtk/attribute/pybind11/PybindDefinition.h b/smtk/attribute/pybind11/PybindDefinition.h index 24fc02f14..e7127a2ea 100644 --- a/smtk/attribute/pybind11/PybindDefinition.h +++ b/smtk/attribute/pybind11/PybindDefinition.h @@ -104,7 +104,8 @@ inline PySharedPtrClass< smtk::attribute::Definition > pybind11_init_smtk_attrib .def("removeTag", &smtk::attribute::Definition::removeTag) .def("ignoreCategories", &smtk::attribute::Definition::ignoreCategories) .def("setIgnoreCategories", &smtk::attribute::Definition::setIgnoreCategories, py::arg("val")) - .def("setUnits", &smtk::attribute::Definition::setUnits, py::arg("newUnits")) + .def("setLocalUnits", &smtk::attribute::Definition::setLocalUnits, py::arg("newUnits"), py::arg("force") = true) + .def("localUnits", &smtk::attribute::Definition::localUnits) .def("units", &smtk::attribute::Definition::units) ; return instance; diff --git a/smtk/attribute/testing/cxx/unitAttributeUnits.cxx b/smtk/attribute/testing/cxx/unitAttributeUnits.cxx index 5dfc06763..8cd65b115 100644 --- a/smtk/attribute/testing/cxx/unitAttributeUnits.cxx +++ b/smtk/attribute/testing/cxx/unitAttributeUnits.cxx @@ -36,6 +36,7 @@ bool testAttributeUnits(const attribute::ResourcePtr& attRes, const std::string& AttributePtr b = attRes->findAttribute("b"); AttributePtr b1 = attRes->findAttribute("b1"); AttributePtr c = attRes->findAttribute("c"); + AttributePtr d = attRes->findAttribute("d"); if (!a->definition()->units().empty()) { @@ -75,6 +76,31 @@ bool testAttributeUnits(const attribute::ResourcePtr& attRes, const std::string& std::cerr << prefix << "Attribute c has units: " << c->units() << " but should be in foo!\n"; status = false; } + if (!d->units().empty()) + { + std::cerr << prefix << "Attribute d has units: " << d->units() << " but should be empty!\n"; + status = false; + } + if (!d->setLocalUnits("K")) + { + std::cerr << prefix << "Could not set attribute d's units to K\n"; + status = false; + } + if (!d->setLocalUnits("miles")) + { + std::cerr << prefix << "Could not set attribute d's units to miles\n"; + status = false; + } + if (d->setLocalUnits("foo")) + { + std::cerr << prefix << "Could set attribute d's units to foo which should not be allowed\n"; + status = false; + } + if (!d->setLocalUnits("")) + { + std::cerr << prefix << "Could not set attribute d to be unit-less\n"; + status = false; + } return status; } @@ -86,20 +112,82 @@ int unitAttributeUnits(int /*unused*/, char* /*unused*/[]) // I. Let's create an attribute resource and some definitions and attributes attribute::ResourcePtr attRes = attribute::Resource::create(); + std::cerr << "Definition Units Tests\n"; // A will not have any units specified DefinitionPtr A = attRes->createDefinition("A"); // B will have a unit of length (meters) DefinitionPtr B = attRes->createDefinition("B"); - B->setUnits("meters"); + B->setLocalUnits("meters"); + + // B1 will derive from B + DefinitionPtr B1 = attRes->createDefinition("B1", B); + // By default B1 should have meters for units + if (B1->units() != "meters") + { + std::cerr << "B1 inherited: " << B1->units() << " - should have been meters\n"; + status = -1; + } + // We should be able to set B1's units to miles + if (!B1->setLocalUnits("miles")) + { + std::cerr << "Was not able to set B1's units to miles\n"; + status = -1; + } + else if (B1->units() != "miles") + { + std::cerr << "B1 units: " << B1->units() << " - should have been miles\n"; + status = -1; + } + // We should not be able to set B1's units to K (Kelvin) + if (B1->setLocalUnits("K")) + { + std::cerr << "Was able to set B1's units to K\n"; + status = -1; + } + // C will have non supported units (foo) DefinitionPtr C = attRes->createDefinition("C"); - C->setUnits("foo"); + C->setLocalUnits("foo"); + + // D will have support all units + DefinitionPtr D = attRes->createDefinition("D"); + D->setLocalUnits("*"); + DefinitionPtr D1 = attRes->createDefinition("D1", D); + // We should be able to set D1 to miles + if (!D1->setLocalUnits("miles")) + { + std::cerr << "Was not able to set D1's units to miles\n"; + status = -1; + } + // We should be able to set D1 to meters + if (!D1->setLocalUnits("meters")) + { + std::cerr << "Was not able to set D1's units to meters\n"; + status = -1; + } + // Now that there are explicit units on D1 we should be able + // to set them to something that is not compatible without forcing it + if (D1->setLocalUnits("K")) + { + std::cerr << "Was able to set D1's units to K\n"; + status = -1; + } + else if (!D1->setLocalUnits("K", true)) + { + std::cerr << "Was not able to force setting D1's units to K\n"; + status = -1; + } + if (status == 0) + { + std::cerr << "All Definition Unit Tests Passed!\n"; + } // Lets create some attributes auto a = attRes->createAttribute("a", "A"); auto b = attRes->createAttribute("b", "B"); auto b1 = attRes->createAttribute("b1", "B"); auto c = attRes->createAttribute("c", "C"); + auto d = attRes->createAttribute("d", "D"); // Lets do some unit assignments // Should not be able to assign any units to an attribute whose diff --git a/smtk/extension/qt/qtAttribute.cxx b/smtk/extension/qt/qtAttribute.cxx index 4c716eb05..95ff5db08 100644 --- a/smtk/extension/qt/qtAttribute.cxx +++ b/smtk/extension/qt/qtAttribute.cxx @@ -33,7 +33,6 @@ #include #include #include -#include #include #include // for atexit() @@ -141,48 +140,54 @@ void qtAttribute::createWidget(bool createWidgetWhenEmpty) m_isEmpty = true; auto att = this->attribute(); - if ( - (att == nullptr) || - ((att->numberOfItems() == 0) && (att->associations() == nullptr) && - (att->units().empty() || (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None)))) + if (!createWidgetWhenEmpty) { - return; - } + if ( + (att == nullptr) || + ((att->numberOfItems() == 0) && (att->associations() == nullptr) && + ((!att->supportsUnits()) || + (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None)))) + { + return; + } - // Based on the View, see if the attribute should be displayed - if (!m_internals->m_view->displayAttribute(att)) - { - return; - } + // Based on the View, see if the attribute should be displayed + if (!m_internals->m_view->displayAttribute(att)) + { + return; + } - // We will always display units if there are any associated with the attribute and we were told to display them else - // we need to see if any items would be displayed - if (att->units().empty() || (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None)) - { - int numShowItems = 0; - std::size_t i, n = att->numberOfItems(); - if (m_internals->m_view) + // We will always display units if there are any associated with the attribute and we were told to display them else + // we need to see if any items would be displayed + if ( + (!att->supportsUnits()) || + (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None)) { - for (i = 0; i < n; i++) + int numShowItems = 0; + std::size_t i, n = att->numberOfItems(); + if (m_internals->m_view) { - if (m_internals->m_view->displayItem(att->item(static_cast(i)))) + for (i = 0; i < n; i++) + { + if (m_internals->m_view->displayItem(att->item(static_cast(i)))) + { + numShowItems++; + } + } + // also check associations + if (m_internals->m_view->displayItem(att->associations())) { numShowItems++; } } - // also check associations - if (m_internals->m_view->displayItem(att->associations())) + else // show everything { - numShowItems++; + numShowItems = static_cast(att->associations() ? n + 1 : n); + } + if (numShowItems == 0) + { + return; } - } - else // show everything - { - numShowItems = static_cast(att->associations() ? n + 1 : n); - } - if ((numShowItems == 0) && !createWidgetWhenEmpty) - { - return; } } @@ -240,9 +245,9 @@ void qtAttribute::createBasicLayout(bool includeAssociations) qtItem* qItem = nullptr; smtk::attribute::AttributePtr att = this->attribute(); auto* uiManager = m_internals->m_view->uiManager(); - // If the attribute has units - display them - if (!(att->units().empty() || - (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::None))) + + // If the attribute has units and we were told not to ignore them then display them + if (att->supportsUnits() && (m_internals->m_unitsMode != qtAttributeInternals::UnitsMode::None)) { QFrame* unitFrame = new QFrame(this->m_widget); std::string unitFrameName = att->name() + "UnitsFrame"; @@ -256,25 +261,27 @@ void qtAttribute::createBasicLayout(bool includeAssociations) unitFrame->setLayout(unitsLayout); std::string unitsLabel = att->name() + "'s units:"; + // Are we suppose to allow editing? if (m_internals->m_unitsMode == qtAttributeInternals::UnitsMode::Editable) { QLabel* uLabel = new QLabel(unitsLabel.c_str(), unitFrame); unitsLayout->addWidget(uLabel); - auto* unitsWidget = new qtUnitsLineEdit( - att->definition()->units().c_str(), att->definition()->unitsSystem(), uiManager, unitFrame); + + // If the attribute's definition has explicit units use them else use those set explicitly on the attribute + QString baseUnits = (att->definition()->units() == "*") ? att->units().c_str() + : att->definition()->units().c_str(); + auto* unitsWidget = + new qtUnitsLineEdit(baseUnits, att->definition()->unitsSystem(), uiManager, unitFrame); std::string unitsWidgetName = att->name() + "UnitsLineWidget"; unitsWidget->setObjectName(unitsWidgetName.c_str()); unitsLayout->addWidget(unitsWidget); unitsWidget->setAndClassifyText(att->units().c_str()); // Create a regular expression validator to prevent leading spaces - unitsWidget->setValidator( - new QRegularExpressionValidator(QRegularExpression("^\\S*$"), this)); QObject::connect( unitsWidget, &qtUnitsLineEdit::editingCompleted, this, &qtAttribute::onUnitsChanged); } - else + else // Units are to be displayed read only { - // if we are here, units are to be displayed as read only so just add the units to the label unitsLabel.append(att->units()); QLabel* uLabel = new QLabel(unitsLabel.c_str(), unitFrame); unitsLayout->addWidget(uLabel); diff --git a/smtk/extension/qt/qtInstancedView.cxx b/smtk/extension/qt/qtInstancedView.cxx index b214308e8..4fb7954f1 100644 --- a/smtk/extension/qt/qtInstancedView.cxx +++ b/smtk/extension/qt/qtInstancedView.cxx @@ -265,7 +265,7 @@ void qtInstancedView::updateUI() this->Internals->m_isEmpty = true; for (i = 0; i < n; i++) { - if ((atts[i]->numberOfItems() > 0) || !atts[i]->units().empty()) + if ((atts[i]->numberOfItems() > 0) || atts[i]->supportsUnits()) { qtAttribute* attInstance = new qtAttribute(atts[i], comps[i], this->widget(), this); if (attInstance) diff --git a/smtk/extension/qt/qtUnitsLineEdit.cxx b/smtk/extension/qt/qtUnitsLineEdit.cxx index fa6e96767..57736d036 100644 --- a/smtk/extension/qt/qtUnitsLineEdit.cxx +++ b/smtk/extension/qt/qtUnitsLineEdit.cxx @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -76,24 +77,17 @@ qtUnitsLineEdit::qtUnitsLineEdit( , m_uiManager(uiManager) { - // Parsing the Item's unit string - bool parsedOK = false; - auto unit = m_unitSys->unit(m_baseUnit.toStdString(), &parsedOK); - - // If the units are supported then setup a completer - if (parsedOK) + QStringList unitChoices; + std::shared_ptr preferred; + auto cit = m_unitSys->m_unitContexts.find(m_unitSys->m_activeUnitContext); + if (cit != m_unitSys->m_unitContexts.end()) { + preferred = cit->second; + } - std::shared_ptr preferred; - auto cit = m_unitSys->m_unitContexts.find(m_unitSys->m_activeUnitContext); - if (cit != m_unitSys->m_unitContexts.end()) - { - preferred = cit->second; - } - - // Get the completer strings - QStringList unitChoices; - + // if the base unit is empty then all supported units are allowed + if (m_baseUnit.isEmpty()) + { // Create a list of possible units names: if (preferred) { @@ -102,7 +96,7 @@ qtUnitsLineEdit::qtUnitsLineEdit( // position 0 of the suggestions. units::CompatibleUnitOptions opts; opts.m_inputUnitPriority = 0; - for (const auto& suggestion : preferred->suggestedUnits(m_baseUnit.toStdString(), opts)) + for (const auto& suggestion : preferred->suggestedUnits("*", opts)) { unitChoices.push_back(suggestion.c_str()); } @@ -114,10 +108,10 @@ qtUnitsLineEdit::qtUnitsLineEdit( // duplicates and is definitely not sorted. // Get list of compatible units - auto compatibleUnits = m_unitSys->compatibleUnits(unit); - for (const auto& unit : compatibleUnits) + auto compatibleUnits = m_unitSys->allUnits(); + for (const auto& unitName : compatibleUnits) { - unitChoices.push_back(unit.name().c_str()); + unitChoices.push_back(unitName.c_str()); } // Lets remove duplicates and sort the list unitChoices.removeDuplicates(); @@ -125,7 +119,53 @@ qtUnitsLineEdit::qtUnitsLineEdit( // Now make the Item's units appear at the top //unitChoices.push_front(dUnits.c_str()); } + } + else + { + // Parsing the unit string + bool parsedOK = false; + auto unit = m_unitSys->unit(m_baseUnit.toStdString(), &parsedOK); + + // If the units are supported then setup a completer + if (parsedOK) + { + + // Create a list of possible units names: + if (preferred) + { + // We have a context; this provides preferred units as a + // developer-ordered list of units, placing dUnits at + // position 0 of the suggestions. + units::CompatibleUnitOptions opts; + opts.m_inputUnitPriority = 0; + for (const auto& suggestion : preferred->suggestedUnits(m_baseUnit.toStdString(), opts)) + { + unitChoices.push_back(suggestion.c_str()); + } + } + else + { + // We don't have a unit-system context; just + // find compatible units. This may present + // duplicates and is definitely not sorted. + // Get list of compatible units + auto compatibleUnits = m_unitSys->compatibleUnits(unit); + for (const auto& unit : compatibleUnits) + { + unitChoices.push_back(unit.name().c_str()); + } + // Lets remove duplicates and sort the list + unitChoices.removeDuplicates(); + unitChoices.sort(); + // Now make the Item's units appear at the top + //unitChoices.push_front(dUnits.c_str()); + } + } + } + + if (!unitChoices.empty()) + { auto* model = new qtCompleterStringModel(this); model->setStringList(unitChoices); m_completer = new QCompleter(model, parent); @@ -136,6 +176,9 @@ qtUnitsLineEdit::qtUnitsLineEdit( // Connect up the signals QObject::connect(this, &QLineEdit::textEdited, this, &qtUnitsLineEdit::onTextEdited); QObject::connect(this, &QLineEdit::editingFinished, this, &qtUnitsLineEdit::onEditFinished); + + // Prevent leading spaces + this->setValidator(new QRegularExpressionValidator(QRegularExpression("^\\S*$"), this)); } bool qtUnitsLineEdit::isCurrentTextValid() const @@ -143,13 +186,15 @@ bool qtUnitsLineEdit::isCurrentTextValid() const auto utext = this->text().toStdString(); // Remove all surrounding white space smtk::common::StringUtil::trim(utext); + + // Empty strings are only allowed if the baseUnit is also empty + // which indicates that any supported units as well as unit-less + // values are permitted if (utext.empty()) { - return false; + return m_baseUnit.isEmpty(); } bool parsedOK = false; - auto baseU = m_unitSys->unit(m_baseUnit.toStdString(), &parsedOK); - // Is the current value supported by the unit system? parsedOK = false; auto newU = m_unitSys->unit(utext, &parsedOK); @@ -157,7 +202,14 @@ bool qtUnitsLineEdit::isCurrentTextValid() const { return false; } + + if (m_baseUnit.isEmpty()) + { + return true; + } + // Can you convert between the units? + auto baseU = m_unitSys->unit(m_baseUnit.toStdString(), &parsedOK); return (m_unitSys->convert(baseU, newU) != nullptr); } diff --git a/smtk/io/XmlDocV8Parser.cxx b/smtk/io/XmlDocV8Parser.cxx index d8d1a3fcb..849ebf7e2 100644 --- a/smtk/io/XmlDocV8Parser.cxx +++ b/smtk/io/XmlDocV8Parser.cxx @@ -166,7 +166,7 @@ void XmlDocV8Parser::processDefinitionAtts(xml_node& defNode, smtk::attribute::D pugi::xml_attribute xatt = defNode.attribute("Units"); if (xatt) { - def->setUnits(xatt.value()); + def->setLocalUnits(xatt.value()); } this->XmlDocV7Parser::processDefinitionAtts(defNode, def); diff --git a/smtk/io/XmlV8StringWriter.cxx b/smtk/io/XmlV8StringWriter.cxx index d80173d29..d3ee57c80 100644 --- a/smtk/io/XmlV8StringWriter.cxx +++ b/smtk/io/XmlV8StringWriter.cxx @@ -48,9 +48,9 @@ void XmlV8StringWriter::processDefinitionInternal( { XmlV7StringWriter::processDefinitionInternal(definition, def); - if (!def->units().empty()) + if (!def->localUnits().empty()) { - definition.append_attribute("Units").set_value(def->units().c_str()); + definition.append_attribute("Units").set_value(def->localUnits().c_str()); } // Add its ID definition.append_attribute("ID").set_value(def->id().toString().c_str()); diff --git a/smtk/project/testing/cxx/TestProjectPortability.cxx b/smtk/project/testing/cxx/TestProjectPortability.cxx index e43f52d83..b902d9d7d 100644 --- a/smtk/project/testing/cxx/TestProjectPortability.cxx +++ b/smtk/project/testing/cxx/TestProjectPortability.cxx @@ -371,7 +371,7 @@ int TestProjectPortability(int /*unused*/, char** const /*unused*/) { std::vector defList; myAtts->definitions(defList); - if (defList.size() != 2) + if (defList.size() != 3) { std::cerr << "Attribute resource missing definitions\n"; return 1; diff --git a/smtk/project/testing/cxx/TestProjectReadWrite2.cxx b/smtk/project/testing/cxx/TestProjectReadWrite2.cxx index 50355ce2e..aab2a7ae0 100644 --- a/smtk/project/testing/cxx/TestProjectReadWrite2.cxx +++ b/smtk/project/testing/cxx/TestProjectReadWrite2.cxx @@ -314,7 +314,7 @@ int TestProjectReadWrite2(int /*unused*/, char** const /*unused*/) { std::vector defList; myAtts->definitions(defList); - if (defList.size() != 2) + if (defList.size() != 3) { std::cerr << "Attribute resource missing definitions\n"; return 1; From fd29ce69b2e27773664286213427cbf8817fb877 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Tue, 23 Jul 2024 17:00:25 -0400 Subject: [PATCH 34/42] ENH: Handling Expressions with Units ValueItems will now test units when assigning expressions. If the expression has units and they are not convertible to the item's units, the assignment will now fail. Also if a double item's units are different from its expression, the expression's evaluated value is converted to those of the item when calling its value methods. See smtk/attribute/testing/c++/unitInfixExpressionEvaluator and data/attribute/DoubleItemExample.sbt for examples. Also added missing data members in ReferenceItemDefinition that should have been copied in the Item's copy method as well as adding more debug information to help track down the reason for the intermittent failures related to the clone resources test. --- .../DoubleItemExample.sbt | 4 +- doc/release/notes/attributeChanges.rst | 11 ++ doc/userguide/attribute/concepts.rst | 17 ++- smtk/attribute/Definition.h | 1 + smtk/attribute/DoubleItem.cxx | 70 ++++++++++ smtk/attribute/DoubleItem.h | 3 + smtk/attribute/ReferenceItem.cxx | 37 ++++- smtk/attribute/ReferenceItemDefinition.cxx | 5 + smtk/attribute/ValueItem.cxx | 88 ++++++++++-- smtk/attribute/ValueItem.h | 9 +- smtk/attribute/ValueItemTemplate.h | 128 +++++++++++------- smtk/attribute/pybind11/PybindValueItem.h | 1 + .../cxx/unitInfixExpressionEvaluator.cxx | 25 +++- smtk/extension/qt/qtInputsItem.cxx | 10 +- 14 files changed, 329 insertions(+), 80 deletions(-) diff --git a/data/attribute/attribute_collection/DoubleItemExample.sbt b/data/attribute/attribute_collection/DoubleItemExample.sbt index 2fca83f56..94a177aff 100644 --- a/data/attribute/attribute_collection/DoubleItemExample.sbt +++ b/data/attribute/attribute_collection/DoubleItemExample.sbt @@ -4,13 +4,13 @@ - + - + doubleFunc diff --git a/doc/release/notes/attributeChanges.rst b/doc/release/notes/attributeChanges.rst index d7b6f8385..d634d68c5 100644 --- a/doc/release/notes/attributeChanges.rst +++ b/doc/release/notes/attributeChanges.rst @@ -28,3 +28,14 @@ are the same as the case of assigning a value with units to a ValueItem. Derived Definitions inherit units associated with their base Definition. When overriding the units being inherited, by default a Definition's units must be compatible with the units coming from its base Definition, though the method provides an option to force units to be set even if they are not compatible. Definitions whose units are "*" indicate that Definitions derived and Attributes that are create from can be assigned any supported units. + +Expression Attributes and Value Items with Units +------------------------------------------------- + +ValueItems will now test units when assigning expressions. If the expression has units and they +are not convertible to the item's units, the assignment will now fail. + +Also if a double item's units are different from its expression, the expression's +evaluated value is converted to those of the item when calling its value methods. + +See smtk/attribute/testing/c++/unitInfixExpressionEvaluator and data/attribute/DoubleItemExample.sbt for examples. diff --git a/doc/userguide/attribute/concepts.rst b/doc/userguide/attribute/concepts.rst index 98c8ff7a9..fb5e14167 100644 --- a/doc/userguide/attribute/concepts.rst +++ b/doc/userguide/attribute/concepts.rst @@ -332,9 +332,9 @@ Please see `unitDoubleItem `_ for a simple example of using expressions involving units with Items. + + Related API ~~~~~~~~~~~ diff --git a/smtk/attribute/Definition.h b/smtk/attribute/Definition.h index 46e9f5d18..93b00572e 100644 --- a/smtk/attribute/Definition.h +++ b/smtk/attribute/Definition.h @@ -447,6 +447,7 @@ class SMTKCORE_EXPORT Definition : public resource::Component ///@{ bool ignoreCategories() const { return m_ignoreCategories; } void setIgnoreCategories(bool val) { m_ignoreCategories = val; } + ///@} ///\brief Return the units associated to the definition /// diff --git a/smtk/attribute/DoubleItem.cxx b/smtk/attribute/DoubleItem.cxx index fc6ced519..5e36badb0 100644 --- a/smtk/attribute/DoubleItem.cxx +++ b/smtk/attribute/DoubleItem.cxx @@ -498,3 +498,73 @@ bool DoubleItem::setToDefault(std::size_t element) } return true; } + +double DoubleItem::value(std::size_t element, smtk::io::Logger& log) const +{ + if (!this->isSet(element)) + { + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" element " << element << " is not set (attribute \"" + << this->attribute()->name() << "\")."); + return 0.0; + } + + if (isExpression()) + { + double eval = this->evaluateExpression(element, log); + const std::string& exStr = this->expression()->units(); + if (exStr.empty() || (exStr == units())) + { + return eval; // no conversion needed + } + // We need to convert to the units of the item + auto unitsSystem = this->definition()->unitsSystem(); + // Is there a units system specified? + if (!unitsSystem) + { + smtkErrorMacro( + log, + "No units system set for Item \"" << this->name() << "\" element " << element + << " (attribute \"" << this->attribute()->name() + << "\")."); + return 0.0; + } + bool status; + auto myUnits = unitsSystem->unit(this->units(), &status); + if (!status) + { + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" element " << element << " (attribute \"" + << this->attribute()->name() << "\")'s units: " << this->units() + << " are not supported for conversion."); + return 0.0; + } + auto exUnits = unitsSystem->unit(exStr, &status); + if (!status) + { + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" element " << element << " (attribute \"" + << this->attribute()->name() << "\")'s expression's units: " << exStr + << " are not supported for conversion."); + return 0.0; + } + + units::Measurement m(eval, exUnits); + auto newM = unitsSystem->convert(m, myUnits, &status); + if (!status) + { + smtkErrorMacro( + log, + "Failed to convert between Item \"" << this->name() << "\" element " << element + << " (attribute \"" << this->attribute()->name() + << "\")'s expression's units: " << exStr << " to " + << this->units() << "."); + return 0.0; + } + return newM.m_value; + } + return m_values[element]; +} diff --git a/smtk/attribute/DoubleItem.h b/smtk/attribute/DoubleItem.h index fe98003b1..c582d9775 100644 --- a/smtk/attribute/DoubleItem.h +++ b/smtk/attribute/DoubleItem.h @@ -43,6 +43,9 @@ class SMTKCORE_EXPORT DoubleItem : public ValueItemTemplate const CopyAssignmentOptions& options, smtk::io::Logger& logger) override; + using ValueItemTemplate::value; + double value(std::size_t element, smtk::io::Logger& log) const override; + using ValueItemTemplate::setValue; ///@{ /// \brief Sets a value of the Item in the units defined in its definition. diff --git a/smtk/attribute/ReferenceItem.cxx b/smtk/attribute/ReferenceItem.cxx index d3ead6dc1..4118e8426 100644 --- a/smtk/attribute/ReferenceItem.cxx +++ b/smtk/attribute/ReferenceItem.cxx @@ -902,9 +902,7 @@ Item::Status ReferenceItem::assign( logger, "ReferenceItem: " << name() << "'s number of values (" << myNumVals << ") can not hold source ReferenceItem's number of values (" - << sourceNumVals << ") and Partial Copying was not permitted." - << " Reference Criteria:\n" - << def->criteriaAsString()); + << sourceNumVals << ") and Partial Copying was not permitted."); return result; } } @@ -999,13 +997,44 @@ Item::Status ReferenceItem::assign( else { result.markFailed(); + std::stringstream reason; auto def = smtk::dynamic_pointer_cast(this->definition()); + if (i >= this->numberOfValues()) + { + reason << "Index: " << i << " is not less than " << this->numberOfValues() << "\n"; + } + if (!def->isValueValid(val)) + { + reason << " Definition says the value is not valid\n"; + } + auto refAtt = m_referencedAttribute.lock(); + if (refAtt == nullptr) + { + reason << " Referenced Attribute is null/n"; + } + else + { + auto refAttRes = refAtt->attributeResource(); + if (refAttRes == nullptr) + { + reason << "Referenced Attribute Resource is null\n"; + } + else + { + auto comp = std::dynamic_pointer_cast(val); + if (refAttRes->findAttribute(comp, def->role()) != nullptr) + { + reason << "Found attribute in referenced resource/n"; + } + } + } smtkErrorMacro( logger, "Could not assign PersistentObject:" << val->name() << " of type: " << val->typeName() << " to ReferenceItem: " << sourceItem->name() << " and allowPartialValues options was not specified." - << " Reference Criteria:\n" + << " Reason:\n" + << reason.str() << "\n Reference Criteria:\n" << def->criteriaAsString()); return result; } diff --git a/smtk/attribute/ReferenceItemDefinition.cxx b/smtk/attribute/ReferenceItemDefinition.cxx index 72f89680f..84e45f7ef 100644 --- a/smtk/attribute/ReferenceItemDefinition.cxx +++ b/smtk/attribute/ReferenceItemDefinition.cxx @@ -221,6 +221,11 @@ void ReferenceItemDefinition::copyTo(Ptr dest, smtk::attribute::ItemDefinition:: dest->setNumberOfRequiredValues(m_numberOfRequiredValues); dest->setMaxNumberOfValues(m_maxNumberOfValues); dest->setIsExtensible(m_isExtensible); + dest->setRole(m_role); + dest->setHoldReference(m_holdReference); + dest->setEnforcesCategories(m_enforcesCategories); + dest->setLockType(m_lockType); + dest->setOnlyResources(m_onlyResources); for (const auto& acceptable : m_acceptable) { dest->setAcceptsEntries(acceptable.first, acceptable.second, true); diff --git a/smtk/attribute/ValueItem.cxx b/smtk/attribute/ValueItem.cxx index 0b89ad945..cfcb55df3 100644 --- a/smtk/attribute/ValueItem.cxx +++ b/smtk/attribute/ValueItem.cxx @@ -16,6 +16,7 @@ #include "smtk/attribute/Resource.h" #include "smtk/attribute/ValueItemDefinition.h" +#include "units/Converter.h" #include "units/System.h" #include // for std::find @@ -305,23 +306,84 @@ bool ValueItem::allowsExpressions() const return def->allowsExpressions(); } -bool ValueItem::setExpression(smtk::attribute::AttributePtr exp) +bool ValueItem::isAcceptable(const smtk::attribute::AttributePtr& exp) const { const ValueItemDefinition* def = static_cast(m_definition.get()); - if (def->allowsExpressions()) + if (!def->allowsExpressions()) { - if (!exp) - { - m_expression->unset(); - return true; - } - if (def->isValidExpression(exp)) - { - m_expression->setValue(exp); - return true; - } + return false; } - return false; + + if (!exp) + { + return true; + } + + if (!def->isValidExpression(exp)) + { + return false; + } + + if (this->units() == exp->units()) + { + return true; + } + + // Are the units (if any) compatible? + if (exp->units().empty()) + { + return true; + } + + if (this->units().empty()) + { + return false; + } + + const auto& unitSys = def->unitsSystem(); + // Can't determine if the units are compatible w/o units system + if (!unitSys) + { + return false; + } + + bool unitsSupported; + + // Are the item units supported by the units system + auto iUnits = unitSys->unit(this->units(), &unitsSupported); + if (!unitsSupported) + { + // the item's units are not supported by the units system + return false; + } + + // Are the expression units supported by the units system + auto eUnits = unitSys->unit(exp->units(), &unitsSupported); + if (!unitsSupported) + { + // the expression's units are not supported by the units system + return false; + } + + return (unitSys->convert(iUnits, eUnits) != nullptr); +} + +bool ValueItem::setExpression(smtk::attribute::AttributePtr exp) +{ + if (!this->isAcceptable(exp)) + { + return false; + } + + if (!exp) + { + m_expression->unset(); + } + else + { + m_expression->setValue(exp); + } + return true; } void ValueItem::visitChildren(std::function visitor, bool activeChildren) diff --git a/smtk/attribute/ValueItem.h b/smtk/attribute/ValueItem.h index 54fb4e0de..93e5ea4ff 100644 --- a/smtk/attribute/ValueItem.h +++ b/smtk/attribute/ValueItem.h @@ -58,11 +58,16 @@ class SMTKCORE_EXPORT ValueItem : public smtk::attribute::Item smtk::attribute::AttributePtr expression() const; bool setExpression(smtk::attribute::AttributePtr exp); + + ///\brief Returns true if the expression would be acceptable based om the item's + /// definition and units requirements + bool isAcceptable(const smtk::attribute::AttributePtr& exp) const; + virtual bool setNumberOfValues(std::size_t newSize) = 0; /** * @brief visitChildren Invoke a function on each (or, if \a findInActiveChildren - * is true, each active) child item. If a subclass presents childern items(ValueItem, - * Group, ComponentItem, ...) then this function should be overriden. + * is true, each active) child item. If a subclass presents children items(ValueItem, + * Group, ComponentItem, ...) then this function should be overridden. * @param visitor a lambda function which would be applied on children items * @param activeChildren a flag indicating whether it should be applied to active children only or not */ diff --git a/smtk/attribute/ValueItemTemplate.h b/smtk/attribute/ValueItemTemplate.h index cd82e6eff..4e903d7ab 100644 --- a/smtk/attribute/ValueItemTemplate.h +++ b/smtk/attribute/ValueItemTemplate.h @@ -45,12 +45,28 @@ class SMTK_ALWAYS_EXPORT ValueItemTemplate : public ValueItem bool setNumberOfValues(std::size_t newSize) override; ///@{ - /// \brief Returns a value of the item in the units specified in the units of its definition - DataT value(std::size_t element = 0) const; - DataT value(smtk::io::Logger& log) const { return this->value(0, log); } - DataT value(std::size_t element, smtk::io::Logger& log) const; + ///\brief Return the value for the item's specified element + /// + /// If the element is not set or in the case of an expression, an error is encountered during + /// evaluation, DataT() is returned. + /// + /// Note: When DataT does not have a default constructor, as in the case of double or + /// int. DataT() results in a zero-initialized value: + /// https://en.cppreference.com/w/cpp/language/value_initialization + virtual DataT value(std::size_t element = 0) const; + virtual DataT value(smtk::io::Logger& log) const { return this->value(0, log); } + virtual DataT value(std::size_t element, smtk::io::Logger& log) const; ///@} + ///\brief Evaluates an Item's expression for a specified element in the units of its definition + /// + /// If the element is not set or an error is encountered during + /// evaluation, DataT() is returned. + /// + /// Note: When DataT does not have a default constructor, as in the case of double or + /// int. DataT() results in a zero-initialized value: + /// https://en.cppreference.com/w/cpp/language/value_initialization + DataT evaluateExpression(std::size_t element, smtk::io::Logger& log) const; using ValueItem::valueAsString; std::string valueAsString(std::size_t element) const override; virtual bool setValue(const DataT& val) { return this->setValue(0, val); } @@ -140,66 +156,78 @@ DataT ValueItemTemplate::value(std::size_t element) const return this->value(element, smtk::io::Logger::instance()); } -// When DataT does not have a default constructor, as in the case of double or -// int. DataT() results in a zero-initialized value: -// https://en.cppreference.com/w/cpp/language/value_initialization template -DataT ValueItemTemplate::value(std::size_t element, smtk::io::Logger& log) const +DataT ValueItemTemplate::evaluateExpression(std::size_t element, smtk::io::Logger& log) const { - if (!this->isSet(element)) + if (!(this->isSet(element) && this->isExpression())) { smtkErrorMacro( log, - "Item \"" << this->name() << "\" element " << element << " is not set (attribute \"" + "Item \"" << this->name() << "\" element " << element << " is either not set " + << " or is not an expression (attribute \"" << this->attribute()->name() << "\")."); + return DataT(); + } + + smtk::attribute::AttributePtr expAtt = this->expression(); + if (!expAtt) + { + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" has no reference expression (attribute \"" << this->attribute()->name() << "\")."); return DataT(); } - if (isExpression()) + std::unique_ptr evaluator = expAtt->createEvaluator(); + if (!evaluator) { - smtk::attribute::AttributePtr expAtt = expression(); - if (!expAtt) - { - smtkErrorMacro( - log, - "Item \"" << this->name() << "\" has no reference expression (attribute \"" - << this->attribute()->name() << "\")."); - return DataT(); - } + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" expression is not evaluate (attribute \"" + << this->attribute()->name() << "\")."); + return DataT(); + } - std::unique_ptr evaluator = expAtt->createEvaluator(); - if (!evaluator) - { - smtkErrorMacro( - log, - "Item \"" << this->name() << "\" expression is not evaluate (attribute \"" - << this->attribute()->name() << "\")."); - return DataT(); - } + smtk::attribute::Evaluator::ValueType result; + // |evaluator| will report errors in |log| for the caller. + if (!evaluator->evaluate( + result, log, element, Evaluator::DependentEvaluationMode::EVALUATE_DEPENDENTS)) + { + return DataT(); + } - smtk::attribute::Evaluator::ValueType result; - // |evaluator| will report errors in |log| for the caller. - if (!evaluator->evaluate( - result, log, element, Evaluator::DependentEvaluationMode::EVALUATE_DEPENDENTS)) - { - return DataT(); - } + DataT resultAsDataT; + try + { + resultAsDataT = boost::get(result); + } + catch (const boost::bad_get&) + { + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" evaluation result was not compatible (attribute \"" + << this->attribute()->name() << "\")."); + return DataT(); + } - DataT resultAsDataT; - try - { - resultAsDataT = boost::get(result); - } - catch (const boost::bad_get&) - { - smtkErrorMacro( - log, - "Item \"" << this->name() << "\" evaluation result was not compatible (attribute \"" - << this->attribute()->name() << "\")."); - return DataT(); - } + return resultAsDataT; +} - return resultAsDataT; +template +DataT ValueItemTemplate::value(std::size_t element, smtk::io::Logger& log) const +{ + if (!this->isSet(element)) + { + smtkErrorMacro( + log, + "Item \"" << this->name() << "\" element " << element << " is not set (attribute \"" + << this->attribute()->name() << "\")."); + return DataT(); + } + + if (isExpression()) + { + return this->evaluateExpression(element, log); } else { diff --git a/smtk/attribute/pybind11/PybindValueItem.h b/smtk/attribute/pybind11/PybindValueItem.h index c4eac83ce..4732cf574 100644 --- a/smtk/attribute/pybind11/PybindValueItem.h +++ b/smtk/attribute/pybind11/PybindValueItem.h @@ -35,6 +35,7 @@ inline PySharedPtrClass< smtk::attribute::ValueItem, smtk::attribute::Item > pyb .def("_findChild", (smtk::attribute::ItemPtr (smtk::attribute::ValueItem::*)(::std::string const &, ::smtk::attribute::SearchStyle)) &smtk::attribute::ValueItem::findChild, py::arg("name"), py::arg("arg1")) .def("_findChild", (smtk::attribute::ConstItemPtr (smtk::attribute::ValueItem::*)(::std::string const &, ::smtk::attribute::SearchStyle) const) &smtk::attribute::ValueItem::findChild, py::arg("name"), py::arg("arg1")) .def("hasDefault", &smtk::attribute::ValueItem::hasDefault) + .def("isAcceptable", &smtk::attribute::ValueItem::isAcceptable) .def("isDiscrete", &smtk::attribute::ValueItem::isDiscrete) .def("isDiscreteIndexValid", &smtk::attribute::ValueItem::isDiscreteIndexValid, py::arg("value")) .def("isExpression", &smtk::attribute::ValueItem::isExpression) diff --git a/smtk/attribute/testing/cxx/unitInfixExpressionEvaluator.cxx b/smtk/attribute/testing/cxx/unitInfixExpressionEvaluator.cxx index 98d086153..ba5a26c00 100644 --- a/smtk/attribute/testing/cxx/unitInfixExpressionEvaluator.cxx +++ b/smtk/attribute/testing/cxx/unitInfixExpressionEvaluator.cxx @@ -26,13 +26,23 @@ const std::string sbt = R"( - + - + + + + + infixExpression + + + infixExpression + + + )"; @@ -65,6 +75,7 @@ void testSimpleEvaluation() smtk::attribute::ResourcePtr attRes = createResourceForTest(); smtk::attribute::DefinitionPtr infixExpDef = attRes->findDefinition("infixExpression"); smtk::attribute::AttributePtr infixExpAtt = attRes->createAttribute(infixExpDef); + smtk::attribute::AttributePtr a = attRes->createAttribute("A"); infixExpAtt->findString("expression")->setValue("2 + 2"); @@ -82,6 +93,16 @@ void testSimpleEvaluation() double computation = boost::get(result); smtkTest(computation == 4.0, "Incorrectly computed 2 + 2.") + + //Lets try assigning the expression which is in inches to a double whose units are feet + auto d1 = a->findDouble("d1"); + smtkTest(d1->setExpression(infixExpAtt), "Could not set expression to d1."); + smtkTest( + d1->value() == 48.0, + "Item d1 should have return 48 (inches) but instead returned: " << d1->value() << "."); + // We should be able to set the expression on d2 which whose units are Kelvin + auto d2 = a->findDouble("d2"); + smtkTest(!d2->setExpression(infixExpAtt), "Was able to set expression to d2."); } void testOneChildExpression() diff --git a/smtk/extension/qt/qtInputsItem.cxx b/smtk/extension/qt/qtInputsItem.cxx index de8615301..0cdcedd1a 100644 --- a/smtk/extension/qt/qtInputsItem.cxx +++ b/smtk/extension/qt/qtInputsItem.cxx @@ -1375,7 +1375,10 @@ void qtInputsItem::displayExpressionWidget(bool checkstate) std::vector::iterator it; for (it = result.begin(); it != result.end(); ++it) { - attNames.push_back((*it)->name().c_str()); + if (inputitem->isAcceptable(*it)) + { + attNames.push_back((*it)->name().c_str()); + } } attNames.sort(); // Now add Please Select and Create Options @@ -1507,15 +1510,12 @@ void qtInputsItem::onExpressionReferenceChanged() new smtk::extension::qtAttributeEditorDialog(newAtt, m_itemInfo.uiManager(), m_widget); auto status = editor->exec(); QStringList itemsInComboBox; - if (status == QDialog::Rejected) + if ((status == QDialog::Rejected) || (!inputitem->setExpression(newAtt))) { lAttResource->removeAttribute(newAtt); } else { - // The user has created a new expression so add it - // to the list of expression names and set the item to use it - inputitem->setExpression(newAtt); itemsInComboBox.append(newAtt->name().c_str()); // Signal that a new attribute was created - since this instance is not From 1ef4b4efc6b7e0721a453d0c36fc91b083aeb98b Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Tue, 30 Jul 2024 13:39:01 -0500 Subject: [PATCH 35/42] Added an optional shortcut parameter for changing modes --- smtk/extension/qt/diagram/qtDiagram.cxx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/smtk/extension/qt/diagram/qtDiagram.cxx b/smtk/extension/qt/diagram/qtDiagram.cxx index e9c3dfb8a..15f75bb3e 100644 --- a/smtk/extension/qt/diagram/qtDiagram.cxx +++ b/smtk/extension/qt/diagram/qtDiagram.cxx @@ -338,6 +338,11 @@ class qtDiagram::Internal { defaultMode = viewMode.get(); } + if (!child.attributeAsString("Shortcut").empty()) + { + QString shortcut = QString::fromStdString(child.attributeAsString("Shortcut")); + viewMode->modeAction()->setShortcut(QKeySequence(shortcut)); + } } else { From fffed54ceb7f945f8d952b4e78a67718ba95d524 Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Thu, 18 Jul 2024 14:21:18 -0500 Subject: [PATCH 36/42] Improve resource locking scheme in operations. This MR introduces the following changes to the way operations lock resources: 1. Introduces a parent-child relationship between operations where a "child" operation that is being run by its "parent" can share resource locks with the parent. 2. In the event that an operation cannot acquire ALL of its required resource locks, it will release any resource locks it did successfully acquire and then retry later. This gives each thread that is currently attempting to acquire resource locks a chance to do so. --- doc/release/notes/changesToOperation.rst | 15 ++ doc/userguide/operation/support.rst | 84 +++++++ smtk/common/Deprecation.h | 6 + .../paraview/mesh/VTKMeshCellSelection.cxx | 2 +- smtk/extension/vtk/operators/CMakeLists.txt | 1 + smtk/markup/operators/TagIndividual.cxx | 2 +- smtk/markup/operators/Ungroup.cxx | 2 +- smtk/mesh/operators/Write.sbt | 2 +- smtk/mesh/operators/WriteResource.cxx | 4 +- smtk/operation/Helper.cxx | 2 +- smtk/operation/Helper.h | 3 +- smtk/operation/Operation.cxx | 225 ++++++++++++++---- smtk/operation/Operation.h | 69 +++++- smtk/operation/operators/ReadResource.cxx | 3 +- smtk/operation/operators/WriteResource.cxx | 3 +- smtk/project/operators/Write.cxx | 3 +- smtk/session/mesh/operators/Transform.cxx | 2 +- smtk/session/mesh/operators/Write.cxx | 4 +- smtk/session/polygon/operators/Write.sbt | 2 +- smtk/session/vtk/operators/Write.cxx | 2 +- 20 files changed, 362 insertions(+), 74 deletions(-) create mode 100644 doc/release/notes/changesToOperation.rst diff --git a/doc/release/notes/changesToOperation.rst b/doc/release/notes/changesToOperation.rst new file mode 100644 index 000000000..267acfa8b --- /dev/null +++ b/doc/release/notes/changesToOperation.rst @@ -0,0 +1,15 @@ +Changes to Operation +==================== + +Deprecated Key struct +--------------------- + +The ``Key`` struct has been deprecated in favor of ``BaseKey`` which contains options to alter +certain behaviors of ``Operation::operate()`` when running an operation from within another +operation's ``operateInternal()`` synchronously. + +``Key`` derives from ``BaseKey`` so that the legacy API can still be satisfied, but any dependent +code should be refactored to use ``Operation::childKey()`` instead + +See the user documentation for more details about the options that can be passed to +``Operation::childKey()`` in the "Resource Locking" section. diff --git a/doc/userguide/operation/support.rst b/doc/userguide/operation/support.rst index 2b0d0184a..5a71e06fc 100644 --- a/doc/userguide/operation/support.rst +++ b/doc/userguide/operation/support.rst @@ -73,6 +73,90 @@ use to register an icon to your operation. Icons are functors that take in a secondary (background) color and return a string holding SVG for an icon that contrasts well with the background. +Resource Locking +---------------- + +To prevent operations running in separate threads from modifying the same persistent objects +at the same time, each operation must lock the resources it uses. +SMTK provides both read- and write-locks for resources. +Locking is performed only at the granularity of resources, not components. +Lock acquisition should never occur on the user-interface thread of an application. +By default, operations inspect their input parameters and acquire locks for all the resources +mentioned (directly or via components they own) before the ``operateInternal()`` method is +called. +You may override this, but you are responsible to ensure resource-locking is consistent +and does not cause deadlocks or race conditions. + +An operation that locks a resource for reading may run simultaneous with other operations +that hold read-locks, but when a write-lock is held, no other operations that lock the +same resource may run simultaneously (whether read- or write-locks are requested). + +It is possible for an operation to: + +1. synchronously run another operation internally as part of its processing. + + The default behavior of ``operate()`` can be altered by passing a value of `Operation::BaseKey` + as the parameter. This value is returned by ``Operation::childKey()`` and is only accessible by + classes that derive from ``Operation``. Any operation that passes the value returned from + ``Operation::childKey()`` establishes itself as the "parent" operation and the nested operation + is considered a "child". The following options are available for altering the default behavior of + ``operate()``: + + .. list-table:: Locking behavior + :widths: 10 40 + :header-rows: 1 + + * - Option + - Description + + * - LockOption::LockAll + - All resource locks will be acquired before ``operateInternal()`` is called. Any resource + locks already acquired by the parent operation will be shared with the child operation, + thus allowing the child operation to acquire any additional resource locks required by it. + The child operation will fail if it attempts to acquire a write lock on a resource that + the parent operation already holds a read lock on. **This is the default option**. + + * - LockOption::ParentLocksOnly + - No additional locks will be attempted to be acquired by the child operation. The child + operation will fail if it does require more resource locks than its parent. + + * - LockOption::SkipLocks + - No resource locking will be attempted by the child operation. **This is the legacy + behavior**. + + .. list-table:: Observer behavior + :widths: 10 40 + :header-rows: 1 + + * - Option + - Description + + * - ObserverOption::InvokeObservers + - Observers will be invoked following parameter validation and again following + ``operateInternal()``. + + * - ObserverOption::SkipObservers + - No observers will be invoked. **This is the default option**. + + .. list-table:: Parameters behavior + :widths: 10 40 + :header-rows: 1 + + * - Option + - Description + + * - ParametersOption::Validate + - The parameters of the child operation will be validated to determine if + ``operateInternal()`` can be called. **This is the default option**. + + * - ParametersOption::SkipValidation + - No validation of the child operation's parameters will take place. **This is the legacy + behavior**. + +2. asynchronously launch another operation to run later in a separate thread. + + + Launching operations -------------------- diff --git a/smtk/common/Deprecation.h b/smtk/common/Deprecation.h index 5ead64ab3..a7e2556ac 100644 --- a/smtk/common/Deprecation.h +++ b/smtk/common/Deprecation.h @@ -76,4 +76,10 @@ #define SMTK_DEPRECATED_IN_24_01(reason) #endif +#if SMTK_DEPRECATION_LVEL >= SMTK_VERSION_CHECK(24, 01, 99) +#define SMTK_DEPRECATED_IN_24_08(reason) SMTK_DEPRECATION(SMTK_DEPRECATION_REASON(24, 08, reason)) +#else +#define SMTK_DEPRECATED_IN_24_08(reason) +#endif + #endif // smtk_common_Deprecation_h diff --git a/smtk/extension/paraview/mesh/VTKMeshCellSelection.cxx b/smtk/extension/paraview/mesh/VTKMeshCellSelection.cxx index d9e53e6d5..4c81a2f54 100644 --- a/smtk/extension/paraview/mesh/VTKMeshCellSelection.cxx +++ b/smtk/extension/paraview/mesh/VTKMeshCellSelection.cxx @@ -183,7 +183,7 @@ bool VTKMeshCellSelection::transcribeCellIdSelection(Result& result) } } - smtk::operation::Operation::Result selectionResult = selectCells->operate(Key()); + smtk::operation::Operation::Result selectionResult = selectCells->operate(this->childKey()); smtk::attribute::ComponentItem::Ptr created = selectionResult->findComponent("created"); selection.insert(created->value()); result->findComponent("created")->appendValue(created->value()); diff --git a/smtk/extension/vtk/operators/CMakeLists.txt b/smtk/extension/vtk/operators/CMakeLists.txt index ed4713c4d..43ece2852 100644 --- a/smtk/extension/vtk/operators/CMakeLists.txt +++ b/smtk/extension/vtk/operators/CMakeLists.txt @@ -47,6 +47,7 @@ vtk_module_link(vtkSMTKOperationsExt smtkIOVTK VTK::FiltersGeometry VTK::FiltersPoints + VTK::IOPLY ) if (SMTK_ENABLE_PARAVIEW_SUPPORT) diff --git a/smtk/markup/operators/TagIndividual.cxx b/smtk/markup/operators/TagIndividual.cxx index 544384809..6308d2944 100644 --- a/smtk/markup/operators/TagIndividual.cxx +++ b/smtk/markup/operators/TagIndividual.cxx @@ -244,7 +244,7 @@ std::size_t TagIndividual::untagNodes( expunged->appendValue(tag); auto delOp = smtk::markup::Delete::create(); delOp->parameters()->associations()->appendValue(tag); - delOp->operate(TagIndividual::Key{}); + delOp->operate(this->childKey()); } else if (numUntagged) { diff --git a/smtk/markup/operators/Ungroup.cxx b/smtk/markup/operators/Ungroup.cxx index 02566bdec..f0c80ece1 100644 --- a/smtk/markup/operators/Ungroup.cxx +++ b/smtk/markup/operators/Ungroup.cxx @@ -60,7 +60,7 @@ Ungroup::Result Ungroup::operateInternal() group->disconnect(); deleteOp->parameters()->associations()->appendValue(group); } - auto deleteResult = deleteOp->operate(Key{}); + auto deleteResult = deleteOp->operate(this->childKey()); auto deleteExpunged = deleteResult->findComponent("expunged"); for (std::size_t ii = 0; ii < deleteExpunged->numberOfValues(); ++ii) { diff --git a/smtk/mesh/operators/Write.sbt b/smtk/mesh/operators/Write.sbt index f40c2a6b3..27b22b45f 100644 --- a/smtk/mesh/operators/Write.sbt +++ b/smtk/mesh/operators/Write.sbt @@ -17,7 +17,7 @@ the original mesh. + Extensible="false" LockType="Read" OnlyResources="true"> diff --git a/smtk/mesh/operators/WriteResource.cxx b/smtk/mesh/operators/WriteResource.cxx index 0a00bae98..80bc351b4 100644 --- a/smtk/mesh/operators/WriteResource.cxx +++ b/smtk/mesh/operators/WriteResource.cxx @@ -115,7 +115,7 @@ WriteResource::Result WriteResource::operateInternal() writeOp->parameters()->associate(resource); // Execute the operation - smtk::operation::Operation::Result writeOpResult = writeOp->operate(Key()); + smtk::operation::Operation::Result writeOpResult = writeOp->operate(this->childKey()); // Test for success if ( @@ -174,7 +174,7 @@ WriteResource::Result WriteResource::operateInternal() writeOp->parameters()->associate(resource); // Execute the operation - smtk::operation::Operation::Result writeOpResult = writeOp->operate(Key()); + smtk::operation::Operation::Result writeOpResult = writeOp->operate(this->childKey()); // Test for success if ( diff --git a/smtk/operation/Helper.cxx b/smtk/operation/Helper.cxx index 99158beca..0bff0538a 100644 --- a/smtk/operation/Helper.cxx +++ b/smtk/operation/Helper.cxx @@ -28,7 +28,7 @@ Helper& Helper::instance() { if (g_instanceStack.empty()) { - g_instanceStack.emplace_back(std::unique_ptr(new Helper)); + g_instanceStack.emplace_back(new Helper); } return *(g_instanceStack.back()); } diff --git a/smtk/operation/Helper.h b/smtk/operation/Helper.h index 93314d1cd..5ca7b2ea7 100644 --- a/smtk/operation/Helper.h +++ b/smtk/operation/Helper.h @@ -63,7 +63,8 @@ class SMTKCORE_EXPORT Helper protected: Helper(); - Operation::Key* m_key; + Operation::Key* m_key{ nullptr }; + /// m_topLevel indicates whether pushInstance() (false) or instance() (true) /// was used to create this helper. bool m_topLevel = true; diff --git a/smtk/operation/Operation.cxx b/smtk/operation/Operation.cxx index 42122380c..290cd78c9 100644 --- a/smtk/operation/Operation.cxx +++ b/smtk/operation/Operation.cxx @@ -31,9 +31,11 @@ #include "nlohmann/json.hpp" +#include #include #include #include +#include namespace { @@ -129,23 +131,64 @@ ResourceAccessMap Operation::identifyLocksRequired() } Operation::Result Operation::operate() +{ + // Call operate that will invoke observers. + return this->operate(BaseKey( + nullptr, ObserverOption::InvokeObservers, LockOption::LockAll, ParametersOption::Validate)); +} + +Operation::Result Operation::operate(const BaseKey& key) { // Gather all requested resources and their lock types. - auto resourcesAndLockTypes = this->identifyLocksRequired(); + const auto resourcesAndLockTypes = this->identifyLocksRequired(); // Mutex to prevent multiple Operations from locking resources at the same // time (which could result in deadlock). static std::mutex mutex; - // Lock the resources. - mutex.lock(); - for (auto& resourceAndLockType : resourcesAndLockTypes) + // Track resources that were actually locked by this operation instance. + ResourceAccessMap lockedByThis; + + size_t numRetries = 0; + + if (key.m_lockOption != LockOption::SkipLocks) { - auto resource = resourceAndLockType.first.lock(); - auto& lockType = resourceAndLockType.second; + // Lock the resources. + while (true) + { + // TODO: A maximum retry limit may be a good thing add here to prevent some operation from + // occupying a thread in the operation thread pool indefinitely in the event that it cannot + // acquire all of its resources' locks within a reasonable amount of time. + if (numRetries++ > 0) + { + // Allow other threads to have CPU time. + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + // Only allow one operation at a time to attempt to acquire all necessary resource locks. + std::lock_guard guard(mutex); -// Leave this for debugging, but do not include it in every debug build -// as it can be quite noisy. + if (key.m_parent) + { + // Inherit resource locks from the parent operation. + this->m_lockedResources = key.m_parent->lockedResources(); + } + + // Populate queue for pending resource locks. + std::queue pending; + for (const auto& resourceAndLockType : resourcesAndLockTypes) + { + pending.push(resourceAndLockType); + } + + while (!pending.empty()) + { + const auto resourceAndLockType = pending.front(); + auto resource = resourceAndLockType.first.lock(); + const auto& lockType = resourceAndLockType.second; + + // Leave this for debugging, but do not include it in every debug build + // as it can be quite noisy. #if 0 // Given the puzzling result of deadlock that can arise if one Operation // calls another Operation using its public API and passes it a Resource @@ -158,17 +201,86 @@ Operation::Result Operation::operate() // This will avoid the inner Operation's resource locking and execute it // directly. Be sure to verify the operation's validity prior to execution // (via the ableToOperate() method). - std::cout << "Operation \"" << this->typeName() << "\" is locking resource " << resource->name() - << " (" << resource->typeName() << ") with lock type \"" - << (lockType == smtk::resource::LockType::Read - ? "Read" - : (lockType == smtk::resource::LockType::Write ? "Write" : "DoNotLock")) - << "\"\n"; + std::cout << "Operation \"" << this->typeName() << "\" is locking resource " << resource->name() + << " (" << resource->typeName() << ") with lock type \"" + << (lockType == smtk::resource::LockType::Read + ? "Read" + : (lockType == smtk::resource::LockType::Write ? "Write" : "DoNotLock")) + << "\"\n"; #endif - resource->lock({}).lock(lockType); + // Is this resource already locked (by a parent perhaps). + const auto it = this->m_lockedResources.find(resource); + if (it != this->m_lockedResources.end()) + { + // Verify that no writer is allowed while the (parent) operation + // already has a read lock on the resource. + if (it->second == resource::LockType::Read && lockType == resource::LockType::Write) + { + // This operation should not be able to operate. Undo the current + // resource locking and fail early. + this->unlockResources(lockedByThis); + smtkErrorMacro( + this->log(), + "Attempted to acquire a write lock on a resource that a parent operation currently " + "holds a read lock on."); + return this->createResult(Outcome::UNABLE_TO_OPERATE); + } + else + { + // This lock is already acquired by the parent, so pop this + // resource from the queue and continue at the next + // resource. + pending.pop(); + continue; + } + } + else if (key.m_lockOption == LockOption::ParentLocksOnly) + { + // The parent does not already hold the lock for this resource, so + // fail early. + return this->createResult(Outcome::UNABLE_TO_OPERATE); + } + + // Attempt to acquire this resource's lock. + if ( + lockType != smtk::resource::LockType::DoNotLock && !resource->lock({}).tryLock(lockType)) + { + // This resource's lock could not be acquired. Release the locks on any + // resource whose lock was successfully acquired so that other threads + // have a chance to attempt the locks if they are waiting. + this->unlockResources(lockedByThis); + lockedByThis.clear(); + this->m_lockedResources.clear(); + + // Stop processing the queue. The locks will be attempted again later. + break; + } + + this->m_lockedResources.insert(resourceAndLockType); + lockedByThis.insert(resourceAndLockType); + + // Pop this resource from the queue. + pending.pop(); + } // while (!pending.empty()) + + if (pending.empty()) + { + // All of the locks were acquired successfully. Continue with the + // operation logic. + break; + } + else + { + // All of the resource locks could not be acquired. Release the locks on any + // resource whose lock was successfully acquired so that other threads + // have a chance to attempt the locks if they are waiting. + this->unlockResources(lockedByThis); + lockedByThis.clear(); + this->m_lockedResources.clear(); + } + } // while (true) } - mutex.unlock(); // Remember where the log was so we only serialize messages for this // operation: @@ -183,10 +295,10 @@ Operation::Result Operation::operate() // -- and observers of both will expect them to be called in pairs. auto manager = m_manager.lock(); bool observePostOperation = manager != nullptr; - Outcome outcome; + Outcome outcome = Outcome::UNKNOWN; // First, we check that the operation is able to operate. - if (!this->ableToOperate()) + if (key.m_paramsOption == ParametersOption::Validate && !this->ableToOperate()) { outcome = Outcome::UNABLE_TO_OPERATE; result = this->createResult(outcome); @@ -194,34 +306,40 @@ Operation::Result Operation::operate() observePostOperation = false; } // Then, we check if any observers wish to cancel this operation. - else if (manager && manager->observers()(*this, EventType::WILL_OPERATE, nullptr)) - { - outcome = Outcome::CANCELED; - result = this->createResult(outcome); - } else { - // Finally, execute the operation. - - // Set the debug level if specified as a convenience for subclasses: - smtk::attribute::IntItem::Ptr debugItem = this->parameters()->findInt("debug level"); - m_debugLevel = ((debugItem && debugItem->isEnabled()) ? debugItem->value() : 0); - - // Perform the derived operation. - result = this->operateInternal(); - // Post-process the result if the operation was successful. - outcome = static_cast(result->findInt("outcome")->value()); - if (outcome == Outcome::SUCCEEDED) + if ( + key.m_observerOption == ObserverOption::InvokeObservers && manager && + manager->observers()(*this, EventType::WILL_OPERATE, nullptr)) { - this->postProcessResult(result); + outcome = Outcome::CANCELED; + result = this->createResult(outcome); } - // By default, all executed operations are assumed to modify any input - // resource accessed with a Write LockType and any resources referenced in - // the result. - if (outcome == Outcome::SUCCEEDED || outcome == Outcome::FAILED) + if (outcome != Outcome::CANCELED) { - this->markModifiedResources(result); + // Finally, execute the operation. + + // Set the debug level if specified as a convenience for subclasses: + smtk::attribute::IntItem::Ptr debugItem = this->parameters()->findInt("debug level"); + m_debugLevel = ((debugItem && debugItem->isEnabled()) ? debugItem->value() : 0); + + // Perform the derived operation. + result = this->operateInternal(); + // Post-process the result if the operation was successful. + outcome = static_cast(result->findInt("outcome")->value()); + if (outcome == Outcome::SUCCEEDED) + { + this->postProcessResult(result); + } + + // By default, all executed operations are assumed to modify any input + // resource accessed with a Write LockType and any resources referenced in + // the result. + if (outcome == Outcome::SUCCEEDED || outcome == Outcome::FAILED) + { + this->markModifiedResources(result); + } } } @@ -241,7 +359,7 @@ Operation::Result Operation::operate() } // Execute post-operation observation - if (observePostOperation) + if (key.m_observerOption == ObserverOption::InvokeObservers && observePostOperation && manager) { manager->observers()(*this, EventType::DID_OPERATE, result); } @@ -255,16 +373,21 @@ Operation::Result Operation::operate() static_cast(smtk::operation::Operation::Outcome::FAILED)); } - // Unlock the resources. - for (auto& resourceAndLockType : resourcesAndLockTypes) + // Unlock the resources locked by this operation instance. + this->unlockResources(lockedByThis); + this->m_lockedResources.clear(); + + return result; +} + +void Operation::unlockResources(const ResourceAccessMap& resources) +{ + for (const auto& resourceAndLockType : resources) { auto resource = resourceAndLockType.first.lock(); - auto& lockType = resourceAndLockType.second; - + const auto& lockType = resourceAndLockType.second; resource->lock({}).unlock(lockType); } - - return result; } Operation::Outcome Operation::safeOperate() @@ -575,6 +698,14 @@ Operation::Specification Operation::createBaseSpecification() const return spec; } +Operation::BaseKey Operation::childKey( + ObserverOption observerOption, + LockOption lockOption, + ParametersOption paramsOption) const +{ + return BaseKey(this, observerOption, lockOption, paramsOption); +} + smtk::resource::ManagerPtr Operation::resourceManager() { diff --git a/smtk/operation/Operation.h b/smtk/operation/Operation.h index 640069c1c..0a94265ee 100644 --- a/smtk/operation/Operation.h +++ b/smtk/operation/Operation.h @@ -14,6 +14,7 @@ #include "smtk/PublicPointerDefs.h" #include "smtk/SharedFromThis.h" +#include "smtk/common/Deprecation.h" #include #include @@ -209,6 +210,9 @@ class SMTKCORE_EXPORT Operation : smtkEnableSharedPtr(Operation) /// on the operation's parameters. virtual ResourceAccessMap identifyLocksRequired(); + /// Returns the set of resources that are currently locked by this operation. + const ResourceAccessMap& lockedResources() const { return this->m_lockedResources; } + /// Perform the actual operation and construct the result. virtual Result operateInternal() = 0; @@ -237,21 +241,61 @@ class SMTKCORE_EXPORT Operation : smtkEnableSharedPtr(Operation) std::weak_ptr m_manager; std::shared_ptr m_managers; - // Operations need the ability to execute Operations without going through - // all of the checks and locks associated with the public operate() method. - // We therefore use a variant of the PassKey pattern to grant Operation - // and all of its children access to another operator's operateInternal() - // method. It is up to the writer of the outer Operation to ensure that - // Resources are accessed with appropriate lock types and that Operations - // called in this manner have valid inputs. - struct Key + enum LockOption { - explicit Key() = default; - Key(std::initializer_list) {} + LockAll, + ParentLocksOnly, + SkipLocks }; + enum ObserverOption + { + InvokeObservers, + SkipObservers + }; + + enum ParametersOption + { + Validate, + SkipValidation + }; + +private: + struct BaseKey + { + BaseKey() = default; + BaseKey( + const Operation* parent, + ObserverOption observerOption, + LockOption lockOption, + ParametersOption paramsOption) + : m_parent(parent) + , m_lockOption(lockOption) + , m_observerOption(observerOption) + , m_paramsOption(paramsOption) + { + } + + const Operation* m_parent{ nullptr }; + LockOption m_lockOption{ LockOption::SkipLocks }; + ObserverOption m_observerOption{ ObserverOption::SkipObservers }; + ParametersOption m_paramsOption{ ParametersOption::SkipValidation }; + }; + +protected: + SMTK_DEPRECATED_IN_24_08("Use this->childKey() instead.") + struct Key : BaseKey + { + Key() = default; + }; + + BaseKey childKey( + ObserverOption observerOption = ObserverOption::SkipObservers, + LockOption lockOption = LockOption::LockAll, + ParametersOption paramsOption = ParametersOption::Validate) const; + public: - Result operate(Key) { return operateInternal(); } + Result operate(const BaseKey& key); private: // Construct the operation's specification. This is typically done by reading @@ -260,10 +304,13 @@ class SMTKCORE_EXPORT Operation : smtkEnableSharedPtr(Operation) // operation's input and output attributes. virtual Specification createSpecification() = 0; + void unlockResources(const ResourceAccessMap& resources); + Specification m_specification; Parameters m_parameters; Definition m_resultDefinition; std::vector> m_results; + ResourceAccessMap m_lockedResources; }; /**\brief Return the outcome of an operation given its \a result object. diff --git a/smtk/operation/operators/ReadResource.cxx b/smtk/operation/operators/ReadResource.cxx index e49b32d0b..dd887f83b 100644 --- a/smtk/operation/operators/ReadResource.cxx +++ b/smtk/operation/operators/ReadResource.cxx @@ -161,7 +161,8 @@ ReadResource::Result ReadResource::operateInternal() // Run the local read internally using Key (do not fire observers) and // copy the output resources into our result. - smtk::operation::Operation::Result readOperationResult = readOperation->operate(Key{}); + smtk::operation::Operation::Result readOperationResult = + readOperation->operate(this->childKey()); if ( readOperationResult->findInt("outcome")->value() != static_cast(smtk::operation::Operation::Outcome::SUCCEEDED)) diff --git a/smtk/operation/operators/WriteResource.cxx b/smtk/operation/operators/WriteResource.cxx index a82cc4069..2d38d7719 100644 --- a/smtk/operation/operators/WriteResource.cxx +++ b/smtk/operation/operators/WriteResource.cxx @@ -144,7 +144,8 @@ smtk::operation::Operation::Result WriteResource::operateInternal() return this->createResult(smtk::operation::Operation::Outcome::FAILED); } - smtk::operation::Operation::Result writeOperationResult = writeOperation->operate(Key()); + smtk::operation::Operation::Result writeOperationResult = + writeOperation->operate(this->childKey()); if ( writeOperationResult->findInt("outcome")->value() != static_cast(smtk::operation::Operation::Outcome::SUCCEEDED)) diff --git a/smtk/project/operators/Write.cxx b/smtk/project/operators/Write.cxx index 5293f6c81..960e67a5c 100644 --- a/smtk/project/operators/Write.cxx +++ b/smtk/project/operators/Write.cxx @@ -111,7 +111,8 @@ Write::Result Write::operateInternal() } write->parameters()->associate(resource); - smtk::operation::Operation::Result writeResult = write->operate(); + smtk::operation::Operation::Result writeResult = + write->operate(this->childKey(ObserverOption::InvokeObservers)); if ( writeResult->findInt("outcome")->value() != static_cast(smtk::operation::Operation::Outcome::SUCCEEDED)) diff --git a/smtk/session/mesh/operators/Transform.cxx b/smtk/session/mesh/operators/Transform.cxx index 8de7a1c7b..1efca253a 100644 --- a/smtk/session/mesh/operators/Transform.cxx +++ b/smtk/session/mesh/operators/Transform.cxx @@ -85,7 +85,7 @@ Transform::Result Transform::operateInternal() } // Execute the transform. - transform->operate(Key()); + transform->operate(this->childKey()); // Access the attribute associated with the modified meshes. Result result = this->createResult(smtk::operation::Operation::Outcome::SUCCEEDED); diff --git a/smtk/session/mesh/operators/Write.cxx b/smtk/session/mesh/operators/Write.cxx index d6ceb9576..a13d2d307 100644 --- a/smtk/session/mesh/operators/Write.cxx +++ b/smtk/session/mesh/operators/Write.cxx @@ -118,7 +118,7 @@ Write::Result Write::operateInternal() exportOp->parameters()->associate(resource); // Execute the operation - smtk::operation::Operation::Result exportOpResult = exportOp->operate(Key()); + smtk::operation::Operation::Result exportOpResult = exportOp->operate(this->childKey()); // Test for success if ( @@ -189,7 +189,7 @@ Write::Result Write::operateInternal() exportOp->parameters()->associate(resource); // Execute the operation - smtk::operation::Operation::Result exportOpResult = exportOp->operate(Key()); + smtk::operation::Operation::Result exportOpResult = exportOp->operate(this->childKey()); // Test for success if ( diff --git a/smtk/session/polygon/operators/Write.sbt b/smtk/session/polygon/operators/Write.sbt index f0ae34177..2cdd6e5c1 100644 --- a/smtk/session/polygon/operators/Write.sbt +++ b/smtk/session/polygon/operators/Write.sbt @@ -4,7 +4,7 @@ - + diff --git a/smtk/session/vtk/operators/Write.cxx b/smtk/session/vtk/operators/Write.cxx index 53c48e140..e736f50b7 100644 --- a/smtk/session/vtk/operators/Write.cxx +++ b/smtk/session/vtk/operators/Write.cxx @@ -135,7 +135,7 @@ Write::Result Write::operateInternal() exportOp->parameters()->associate(dataset.entityRecord()); exportOp->parameters()->findFile("filename")->setValue(modelFile); - Result exportOpResult = exportOp->operate(Key()); + Result exportOpResult = exportOp->operate(this->childKey()); if (exportOpResult->findInt("outcome")->value() != static_cast(Outcome::SUCCEEDED)) { From f4cca8d1b81af0d600e568fc54b538fc864b9584 Mon Sep 17 00:00:00 2001 From: Robert O'Bara Date: Thu, 1 Aug 2024 13:32:08 -0400 Subject: [PATCH 37/42] ENH: Adding Custom Filter Support for Attributes Properties on Definitions can now be *inherited* by Attributes and derived Definitions when creating queries and association rules. For example if Definition **A** has a floating-point property "alpha" with value 5.0, and if Definition **B** is derived from **A** and Attribute **a** is from **A** and Attribute **b** is from **B**, then all would match the rule "any[ floating-point { 'alpha' = 5.0 }]". If later **B** was to also have floating-point property "alpha" with value 10.0 associated with it, it would override the value coming from **A** so both it and **b** would no longer pass the rule. Similarly, properties on Attributes can override the values coming from its Definition. Also implemented differentiation between attributes and definitions. If the rule starts with *attribute* then only attributes have the possibility of matching the rest of the rule. Similarly, if the rule starts with *definition* then only definitions have the possibility of matching the rest of the rule. If the rule starts with *any* or * then either attributes or definitions are allowed. **Note** that the ``properties()`` methods on attribute and definition objects do not return *inherited* values but instead only those values local to the object. In the future, we may add methods to interact directly with inherited properties but for now only filter-strings process inherited properties. See smtk/attribute/testing/c++/unitPropertiesFilter.cxx and data/attribute/propertiesFilterExample.sbt for examples. Also added additional debugging methods to help track down clone resource test intermittent failures. --- .../propertiesFilterExample.sbt | 26 +++ doc/release/notes/attributeChanges.rst | 15 +- smtk/attribute/ReferenceItem.cxx | 2 +- smtk/attribute/ReferenceItemDefinition.cxx | 90 +++++++- smtk/attribute/ReferenceItemDefinition.h | 4 + smtk/attribute/Resource.cxx | 5 +- smtk/attribute/filter/Action.h | 205 ++++++++++++++++++ smtk/attribute/filter/Attribute.h | 81 +++++-- smtk/attribute/filter/FloatingPointActions.h | 51 +++++ smtk/attribute/filter/Grammar.h | 9 +- smtk/attribute/filter/IntegerActions.h | 50 +++++ smtk/attribute/filter/ResourceActions.h | 19 ++ smtk/attribute/filter/RuleFor.h | 127 +++++++++++ smtk/attribute/filter/StringActions.h | 132 +++++++++++ smtk/attribute/testing/cxx/CMakeLists.txt | 1 + .../testing/cxx/unitAttributeAssociation.cxx | 4 +- .../testing/cxx/unitPropertiesFilter.cxx | 138 ++++++++++++ smtk/graph/Resource.h | 3 + smtk/io/XmlPropertyParsingHelper.txx | 5 + smtk/project/Project.cxx | 1 + smtk/resource/CMakeLists.txt | 14 +- smtk/resource/Resource.cxx | 1 + smtk/resource/filter/Action.h | 20 +- smtk/resource/filter/Filter.h | 4 +- smtk/resource/filter/FloatingPointActions.h | 50 +++++ ...FloatingPoint.h => FloatingPointGrammar.h} | 21 +- smtk/resource/filter/Grammar.h | 7 +- smtk/resource/filter/IntegerActions.h | 50 +++++ .../filter/{Integer.h => IntegerGrammar.h} | 19 +- smtk/resource/filter/ResourceActions.h | 18 ++ smtk/resource/filter/Rule.h | 29 --- smtk/resource/filter/RuleFor.h | 58 +++++ smtk/resource/filter/StringActions.h | 128 +++++++++++ .../filter/{String.h => StringGrammar.h} | 93 +------- smtk/resource/filter/VectorActions.h | 102 +++++++++ .../filter/{Vector.h => VectorGrammar.h} | 74 +------ .../testing/cxx/TestResourceFilter.cxx | 1 + 37 files changed, 1383 insertions(+), 274 deletions(-) create mode 100644 data/attribute/attribute_collection/propertiesFilterExample.sbt create mode 100644 smtk/attribute/filter/Action.h create mode 100644 smtk/attribute/filter/FloatingPointActions.h create mode 100644 smtk/attribute/filter/IntegerActions.h create mode 100644 smtk/attribute/filter/ResourceActions.h create mode 100644 smtk/attribute/filter/RuleFor.h create mode 100644 smtk/attribute/filter/StringActions.h create mode 100644 smtk/attribute/testing/cxx/unitPropertiesFilter.cxx create mode 100644 smtk/resource/filter/FloatingPointActions.h rename smtk/resource/filter/{FloatingPoint.h => FloatingPointGrammar.h} (78%) create mode 100644 smtk/resource/filter/IntegerActions.h rename smtk/resource/filter/{Integer.h => IntegerGrammar.h} (74%) create mode 100644 smtk/resource/filter/ResourceActions.h create mode 100644 smtk/resource/filter/RuleFor.h create mode 100644 smtk/resource/filter/StringActions.h rename smtk/resource/filter/{String.h => StringGrammar.h} (63%) create mode 100644 smtk/resource/filter/VectorActions.h rename smtk/resource/filter/{Vector.h => VectorGrammar.h} (55%) diff --git a/data/attribute/attribute_collection/propertiesFilterExample.sbt b/data/attribute/attribute_collection/propertiesFilterExample.sbt new file mode 100644 index 000000000..d73300c76 --- /dev/null +++ b/data/attribute/attribute_collection/propertiesFilterExample.sbt @@ -0,0 +1,26 @@ + + + + + + 100 + cat + 3.141 + + + + + 200 + dog + + + + + + + 500 + + + + + diff --git a/doc/release/notes/attributeChanges.rst b/doc/release/notes/attributeChanges.rst index d634d68c5..5aaebc44b 100644 --- a/doc/release/notes/attributeChanges.rst +++ b/doc/release/notes/attributeChanges.rst @@ -38,4 +38,17 @@ are not convertible to the item's units, the assignment will now fail. Also if a double item's units are different from its expression, the expression's evaluated value is converted to those of the item when calling its value methods. -See smtk/attribute/testing/c++/unitInfixExpressionEvaluator and data/attribute/DoubleItemExample.sbt for examples. +See smtk/attribute/testing/c++/unitInfixExpressionEvaluator.cxx and data/attribute/DoubleItemExample.sbt for examples. + +Expanded Support For Property Filtering +--------------------------------------- + +Properties on Definitions can now be *inherited* by Attributes and derived Definitions when creating queries and association rules. + +For example if Definition **A** has a floating-point property "alpha" with value 5.0, and if Definition **B** is derived from **A** and Attribute **a** is from **A** and Attribute **b** is from **B**, then all would match the rule "any[ floating-point { 'alpha' = 5.0 }]". If later **B** was to also have floating-point property "alpha" with value 10.0 associated with it, it would override the value coming from **A** so both it and **b** would no longer pass the rule. Similarly, properties on Attributes can override the values coming from its Definition. + +Also implemented differentiation between attributes and definitions. If the rule starts with *attribute* then only attributes have the possibility of matching the rest of the rule. Similarly, if the rule starts with *definition* then only definitions have the possibility of matching the rest of the rule. If the rule starts with *any* or * then either attributes or definitions are allowed. + +**Note** that the ``properties()`` methods on attribute and definition objects do not return *inherited* values but instead only those values local to the object. In the future, we may add methods to interact directly with inherited properties but for now only filter-strings process inherited properties. + +See smtk/attribute/testing/c++/unitPropertiesFilter.cxx and data/attribute/propertiesFilterExample.sbt for examples. diff --git a/smtk/attribute/ReferenceItem.cxx b/smtk/attribute/ReferenceItem.cxx index 4118e8426..e26097efd 100644 --- a/smtk/attribute/ReferenceItem.cxx +++ b/smtk/attribute/ReferenceItem.cxx @@ -1005,7 +1005,7 @@ Item::Status ReferenceItem::assign( } if (!def->isValueValid(val)) { - reason << " Definition says the value is not valid\n"; + reason << def->validityCheck(val); } auto refAtt = m_referencedAttribute.lock(); if (refAtt == nullptr) diff --git a/smtk/attribute/ReferenceItemDefinition.cxx b/smtk/attribute/ReferenceItemDefinition.cxx index 84e45f7ef..3e13a16cc 100644 --- a/smtk/attribute/ReferenceItemDefinition.cxx +++ b/smtk/attribute/ReferenceItemDefinition.cxx @@ -117,6 +117,27 @@ bool ReferenceItemDefinition::isValueValid(resource::ConstPersistentObjectPtr en return ok; } +std::string ReferenceItemDefinition::validityCheck(resource::ConstPersistentObjectPtr entity) const +{ + std::string status; + if (!entity) + { + return "Entity was null\n"; + } + + const smtk::resource::Resource* rsrc; + const smtk::resource::Component* comp; + if ((rsrc = dynamic_cast(entity.get()))) + { + return "Entity was a Resource\n"; + } + else if ((comp = dynamic_cast(entity.get()))) + { + return this->componentValidityCheck(comp); + } + return "Unsupported Condition\n"; +} + std::size_t ReferenceItemDefinition::numberOfRequiredValues() const { return m_numberOfRequiredValues; @@ -376,9 +397,7 @@ bool ReferenceItemDefinition::checkComponent(const smtk::resource::Component* co // ...ask (a) if the filter explicitly rejects components, (b) if our // resource is of the right type, and (b) if its associated filter accepts // the component. - if ( - !m_onlyResources && rsrc->isOfType(acceptable.first) && - rsrc->queryOperation(acceptable.second)(*comp)) + if (rsrc->isOfType(acceptable.first) && rsrc->queryOperation(acceptable.second)(*comp)) { return this->checkCategories(comp); } @@ -387,6 +406,71 @@ bool ReferenceItemDefinition::checkComponent(const smtk::resource::Component* co return false; } +std::string ReferenceItemDefinition::componentValidityCheck( + const smtk::resource::Component* comp) const +{ + // All components are required to have resources in order to be valid. + auto rsrc = comp->resource(); + std::stringstream reason; + if (m_onlyResources || !rsrc) + { + if (m_onlyResources) + { + return "Definition only supported Resources and a Component was provided\n"; + } + else + { + return "Component had no Resource\n"; + } + } + + // For every element in the rejected filter map... + for (const auto& rejected : m_rejected) + { + // ...ask (a) if the filter explicitly rejects components, (b) if our + // resource is of the right type, and (b) if its associated filter accepts + // the component. + if (rsrc->isOfType(rejected.first) && rsrc->queryOperation(rejected.second)(*comp)) + { + reason << "Rejected due to Rule: " << rejected.first << "," << rejected.second << std::endl; + return reason.str(); + } + } + + // If there are no filter values, then we accept all components. + if (m_acceptable.empty()) + { + return "Component Passed!/n"; + } + + // For every element in the accepted filter map... + for (const auto& acceptable : m_acceptable) + { + // ...ask (a) if the filter explicitly rejects components, (b) if our + // resource is of the right type, and (b) if its associated filter accepts + // the component. + if (rsrc->isOfType(acceptable.first) && rsrc->queryOperation(acceptable.second)(*comp)) + { + if (this->checkCategories(comp)) + { + return "Component Passed!\n"; + } + else + { + return "Component Match Acceptance Rule but failed Categories Check\n"; + } + } + } + // In this case revisit the acceptable criteria + reason << "Component Failed all Acceptance Criteria: Type Info: " << rsrc->typeName() << ", " + << comp->typeName() << " Criteria: \n"; + for (const auto& acceptable : m_acceptable) + { + reason << acceptable.first << "," << acceptable.second << std::endl; + } + return reason.str(); +} + std::size_t ReferenceItemDefinition::addConditional( const std::string& resourceQuery, const std::string& componentQuery, diff --git a/smtk/attribute/ReferenceItemDefinition.h b/smtk/attribute/ReferenceItemDefinition.h index e8e64c07e..c825d6d0c 100644 --- a/smtk/attribute/ReferenceItemDefinition.h +++ b/smtk/attribute/ReferenceItemDefinition.h @@ -75,6 +75,8 @@ class SMTKCORE_EXPORT ReferenceItemDefinition : public ItemDefinition bool enforcesCategories() const { return m_enforcesCategories; } virtual bool isValueValid(resource::ConstPersistentObjectPtr entity) const; + /// Debug method that returns a description concerning the validity of the entity + std::string validityCheck(resource::ConstPersistentObjectPtr entity) const; /// Return the number of values required by this definition. std::size_t numberOfRequiredValues() const; @@ -265,6 +267,8 @@ class SMTKCORE_EXPORT ReferenceItemDefinition : public ItemDefinition void setUnitsSystem(const shared_ptr& unitsSystem) override; + /// Debug method that returns a description concerning the validity of the component + std::string componentValidityCheck(const smtk::resource::Component* comp) const; bool m_useCommonLabel; std::vector m_valueLabels; bool m_isExtensible; diff --git a/smtk/attribute/Resource.cxx b/smtk/attribute/Resource.cxx index db7827dcf..012a17d52 100644 --- a/smtk/attribute/Resource.cxx +++ b/smtk/attribute/Resource.cxx @@ -28,6 +28,8 @@ #include "smtk/resource/Metadata.h" #include "smtk/resource/filter/Filter.h" +#include "smtk/attribute/filter/ResourceActions.h" + #include "smtk/common/UUID.h" #include "smtk/common/UUIDGenerator.h" @@ -1067,7 +1069,8 @@ std::string Resource::createAttributeQuery(const std::string& defType) std::function Resource::queryOperation( const std::string& filterString) const { - return smtk::resource::filter::Filter(filterString); + return smtk::resource::filter:: + Filter(filterString); } // visit all components in the resource. diff --git a/smtk/attribute/filter/Action.h b/smtk/attribute/filter/Action.h new file mode 100644 index 000000000..05d10f099 --- /dev/null +++ b/smtk/attribute/filter/Action.h @@ -0,0 +1,205 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_filter_Action_h +#define smtk_attribute_filter_Action_h + +#include "smtk/resource/filter/Property.h" +#include "smtk/resource/filter/Rules.h" + +#include "smtk/attribute/Attribute.h" +#include "smtk/attribute/Definition.h" + +#include "smtk/Regex.h" + +SMTK_THIRDPARTY_PRE_INCLUDE +#include "tao/pegtl.hpp" +SMTK_THIRDPARTY_POST_INCLUDE + +#include + +namespace smtk +{ +namespace attribute +{ +namespace filter +{ + +using namespace tao::pegtl; + +/// A base class template for processing PEGTL rules. A specialization of this +/// class template must exist for each PEGTL rule to be processed. +template +struct Action : nothing +{ +}; + +/// PEGTL requires that a template class be declared with a specialization for +/// each PEGTL rule to be processed. SMTK's Property-based grammar uses a common +/// syntax for each property type, making it practical for us to describe generic +/// Actions templated over the property type. We then link these actions +/// together with their respective rules using inheritance (as is PEGTL's wont). + +/// Construct a new filter rule specific to a given type. +template class RuleClass> +struct TypeNameAction +{ + template + static void apply(const Input&, smtk::resource::filter::Rules& rules) + { + rules.emplace_back(new RuleClass()); + } +}; + +/// Append the filter rule with a means of discriminating property keys to match +/// the rule input. +template class RuleClass> +struct NameAction +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::string name = input.string(); + static_cast*>(rule.get())->acceptableKeys = + [name](const smtk::resource::PersistentObject& object) -> std::vector { + std::vector returnValue; + if (object.properties().contains(name)) + { + returnValue.push_back(name); + } + else + { + // if the object is an attribute we need to check its definitions, or + // if the object is definition, we need to check the definitions it is based on + // If the object is an attribute then check its Definitions + smtk::attribute::DefinitionPtr def; + + const auto* att = dynamic_cast(&object); + if (att) + { + def = att->definition(); + } + else + { + const auto* attDef = dynamic_cast(&object); + if (attDef) + { + def = attDef->baseDefinition(); + } + } + if (def) + { + for (; def != nullptr; def = def->baseDefinition()) + { + if (def->properties().contains(name)) + { + returnValue.push_back(name); + break; + } + } + } + } + return returnValue; + }; + } +}; + +/// Append the filter rule with a means of discriminating property keys to match +/// the rule input. +template class RuleClass> +struct RegexAction +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + smtk::regex regex(input.string()); + static_cast*>(rule.get())->acceptableKeys = + [regex](const smtk::resource::PersistentObject& object) -> std::vector { + std::vector returnValue; + for (const auto& key : object.properties().get().keys()) + { + if (smtk::regex_match(key, regex)) + { + returnValue.push_back(key); + } + } + // if the object is an attribute we need to check its definitions, or + // if the object is definition, we need to check the definitions it is based on + // If the object is an attribute then check its Definitions + smtk::attribute::DefinitionPtr def; + + const auto* att = dynamic_cast(&object); + if (att) + { + def = att->definition(); + } + else + { + const auto* attDef = dynamic_cast(&object); + if (attDef) + { + def = attDef->baseDefinition(); + } + } + if (def) + { + for (; def != nullptr; def = def->baseDefinition()) + { + for (const auto& key : def->properties().get().keys()) + { + if (smtk::regex_match(key, regex)) + { + returnValue.push_back(key); + } + } + } + } + return returnValue; + }; + } +}; + +/// Append the filter rule with a means of discriminating property values to +/// match the rule input. +template class RuleClass> +struct ValueAction +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + Type value = smtk::resource::filter::Property::convert(input.string()); + static_cast*>(rule.get())->acceptableValue = [value](const Type& val) -> bool { + return val == value; + }; + } +}; + +/// Append the filter rule with a means of discriminating property values to +/// match the rule input. +template class RuleClass> +struct ValueRegexAction +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + smtk::regex regex(input.string()); + static_cast*>(rule.get())->acceptableValue = [regex](const Type& val) -> bool { + return smtk::regex_match(val, regex); + }; + } +}; +} // namespace filter +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/filter/Attribute.h b/smtk/attribute/filter/Attribute.h index c4402aa53..0bfdf215d 100644 --- a/smtk/attribute/filter/Attribute.h +++ b/smtk/attribute/filter/Attribute.h @@ -12,7 +12,8 @@ #include "smtk/attribute/Attribute.h" #include "smtk/attribute/Definition.h" -#include "smtk/resource/filter/Action.h" +#include "smtk/attribute/filter/Action.h" +#include "smtk/attribute/filter/Grammar.h" #include "smtk/resource/filter/Enclosed.h" #include "smtk/resource/filter/Name.h" #include "smtk/resource/filter/Property.h" @@ -29,6 +30,17 @@ namespace filter using namespace tao::pegtl; +struct ComponentHeader + : sor< + TAO_PEGTL_ISTRING("attribute"), + TAO_PEGTL_ISTRING("definition"), + TAO_PEGTL_ISTRING("smtk::attribute::Attribute"), + TAO_PEGTL_ISTRING("smtk::attribute::Definition"), + TAO_PEGTL_STRING("*"), + TAO_PEGTL_ISTRING("any")> +{ +}; + /// Description for grammar describing an Attribute's Definition Type specification. /// The following are valid examples: /// * type = 'foo' : Would match an Attribute whose Definition derivation contains a Definition with type() = foo @@ -120,9 +132,54 @@ class TypeRegexRule : public smtk::resource::filter::Rule std::string m_typeRegex; }; +// Rule for dealing with component header information +class ComponentHeaderRule : public smtk::resource::filter::Rule +{ +public: + ComponentHeaderRule(const std::string& componentType) + : m_componentType(componentType) + { + } + + ~ComponentHeaderRule() override = default; + + bool operator()(const smtk::resource::PersistentObject& object) const override + { + std::cerr << "Header Test: " << m_componentType << " Object: " << object.name() << std::endl; + // Allow "any" and "*" to always pass + if ((m_componentType == "any") || (m_componentType == "*")) + { + return true; + } + if ((m_componentType == "attribute") || (m_componentType == "smtk::attribute::Attribute")) + { + const auto* attribute = dynamic_cast(&object); + return (attribute != nullptr); + } + if ((m_componentType == "definition") || (m_componentType == "smtk::attribute::Definition")) + { + const auto* definition = dynamic_cast(&object); + return (definition != nullptr); + } + // Unsupported Component Type + return false; + } + std::string m_componentType; +}; + /// Actions related to parsing rules for this type. They basically insert the /// appropriate rule into the set of rules. -struct TypeNameAction +struct ComponentHeaderAction +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::string componentTypeName = input.string(); + rules.emplace_back(new ComponentHeaderRule(componentTypeName)); + } +}; + +struct AttributeTypeSpecTypeNameAction { template static void apply(const Input& input, smtk::resource::filter::Rules& rules) @@ -132,7 +189,7 @@ struct TypeNameAction } }; -struct TypeRegexAction +struct AttributeTypeSpecTypeRegexAction { template static void apply(const Input& input, smtk::resource::filter::Rules& rules) @@ -142,26 +199,20 @@ struct TypeRegexAction } }; -} // namespace filter -} // namespace attribute -/// Since the Action template class is defined in the resource::filter name space, we need be in that -/// namespace to do template specialization. -namespace resource -{ -namespace filter +template<> +struct Action : ComponentHeaderAction { +}; template<> -struct Action - : smtk::attribute::filter::TypeNameAction +struct Action : AttributeTypeSpecTypeNameAction { }; template<> -struct Action - : smtk::attribute::filter::TypeRegexAction +struct Action : AttributeTypeSpecTypeRegexAction { }; } // namespace filter -} // namespace resource +} // namespace attribute } // namespace smtk #endif diff --git a/smtk/attribute/filter/FloatingPointActions.h b/smtk/attribute/filter/FloatingPointActions.h new file mode 100644 index 000000000..fa629116a --- /dev/null +++ b/smtk/attribute/filter/FloatingPointActions.h @@ -0,0 +1,51 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_filter_FloatingPointActions_h +#define smtk_attribute_filter_FloatingPointActions_h + +#include "smtk/attribute/filter/Action.h" +#include "smtk/attribute/filter/RuleFor.h" + +#include "smtk/resource/filter/Name.h" +#include "smtk/resource/filter/Property.h" +#include "smtk/resource/filter/VectorActions.h" + +namespace smtk +{ +namespace attribute +{ +namespace filter +{ + +using namespace tao::pegtl; + +// clang-format off + +/// Actions related to parsing rules for double properties w/r the Attribute Resource +template <> struct Action::TypeName> : TypeNameAction {}; +template <> struct Action::Name> : NameAction {}; +template <> struct Action::Regex> : RegexAction {}; +template <> struct Action::Value> : ValueAction {}; + +/// Actions related to parsing rules for vectors of this type. +template <> struct Action >::TypeName> : + TypeNameAction, RuleFor > {}; +template <> struct Action >::Name> : + NameAction, RuleFor > {}; +template <> struct Action >::Regex> : + RegexAction, RuleFor > {}; +template <> struct Action >::Value> : + smtk::resource::filter::ValueAction, RuleFor > {}; +// clang-format on +} // namespace filter +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/filter/Grammar.h b/smtk/attribute/filter/Grammar.h index f5bb1559c..b09b504c0 100644 --- a/smtk/attribute/filter/Grammar.h +++ b/smtk/attribute/filter/Grammar.h @@ -14,9 +14,10 @@ #include "smtk/attribute/filter/Attribute.h" -#include "smtk/resource/filter/FloatingPoint.h" -#include "smtk/resource/filter/Integer.h" -#include "smtk/resource/filter/String.h" +#include "smtk/resource/filter/FloatingPointGrammar.h" +#include "smtk/resource/filter/IntegerGrammar.h" +#include "smtk/resource/filter/StringGrammar.h" +#include "smtk/resource/filter/VectorGrammar.h" namespace smtk { @@ -30,7 +31,7 @@ namespace filter /// Definition type information. struct SMTKCORE_EXPORT Grammar : seq< - sor, + ComponentHeader, opt struct Action::TypeName> : TypeNameAction {}; +template <> struct Action::Name> : NameAction {}; +template <> struct Action::Regex> : RegexAction {}; +template <> struct Action::Value> : ValueAction {}; + +/// Actions related to parsing rules for vectors of this type. +template <> struct Action>::TypeName> : + TypeNameAction, RuleFor > {}; +template <> struct Action >::Name> : + NameAction, RuleFor > {}; +template <> struct Action >::Regex> : + RegexAction, RuleFor > {}; +template <> struct Action >::Value> : + smtk::resource::filter::ValueAction, RuleFor > {}; +// clang-format on +} // namespace filter +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/filter/ResourceActions.h b/smtk/attribute/filter/ResourceActions.h new file mode 100644 index 000000000..6369f3365 --- /dev/null +++ b/smtk/attribute/filter/ResourceActions.h @@ -0,0 +1,19 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_filter_AttributeActions_h +#define smtk_attribute_filter_AttributeActions_h + +#include "smtk/attribute/filter/Action.h" +#include "smtk/attribute/filter/Attribute.h" +#include "smtk/attribute/filter/FloatingPointActions.h" +#include "smtk/attribute/filter/IntegerActions.h" +#include "smtk/attribute/filter/StringActions.h" + +#endif diff --git a/smtk/attribute/filter/RuleFor.h b/smtk/attribute/filter/RuleFor.h new file mode 100644 index 000000000..6c4527eac --- /dev/null +++ b/smtk/attribute/filter/RuleFor.h @@ -0,0 +1,127 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_filter_RuleFor_h +#define smtk_attribute_filter_RuleFor_h + +#include "smtk/attribute/Attribute.h" +#include "smtk/attribute/Definition.h" + +#include "smtk/resource/PersistentObject.h" +#include "smtk/resource/filter/Rule.h" + +#include + +namespace smtk +{ +namespace attribute +{ +namespace filter +{ + +///\brief Filter Grammar Rules for dealing with filtering Attributes and Definitions +/// +/// A class template based on smtk::resource::filter::RuleFor. This implementation +/// provides support for rules dealing with a specific property type that take into +/// consideration the relationship between Attributes and Definitions. For filtering +/// purposes an attribute will inherit properties from it Definition in terms of names and +/// values. An Attribute will override a value for a particular property type/name if it +/// explicitly as the same property name and type. Similarity, a Definition will inherit +/// properties from its Base Definition. +template +class RuleFor : public smtk::resource::filter::Rule +{ +public: + RuleFor() + : acceptableKeys( + [](const smtk::resource::PersistentObject&) { return std::vector(); }) + , acceptableValue([](const Type&) { return true; }) + { + } + + ~RuleFor() override = default; + + bool operator()(const smtk::resource::PersistentObject& object) const override + { + auto acceptable = acceptableKeys(object); + // First check to see if the object itself matches the criteria + for (auto it = acceptable.begin(); it != acceptable.end();) + { + if (object.properties().contains(*it)) + { + if (acceptableValue(object.properties().at(*it))) + { + return true; + } + // Else remove the key since this key overrides anything coming from + // its definitions + it = acceptable.erase(it); + } + else + { + // skip it + ++it; + } + } + // Ok since the object itself didn't match the criteria, lets test its definitions + smtk::attribute::DefinitionPtr def; + + const auto* att = dynamic_cast(&object); + if (att) + { + def = att->definition(); + } + else + { + const auto* attDef = dynamic_cast(&object); + if (attDef) + { + def = attDef->baseDefinition(); + } + else + { + return false; + } + } + for (; def != nullptr; def = def->baseDefinition()) + { + for (auto it = acceptable.begin(); it != acceptable.end();) + { + if (def->properties().contains(*it)) + { + if (acceptableValue(def->properties().at(*it))) + { + return true; + } + // Else remove the key since this key overrides anything coming from + // its base definition + it = acceptable.erase(it); + } + else + { + // skip it + ++it; + } + } + } + return false; + } + + // Given a persistent object, return a vector of keys that match the + // name filter. + std::function(const smtk::resource::PersistentObject&)> acceptableKeys; + + // Given a value, determine whether this passes the filter. + std::function acceptableValue; +}; +} // namespace filter +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/filter/StringActions.h b/smtk/attribute/filter/StringActions.h new file mode 100644 index 000000000..066e9b673 --- /dev/null +++ b/smtk/attribute/filter/StringActions.h @@ -0,0 +1,132 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_attribute_filter_StringActions_h +#define smtk_attribute_filter_StringActions_h + +#include "smtk/attribute/filter/Action.h" +#include "smtk/attribute/filter/RuleFor.h" +#include "smtk/resource/filter/Name.h" +#include "smtk/resource/filter/StringGrammar.h" +#include "smtk/resource/filter/VectorActions.h" + +#include "smtk/Regex.h" + +namespace smtk +{ +namespace attribute +{ +namespace filter +{ + +using namespace tao::pegtl; + +// clang-format off + +/// Actions related to parsing rules for string properties w/r the Attribute Resource +template <> struct Action::TypeName> : TypeNameAction {}; +template <> struct Action::Name> : NameAction {}; +template <> struct Action::Regex> : RegexAction {}; +template <> struct Action::Value> : ValueAction {}; +template <> struct Action::ValueRegex> : ValueRegexAction {}; + + +/// Actions related to parsing rules for this type. +template <> struct Action >::TypeName> + : TypeNameAction, RuleFor > {}; +template <> struct Action >::Name> + : NameAction, RuleFor > {}; +template <> struct Action >::Regex> + : RegexAction, RuleFor > {}; +// clang-format on + +/// Specialization of ValueAction to accommodate vectors of strings. +/// +/// TODO: There should be a way to reuse the original implementations for +/// the following Actions from smtk::resource::filter +/// +template<> +struct Action>::Value> +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::vector value; + smtk::regex re(","); + std::string in = input.string(); + std::sregex_token_iterator it(in.begin(), in.end(), re, -1), last; + for (int id = 0; it != last; ++it, ++id) + { + std::string str = it->str(); + str = str.substr(1 + str.find_first_of('\'')); + str = str.substr(0, str.size() - 1); + value.push_back(str); + } + + static_cast>*>(rule.get())->acceptableValue = + [value](const std::vector& val) -> bool { + if (val.size() != value.size()) + { + return false; + } + for (std::size_t i = 0; i < value.size(); ++i) + { + if (val[i] != value[i]) + { + return false; + } + } + return true; + }; + } +}; + +/// Specialization of ValueRegexAction to accommodate vectors of strings. +template<> +struct Action>::ValueRegex> +{ + template + static void apply(const Input& input, smtk::resource::filter::Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::vector regex; + + smtk::regex re(","); + std::sregex_token_iterator it(input.string().begin(), input.string().end(), re, -1), last; + for (int id = 0; it != last; ++it, ++id) + { + std::string str = it->str(); + str = str.substr(1 + str.find_first_of('/')); + str = str.substr(0, str.size() - 1); + regex.emplace_back(str.c_str()); + } + + static_cast>*>(rule.get())->acceptableValue = + [regex](const std::vector& val) -> bool { + if (val.size() != regex.size()) + { + return false; + } + for (std::size_t i = 0; i < regex.size(); ++i) + { + if (!smtk::regex_match(val[i], regex[i])) + { + return false; + } + } + return true; + }; + } +}; +} // namespace filter +} // namespace attribute +} // namespace smtk + +#endif diff --git a/smtk/attribute/testing/cxx/CMakeLists.txt b/smtk/attribute/testing/cxx/CMakeLists.txt index a72b0056b..52bc5a345 100644 --- a/smtk/attribute/testing/cxx/CMakeLists.txt +++ b/smtk/attribute/testing/cxx/CMakeLists.txt @@ -114,6 +114,7 @@ set(unit_tests_which_require_data unitAnalysisConfigurations.cxx unitConditionalGroup.cxx unitItemBlocks.cxx + unitPropertiesFilter.cxx unitTemplates.cxx unitXmlReaderProperties.cxx ) diff --git a/smtk/attribute/testing/cxx/unitAttributeAssociation.cxx b/smtk/attribute/testing/cxx/unitAttributeAssociation.cxx index 7649a4b28..37a24714a 100644 --- a/smtk/attribute/testing/cxx/unitAttributeAssociation.cxx +++ b/smtk/attribute/testing/cxx/unitAttributeAssociation.cxx @@ -110,7 +110,7 @@ int unitAttributeAssociation(int /*unused*/, char* /*unused*/[]) modelMgr = model::Resource::create(); smtkTest( associateOperation->parameters()->findResource("associate to") != nullptr, - "Cannot access associate opration's input resource parameter."); + "Cannot access associate operation's input resource parameter."); associateOperation->parameters()->findResource("associate to")->setValue(modelMgr); auto result = associateOperation->operate(); @@ -133,7 +133,7 @@ int unitAttributeAssociation(int /*unused*/, char* /*unused*/[]) smtkTest( dissociateOperation->parameters()->findResource("dissociate from") != nullptr, - "Cannot access dissociate opration's input resource parameter."); + "Cannot access dissociate operation's input resource parameter."); dissociateOperation->parameters()->findResource("dissociate from")->setValue(modelMgr); smtkTest(dissociateOperation->ableToOperate() == true, "Dissociate operator cannot operate"); diff --git a/smtk/attribute/testing/cxx/unitPropertiesFilter.cxx b/smtk/attribute/testing/cxx/unitPropertiesFilter.cxx new file mode 100644 index 000000000..4629a1f7a --- /dev/null +++ b/smtk/attribute/testing/cxx/unitPropertiesFilter.cxx @@ -0,0 +1,138 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#include "smtk/attribute/Attribute.h" +#include "smtk/attribute/Resource.h" +#include "smtk/io/AttributeReader.h" +#include "smtk/io/Logger.h" + +#include "smtk/common/testing/cxx/helpers.h" + +using namespace smtk::attribute; +using namespace smtk::common; +using namespace smtk; + +namespace +{ +const double double_epsilon = 1.e-10; +} + +int unitPropertiesFilter(int /*unused*/, char* /*unused*/[]) +{ + // Read in the test template + std::string attFile; + attFile = SMTK_DATA_DIR; + attFile += "/attribute/attribute_collection/propertiesFilterExample.sbt"; + io::AttributeReader reader; + io::Logger logger; + auto attRes = attribute::Resource::create(); + reader.read(attRes, attFile, logger); + if (logger.hasErrors()) + { + std::cerr << "Errors Generated when reading SBT file :\n" << logger.convertToString(); + return -1; + } + std::cerr << std::boolalpha; // To print out booleans + + // Lets find the definitions and attributes + + auto defBase = attRes->findDefinition("Base"); + auto defA = attRes->findDefinition("A"); + auto a = attRes->findAttribute("a"); + auto a1 = attRes->findAttribute("a1"); + + smtkTest(defBase != nullptr, "Could not find Definition Base"); + smtkTest(defA != nullptr, "Could not find Definition A"); + smtkTest(a != nullptr, "Could not find Attribute a"); + smtkTest(a1 != nullptr, "Could not find Attribute a1"); + + // Supported Component Headers include: *, any, attribute, smtk::attribute::Attribute, + // and smtk::attribute::Definition + auto filter = attRes->queryOperation("any[ int { 'alpha' }]"); + // All Definitions and Attributes should be considered having a property call alpha + smtkTest(filter(*defBase), "Definition Base did not have property alpha"); + smtkTest(filter(*defA), "Definition A did not have property alpha"); + smtkTest(filter(*a), "Attribute a did not have property alpha"); + smtkTest(filter(*a1), "Attribute a1 did not have property alpha"); + std::cerr << "Passed Alpha Test\n"; + + filter = attRes->queryOperation("definition[ int { 'alpha' }]"); + // All Definitions and Attributes should be considered having a property call alpha + smtkTest(filter(*defBase), "Definition Base did not have property alpha"); + smtkTest(filter(*defA), "Definition A did not have property alpha"); + smtkTest(!filter(*a), "Attribute a passed definition only test"); + smtkTest(!filter(*a1), "Attribute a1 passed definition only test"); + std::cerr << "Passed Alpha Test - Definition Only\n"; + + filter = attRes->queryOperation("smtk::attribute::Attribute[ int { 'alpha' }]"); + // All Definitions and Attributes should be considered having a property call alpha + smtkTest(!filter(*defBase), "Definition Base passed attribute only test"); + smtkTest(!filter(*defA), "Definition A passed attribute only test"); + smtkTest(filter(*a), "Attribute a did not have property alpha"); + smtkTest(filter(*a1), "Attribute a1 did not have property alpha"); + std::cerr << "Passed Alpha Test - Attribute Only\n"; + + filter = attRes->queryOperation("any[ int { 'alpha' = 100 }]"); + // Only the Base should pass this test since everyone else redefines alpha + smtkTest(filter(*defBase), "Definition Base did not have property alpha = 100"); + smtkTest(!filter(*defA), "Definition A did have property alpha = 100 but shouldn't have"); + smtkTest(!filter(*a), "Attribute a did have property alpha = 100 but shouldn't have"); + smtkTest(!filter(*a1), "Attribute a1 did have property alpha = 100 but shouldn't have"); + std::cerr << "Passed Alpha = 100 Test\n"; + + filter = attRes->queryOperation("any[ int { 'alpha' = 200 }]"); + // A and a1 should pass + smtkTest(!filter(*defBase), "Definition Base did have property alpha = 200 but shouldn't have"); + smtkTest(filter(*defA), "Definition A did not have property alpha = 200"); + smtkTest(!filter(*a), "Attribute a did have property alpha = 200 but shouldn't have"); + smtkTest(filter(*a1), "Attribute a1 did not have property alpha = 200"); + std::cerr << "Passed Alpha = 200 Test\n"; + + filter = attRes->queryOperation("any[ int { 'alpha' = 500 }]"); + // only Attribute a should pass + smtkTest(!filter(*defBase), "Definition Base did have property alpha = 500 but shouldn't have"); + smtkTest(!filter(*defA), "Definition A did have property alpha = 500 but shouldn't have"); + smtkTest(filter(*a), "Attribute a did not have property alpha = 500"); + smtkTest(!filter(*a1), "Attribute a1 did have property alpha = 500 but shouldn't have"); + std::cerr << "Passed Alpha = 500 Test\n"; + + filter = attRes->queryOperation("any[ string { 'beta' = 'cat' }]"); + // Only the Base should pass this test since everyone else redefines beta + smtkTest(filter(*defBase), "Definition Base did not have property beta = cat"); + smtkTest(!filter(*defA), "Definition A did have property beta = cat but shouldn't have"); + smtkTest(!filter(*a), "Attribute a did have property beta = cat but shouldn't have"); + smtkTest(!filter(*a1), "Attribute a1 did have property beta = cat but shouldn't have"); + std::cerr << "Passed Beta = cat Test\n"; + + filter = attRes->queryOperation("any[ string { 'beta' = 'dog' }]"); + // Everything except Base should pass + smtkTest(!filter(*defBase), "Definition Base did have property beta = dog but shouldn't have"); + smtkTest(filter(*defA), "Definition A did not have property beta = dog"); + smtkTest(filter(*a), "Attribute a did not have property beta = dog"); + smtkTest(filter(*a1), "Attribute a1 did not have property beta = dog"); + std::cerr << "Passed Beta = dog Test\n"; + + filter = attRes->queryOperation("any[ floating-point { 'gamma' = 3.141 }]"); + // Everything should pass since they all get the value from the Base Def + smtkTest(filter(*defBase), "Definition Base did not have property gamma = 3.141"); + smtkTest(filter(*defA), "Definition A did not have property gamma = 3.141"); + smtkTest(filter(*a), "Attribute a did not have property gamma = 3.141"); + smtkTest(filter(*a1), "Attribute a1 did not have property gamma = 3.141"); + std::cerr << "Passed Gamma = 3.141 Test\n"; + + filter = attRes->queryOperation("any[ floating-point { 'delta' }]"); + // Everything should fail since this property does not exist. + smtkTest(!filter(*defBase), "Definition Base did have property delta but shouldn't have"); + smtkTest(!filter(*defA), "Definition A did have property delta but shouldn't have"); + smtkTest(!filter(*a), "Attribute a did have property delta but shouldn't have"); + smtkTest(!filter(*a1), "Attribute a1 did have property delta but shouldn't have"); + std::cerr << "Passed Delta Test\n"; + + return 0; +} diff --git a/smtk/graph/Resource.h b/smtk/graph/Resource.h index 8a6e6cde1..ca52cdac7 100644 --- a/smtk/graph/Resource.h +++ b/smtk/graph/Resource.h @@ -28,6 +28,7 @@ #include "smtk/resource/CopyOptions.h" #include "smtk/resource/filter/Filter.h" +#include "smtk/resource/filter/ResourceActions.h" #include #include @@ -452,10 +453,12 @@ std::shared_ptr Resource::clone( std::shared_ptr result; if (auto rsrcMgr = this->manager()) { + std::cerr << "Clone: Asking Resource Manager\n"; result = std::dynamic_pointer_cast(rsrcMgr->create(this->typeName())); } if (!result) { + std::cerr << "Clone: Creating it myself\n"; result = Resource::create(); } if (!result) diff --git a/smtk/io/XmlPropertyParsingHelper.txx b/smtk/io/XmlPropertyParsingHelper.txx index 12c3b5cdb..2a704f5e5 100644 --- a/smtk/io/XmlPropertyParsingHelper.txx +++ b/smtk/io/XmlPropertyParsingHelper.txx @@ -86,6 +86,11 @@ void processProperties(T& object, xml_node& propertiesNode, smtk::io::Logger& lo { object->properties().template get()[propName] = propNode.text().as_int(); } + else if (propType == "long") + { + std::string v = propNode.text().as_string(); + object->properties().template get()[propName] = std::stol(v); + } else if (propType == "double") { object->properties().template get()[propName] = propNode.text().as_double(); diff --git a/smtk/project/Project.cxx b/smtk/project/Project.cxx index 574660a84..3692730f8 100644 --- a/smtk/project/Project.cxx +++ b/smtk/project/Project.cxx @@ -13,6 +13,7 @@ #include "smtk/resource/Manager.h" #include "smtk/resource/filter/Filter.h" +#include "smtk/resource/filter/ResourceActions.h" #include "smtk/task/Instances.h" #include "smtk/task/Manager.h" diff --git a/smtk/resource/CMakeLists.txt b/smtk/resource/CMakeLists.txt index 9ed92beb9..ea3e64e75 100644 --- a/smtk/resource/CMakeLists.txt +++ b/smtk/resource/CMakeLists.txt @@ -50,15 +50,21 @@ set(resourceHeaders filter/Action.h filter/Enclosed.h filter/Filter.h - filter/FloatingPoint.h + filter/FloatingPointActions.h + filter/FloatingPointGrammar.h filter/Grammar.h - filter/Integer.h + filter/IntegerActions.h + filter/IntegerGrammar.h filter/Name.h filter/Property.h + filter/ResourceActions.h filter/Rule.h + filter/RuleFor.h filter/Rules.h - filter/String.h - filter/Vector.h + filter/StringActions.h + filter/StringGrammar.h + filter/VectorActions.h + filter/VectorGrammar.h json/Helper.h json/jsonComponentLinkBase.h json/jsonPropertyCoordinateFrame.h diff --git a/smtk/resource/Resource.cxx b/smtk/resource/Resource.cxx index 54a74c87a..361deccd7 100644 --- a/smtk/resource/Resource.cxx +++ b/smtk/resource/Resource.cxx @@ -17,6 +17,7 @@ #include "smtk/resource/Manager.h" #include "smtk/resource/filter/Filter.h" +#include "smtk/resource/filter/ResourceActions.h" #include "smtk/common/Paths.h" #include "smtk/common/TypeName.h" diff --git a/smtk/resource/filter/Action.h b/smtk/resource/filter/Action.h index aa86a3597..e55924149 100644 --- a/smtk/resource/filter/Action.h +++ b/smtk/resource/filter/Action.h @@ -44,19 +44,19 @@ struct Action : nothing /// together with their respective rules using inheritance (as is PEGTL's wont). /// Construct a new filter rule specific to a given type. -template +template class RuleClass> struct TypeNameAction { template static void apply(const Input&, Rules& rules) { - rules.emplace_back(new RuleFor()); + rules.emplace_back(new RuleClass()); } }; /// Append the filter rule with a means of discriminating property keys to match /// the rule input. -template +template class RuleClass> struct NameAction { template @@ -64,7 +64,7 @@ struct NameAction { std::unique_ptr& rule = rules.data().back(); std::string name = input.string(); - static_cast*>(rule.get())->acceptableKeys = + static_cast*>(rule.get())->acceptableKeys = [name](const PersistentObject& object) -> std::vector { std::vector returnValue; if (object.properties().contains(name)) @@ -78,7 +78,7 @@ struct NameAction /// Append the filter rule with a means of discriminating property keys to match /// the rule input. -template +template class RuleClass> struct RegexAction { template @@ -86,7 +86,7 @@ struct RegexAction { std::unique_ptr& rule = rules.data().back(); smtk::regex regex(input.string()); - static_cast*>(rule.get())->acceptableKeys = + static_cast*>(rule.get())->acceptableKeys = [regex](const PersistentObject& object) -> std::vector { std::vector returnValue; for (const auto& key : object.properties().get().keys()) @@ -103,7 +103,7 @@ struct RegexAction /// Append the filter rule with a means of discriminating property values to /// match the rule input. -template +template class RuleClass> struct ValueAction { template @@ -111,7 +111,7 @@ struct ValueAction { std::unique_ptr& rule = rules.data().back(); Type value = Property::convert(input.string()); - static_cast*>(rule.get())->acceptableValue = [value](const Type& val) -> bool { + static_cast*>(rule.get())->acceptableValue = [value](const Type& val) -> bool { return val == value; }; } @@ -119,7 +119,7 @@ struct ValueAction /// Append the filter rule with a means of discriminating property values to /// match the rule input. -template +template class RuleClass> struct ValueRegexAction { template @@ -127,7 +127,7 @@ struct ValueRegexAction { std::unique_ptr& rule = rules.data().back(); smtk::regex regex(input.string()); - static_cast*>(rule.get())->acceptableValue = [regex](const Type& val) -> bool { + static_cast*>(rule.get())->acceptableValue = [regex](const Type& val) -> bool { return smtk::regex_match(val, regex); }; } diff --git a/smtk/resource/filter/Filter.h b/smtk/resource/filter/Filter.h index 6424d9ee9..4bc970447 100644 --- a/smtk/resource/filter/Filter.h +++ b/smtk/resource/filter/Filter.h @@ -33,7 +33,7 @@ namespace filter /// /// Given a PEGTL grammar, smtk::resource::filter::Filter is a copyable functor /// that converts a filter string into a set of filter rules. -template +template class ActionClass = Action> class Filter { public: @@ -89,7 +89,7 @@ class Filter tao::pegtl::string_input<> in(filterString, "constructRule"); try { - tao::pegtl::parse(in, rules); + tao::pegtl::parse(in, rules); } catch (tao::pegtl::parse_error& err) { diff --git a/smtk/resource/filter/FloatingPointActions.h b/smtk/resource/filter/FloatingPointActions.h new file mode 100644 index 000000000..ac01c8f73 --- /dev/null +++ b/smtk/resource/filter/FloatingPointActions.h @@ -0,0 +1,50 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_resource_filter_FloatingPointActions_h +#define smtk_resource_filter_FloatingPointActions_h + +#include "smtk/resource/filter/Action.h" +#include "smtk/resource/filter/FloatingPointGrammar.h" +#include "smtk/resource/filter/Name.h" +#include "smtk/resource/filter/RuleFor.h" +#include "smtk/resource/filter/VectorActions.h" + +namespace smtk +{ +namespace resource +{ +namespace filter +{ + +using namespace tao::pegtl; + +// clang-format off + +/// Actions related to parsing rules for doubles. +template <> struct Action::TypeName> : TypeNameAction {}; +template <> struct Action::Name> : NameAction {}; +template <> struct Action::Regex> : RegexAction {}; +template <> struct Action::Value> : ValueAction {}; + +/// Actions related to parsing rules for vectors of this type. +template <> struct Action >::TypeName> : + TypeNameAction, RuleFor > {}; +template <> struct Action >::Name> : + NameAction, RuleFor > {}; +template <> struct Action >::Regex> : + RegexAction, RuleFor > {}; +template <> struct Action >::Value> : + ValueAction, RuleFor > {}; +// clang-format on +} // namespace filter +} // namespace resource +} // namespace smtk + +#endif diff --git a/smtk/resource/filter/FloatingPoint.h b/smtk/resource/filter/FloatingPointGrammar.h similarity index 78% rename from smtk/resource/filter/FloatingPoint.h rename to smtk/resource/filter/FloatingPointGrammar.h index 1c5f58274..551431fb3 100644 --- a/smtk/resource/filter/FloatingPoint.h +++ b/smtk/resource/filter/FloatingPointGrammar.h @@ -7,13 +7,11 @@ // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. See the above copyright notice for more information. //========================================================================= -#ifndef smtk_resource_filter_FloatingPoint_h -#define smtk_resource_filter_FloatingPoint_h +#ifndef smtk_resource_filter_FloatingPointGrammar_h +#define smtk_resource_filter_FloatingPointGrammar_h -#include "smtk/resource/filter/Action.h" #include "smtk/resource/filter/Name.h" #include "smtk/resource/filter/Property.h" -#include "smtk/resource/filter/Vector.h" namespace smtk { @@ -101,21 +99,6 @@ struct Property static double convert(const std::string& input) { return std::stod(input); } }; -/// Actions related to parsing rules for this type. -template <> struct Action::TypeName> : TypeNameAction {}; -template <> struct Action::Name> : NameAction {}; -template <> struct Action::Regex> : RegexAction {}; -template <> struct Action::Value> : ValueAction {}; - -/// Actions related to parsing rules for vectors of this type. -template <> struct Action >::TypeName> : - TypeNameAction > {}; -template <> struct Action >::Name> : - NameAction > {}; -template <> struct Action >::Regex> : - RegexAction > {}; -template <> struct Action >::Value> : - ValueAction > {}; // clang-format on } // namespace filter } // namespace resource diff --git a/smtk/resource/filter/Grammar.h b/smtk/resource/filter/Grammar.h index 5affa6ae2..261ccce96 100644 --- a/smtk/resource/filter/Grammar.h +++ b/smtk/resource/filter/Grammar.h @@ -12,9 +12,10 @@ #include "smtk/CoreExports.h" -#include "smtk/resource/filter/FloatingPoint.h" -#include "smtk/resource/filter/Integer.h" -#include "smtk/resource/filter/String.h" +#include "smtk/resource/filter/FloatingPointGrammar.h" +#include "smtk/resource/filter/IntegerGrammar.h" +#include "smtk/resource/filter/StringGrammar.h" +#include "smtk/resource/filter/VectorGrammar.h" namespace smtk { diff --git a/smtk/resource/filter/IntegerActions.h b/smtk/resource/filter/IntegerActions.h new file mode 100644 index 000000000..6487c94f3 --- /dev/null +++ b/smtk/resource/filter/IntegerActions.h @@ -0,0 +1,50 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_resource_filter_IntegerActions_h +#define smtk_resource_filter_IntegerActions_h + +#include "smtk/resource/filter/Action.h" +#include "smtk/resource/filter/IntegerGrammar.h" +#include "smtk/resource/filter/Name.h" +#include "smtk/resource/filter/RuleFor.h" +#include "smtk/resource/filter/VectorActions.h" + +namespace smtk +{ +namespace resource +{ +namespace filter +{ + +using namespace tao::pegtl; + +// clang-format off + +/// Actions related to parsing rules for integers +template <> struct Action::TypeName> : TypeNameAction {}; +template <> struct Action::Name> : NameAction {}; +template <> struct Action::Regex> : RegexAction {}; +template <> struct Action::Value> : ValueAction {}; + +/// Actions related to parsing rules for vectors of this type. +template <> struct Action>::TypeName> : + TypeNameAction, RuleFor > {}; +template <> struct Action >::Name> : + NameAction, RuleFor > {}; +template <> struct Action >::Regex> : + RegexAction, RuleFor > {}; +template <> struct Action >::Value> : + ValueAction, RuleFor > {}; +// clang-format on +} // namespace filter +} // namespace resource +} // namespace smtk + +#endif diff --git a/smtk/resource/filter/Integer.h b/smtk/resource/filter/IntegerGrammar.h similarity index 74% rename from smtk/resource/filter/Integer.h rename to smtk/resource/filter/IntegerGrammar.h index 31ed5f3fc..ba71f2d92 100644 --- a/smtk/resource/filter/Integer.h +++ b/smtk/resource/filter/IntegerGrammar.h @@ -7,8 +7,8 @@ // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. See the above copyright notice for more information. //========================================================================= -#ifndef smtk_resource_filter_Integer_h -#define smtk_resource_filter_Integer_h +#ifndef smtk_resource_filter_IntegerGrammar_h +#define smtk_resource_filter_IntegerGrammar_h #include "smtk/resource/filter/Name.h" #include "smtk/resource/filter/Property.h" @@ -70,21 +70,6 @@ struct Property static long convert(const std::string& input) { return std::stol(input); } }; -/// Actions related to parsing rules for this type. -template <> struct Action::TypeName> : TypeNameAction {}; -template <> struct Action::Name> : NameAction {}; -template <> struct Action::Regex> : RegexAction {}; -template <> struct Action::Value> : ValueAction {}; - -/// Actions related to parsing rules for vectors of this type. -template <> struct Action >::TypeName> : - TypeNameAction > {}; -template <> struct Action >::Name> : - NameAction > {}; -template <> struct Action >::Regex> : - RegexAction > {}; -template <> struct Action >::Value> : - ValueAction > {}; // clang-format on } // namespace filter } // namespace resource diff --git a/smtk/resource/filter/ResourceActions.h b/smtk/resource/filter/ResourceActions.h new file mode 100644 index 000000000..67bcd3945 --- /dev/null +++ b/smtk/resource/filter/ResourceActions.h @@ -0,0 +1,18 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_resource_filter_ResourceActions_h +#define smtk_resource_filter_ResourceActions_h + +#include "smtk/resource/filter/Action.h" +#include "smtk/resource/filter/FloatingPointActions.h" +#include "smtk/resource/filter/IntegerActions.h" +#include "smtk/resource/filter/StringActions.h" + +#endif diff --git a/smtk/resource/filter/Rule.h b/smtk/resource/filter/Rule.h index 80238c565..889ca26ed 100644 --- a/smtk/resource/filter/Rule.h +++ b/smtk/resource/filter/Rule.h @@ -30,35 +30,6 @@ class Rule virtual bool operator()(const PersistentObject&) const = 0; }; -/// A class template for rules dealing with a specific property type. -template -class RuleFor : public Rule -{ -public: - RuleFor() - : acceptableKeys([](const PersistentObject&) { return std::vector(); }) - , acceptableValue([](const Type&) { return true; }) - { - } - - ~RuleFor() override = default; - - bool operator()(const PersistentObject& object) const override - { - auto acceptable = acceptableKeys(object); - return std::any_of( - acceptable.begin(), acceptable.end(), [this, &object](const std::string& key) { - return acceptableValue(object.properties().at(key)); - }); - } - - // Given a persistent object, return a vector of keys that match the - // name filter. - std::function(const PersistentObject&)> acceptableKeys; - - // Given a value, determine whether this passes the filter. - std::function acceptableValue; -}; } // namespace filter } // namespace resource } // namespace smtk diff --git a/smtk/resource/filter/RuleFor.h b/smtk/resource/filter/RuleFor.h new file mode 100644 index 000000000..ec91e05dd --- /dev/null +++ b/smtk/resource/filter/RuleFor.h @@ -0,0 +1,58 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_resource_filter_RuleFor_h +#define smtk_resource_filter_RuleFor_h + +#include "smtk/resource/PersistentObject.h" +#include "smtk/resource/filter/Rule.h" + +#include + +namespace smtk +{ +namespace resource +{ +namespace filter +{ + +/// A class template for rules dealing with a specific property type. +template +class RuleFor : public Rule +{ +public: + RuleFor() + : acceptableKeys([](const PersistentObject&) { return std::vector(); }) + , acceptableValue([](const Type&) { return true; }) + { + } + + ~RuleFor() override = default; + + bool operator()(const PersistentObject& object) const override + { + auto acceptable = acceptableKeys(object); + return std::any_of( + acceptable.begin(), acceptable.end(), [this, &object](const std::string& key) { + return acceptableValue(object.properties().at(key)); + }); + } + + // Given a persistent object, return a vector of keys that match the + // name filter. + std::function(const PersistentObject&)> acceptableKeys; + + // Given a value, determine whether this passes the filter. + std::function acceptableValue; +}; +} // namespace filter +} // namespace resource +} // namespace smtk + +#endif diff --git a/smtk/resource/filter/StringActions.h b/smtk/resource/filter/StringActions.h new file mode 100644 index 000000000..9e92c9da0 --- /dev/null +++ b/smtk/resource/filter/StringActions.h @@ -0,0 +1,128 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_resource_filter_StringActions_h +#define smtk_resource_filter_StringActions_h + +#include "smtk/resource/filter/Action.h" +#include "smtk/resource/filter/Name.h" +#include "smtk/resource/filter/RuleFor.h" +#include "smtk/resource/filter/StringGrammar.h" +#include "smtk/resource/filter/VectorActions.h" + +#include "smtk/Regex.h" + +namespace smtk +{ +namespace resource +{ +namespace filter +{ + +using namespace tao::pegtl; + +// clang-format off + +/// Actions related to parsing rules for strings. +template <> struct Action::TypeName> : TypeNameAction {}; +template <> struct Action::Name> : NameAction {}; +template <> struct Action::Regex> : RegexAction {}; +template <> struct Action::Value> : ValueAction {}; +template <> struct Action::ValueRegex> : ValueRegexAction {}; + + +/// Actions related to parsing rules for this type. +template <> struct Action >::TypeName> + : TypeNameAction, RuleFor > {}; +template <> struct Action >::Name> + : NameAction, RuleFor > {}; +template <> struct Action >::Regex> + : RegexAction, RuleFor > {}; +// clang-format on + +/// Specialization of ValueAction to accommodate vectors of strings. +template<> +struct Action>::Value> +{ + template + static void apply(const Input& input, Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::vector value; + smtk::regex re(","); + std::string in = input.string(); + std::sregex_token_iterator it(in.begin(), in.end(), re, -1), last; + for (int id = 0; it != last; ++it, ++id) + { + std::string str = it->str(); + str = str.substr(1 + str.find_first_of('\'')); + str = str.substr(0, str.size() - 1); + value.push_back(str); + } + + static_cast>*>(rule.get())->acceptableValue = + [value](const std::vector& val) -> bool { + if (val.size() != value.size()) + { + return false; + } + for (std::size_t i = 0; i < value.size(); ++i) + { + if (val[i] != value[i]) + { + return false; + } + } + return true; + }; + } +}; + +/// Specialization of ValueRegexAction to accommodate vectors of strings. +template<> +struct Action>::ValueRegex> +{ + template + static void apply(const Input& input, Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::vector regex; + + smtk::regex re(","); + std::sregex_token_iterator it(input.string().begin(), input.string().end(), re, -1), last; + for (int id = 0; it != last; ++it, ++id) + { + std::string str = it->str(); + str = str.substr(1 + str.find_first_of('/')); + str = str.substr(0, str.size() - 1); + regex.emplace_back(str.c_str()); + } + + static_cast>*>(rule.get())->acceptableValue = + [regex](const std::vector& val) -> bool { + if (val.size() != regex.size()) + { + return false; + } + for (std::size_t i = 0; i < regex.size(); ++i) + { + if (!smtk::regex_match(val[i], regex[i])) + { + return false; + } + } + return true; + }; + } +}; +} // namespace filter +} // namespace resource +} // namespace smtk + +#endif diff --git a/smtk/resource/filter/String.h b/smtk/resource/filter/StringGrammar.h similarity index 63% rename from smtk/resource/filter/String.h rename to smtk/resource/filter/StringGrammar.h index a84dad841..9bce5942c 100644 --- a/smtk/resource/filter/String.h +++ b/smtk/resource/filter/StringGrammar.h @@ -7,8 +7,8 @@ // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. See the above copyright notice for more information. //========================================================================= -#ifndef smtk_resource_filter_String_h -#define smtk_resource_filter_String_h +#ifndef smtk_resource_filter_StringGrammar_h +#define smtk_resource_filter_StringGrammar_h #include "smtk/resource/filter/Name.h" #include "smtk/resource/filter/Property.h" @@ -83,13 +83,6 @@ struct Property static const std::string& convert(const std::string& input) { return input; } }; -/// Actions related to parsing rules for this type. -template <> struct Action::TypeName> : TypeNameAction {}; -template <> struct Action::Name> : NameAction {}; -template <> struct Action::Regex> : RegexAction {}; -template <> struct Action::Value> : ValueAction {}; -template <> struct Action::ValueRegex> : ValueRegexAction {}; - /// A specialization for describing the grammar for parsing rules related to /// vector properties. This specialization is explicit to accommodate /// referencing values via regex. @@ -138,90 +131,8 @@ struct Property > space> {}; }; -/// Actions related to parsing rules for this type. -template <> struct Action >::TypeName> - : TypeNameAction > {}; -template <> struct Action >::Name> - : NameAction > {}; -template <> struct Action >::Regex> - : RegexAction > {}; // clang-format on -/// Specialization of ValueAction to accommodate vectors of types. -template<> -struct Action>::Value> -{ - template - static void apply(const Input& input, Rules& rules) - { - std::unique_ptr& rule = rules.data().back(); - std::vector value; - smtk::regex re(","); - std::string in = input.string(); - std::sregex_token_iterator it(in.begin(), in.end(), re, -1), last; - for (int id = 0; it != last; ++it, ++id) - { - std::string str = it->str(); - str = str.substr(1 + str.find_first_of('\'')); - str = str.substr(0, str.size() - 1); - value.push_back(str); - } - - static_cast>*>(rule.get())->acceptableValue = - [value](const std::vector& val) -> bool { - if (val.size() != value.size()) - { - return false; - } - for (std::size_t i = 0; i < value.size(); ++i) - { - if (val[i] != value[i]) - { - return false; - } - } - return true; - }; - } -}; - -/// Specialization of ValueRegexAction to accommodate vectors of types. -template<> -struct Action>::ValueRegex> -{ - template - static void apply(const Input& input, Rules& rules) - { - std::unique_ptr& rule = rules.data().back(); - std::vector regex; - - smtk::regex re(","); - std::sregex_token_iterator it(input.string().begin(), input.string().end(), re, -1), last; - for (int id = 0; it != last; ++it, ++id) - { - std::string str = it->str(); - str = str.substr(1 + str.find_first_of('/')); - str = str.substr(0, str.size() - 1); - regex.emplace_back(str.c_str()); - } - - static_cast>*>(rule.get())->acceptableValue = - [regex](const std::vector& val) -> bool { - if (val.size() != regex.size()) - { - return false; - } - for (std::size_t i = 0; i < regex.size(); ++i) - { - if (!smtk::regex_match(val[i], regex[i])) - { - return false; - } - } - return true; - }; - } -}; } // namespace filter } // namespace resource } // namespace smtk diff --git a/smtk/resource/filter/VectorActions.h b/smtk/resource/filter/VectorActions.h new file mode 100644 index 000000000..915781bbc --- /dev/null +++ b/smtk/resource/filter/VectorActions.h @@ -0,0 +1,102 @@ +//========================================================================= +// Copyright (c) Kitware, Inc. +// All rights reserved. +// See LICENSE.txt for details. +// +// This software is distributed WITHOUT ANY WARRANTY; without even +// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR +// PURPOSE. See the above copyright notice for more information. +//========================================================================= +#ifndef smtk_resource_filter_VectorActions_h +#define smtk_resource_filter_VectorActions_h + +#include "smtk/resource/filter/Name.h" +#include "smtk/resource/filter/Property.h" + +#include +#include + +namespace smtk +{ +namespace resource +{ +namespace filter +{ + +using namespace tao::pegtl; + +/// Specialization of ValueAction to accommodate vectors of types. +template class RuleClass> +struct ValueAction, RuleClass> +{ + template + static void apply(const Input& input, Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::vector value; + std::regex re(","); + std::string in = input.string(); + std::sregex_token_iterator it(in.begin(), in.end(), re, -1), last; + for (int id = 0; it != last; ++it, ++id) + { + std::string str = it->str(); + value.push_back(Property::convert(str)); + } + + static_cast>*>(rule.get())->acceptableValue = + [value](const std::vector& val) -> bool { + if (val.size() != value.size()) + { + return false; + } + for (std::size_t i = 0; i < value.size(); ++i) + { + if (val[i] != value[i]) + { + return false; + } + } + return true; + }; + } +}; + +/// Specialization of ValueRegexAction to accommodate vectors of types. +template class RuleClass> +struct ValueRegexAction, RuleClass> +{ + template + static void apply(const Input& input, Rules& rules) + { + std::unique_ptr& rule = rules.data().back(); + std::vector regex; + + std::regex re(","); + std::sregex_token_iterator it(input.string().begin(), input.string().end(), re, -1), last; + for (int id = 0; it != last; ++it, ++id) + { + regex.emplace_back(it->str().c_str()); + } + + static_cast>*>(rule.get())->acceptableValue = + [regex](const std::vector& val) -> bool { + if (val.size() != regex.size()) + { + return false; + } + for (std::size_t i = 0; i < regex.size(); ++i) + { + if (!std::regex_match(val[i], regex[i])) + { + return false; + } + } + return true; + }; + } +}; +} // namespace filter +} // namespace resource +} // namespace smtk + +#endif diff --git a/smtk/resource/filter/Vector.h b/smtk/resource/filter/VectorGrammar.h similarity index 55% rename from smtk/resource/filter/Vector.h rename to smtk/resource/filter/VectorGrammar.h index 37a30d252..d1320daf9 100644 --- a/smtk/resource/filter/Vector.h +++ b/smtk/resource/filter/VectorGrammar.h @@ -7,8 +7,8 @@ // the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR // PURPOSE. See the above copyright notice for more information. //========================================================================= -#ifndef smtk_resource_filter_Vector_h -#define smtk_resource_filter_Vector_h +#ifndef smtk_resource_filter_VectorGrammar_h +#define smtk_resource_filter_VectorGrammar_h #include "smtk/resource/filter/Name.h" #include "smtk/resource/filter/Property.h" @@ -66,76 +66,6 @@ struct Property > }; // clang-format on -/// Specialization of ValueAction to accommodate vectors of types. -template -struct ValueAction> -{ - template - static void apply(const Input& input, Rules& rules) - { - std::unique_ptr& rule = rules.data().back(); - std::vector value; - std::regex re(","); - std::string in = input.string(); - std::sregex_token_iterator it(in.begin(), in.end(), re, -1), last; - for (int id = 0; it != last; ++it, ++id) - { - std::string str = it->str(); - value.push_back(Property::convert(str)); - } - - static_cast>*>(rule.get())->acceptableValue = - [value](const std::vector& val) -> bool { - if (val.size() != value.size()) - { - return false; - } - for (std::size_t i = 0; i < value.size(); ++i) - { - if (val[i] != value[i]) - { - return false; - } - } - return true; - }; - } -}; - -/// Specialization of ValueRegexAction to accommodate vectors of types. -template -struct ValueRegexAction> -{ - template - static void apply(const Input& input, Rules& rules) - { - std::unique_ptr& rule = rules.data().back(); - std::vector regex; - - std::regex re(","); - std::sregex_token_iterator it(input.string().begin(), input.string().end(), re, -1), last; - for (int id = 0; it != last; ++it, ++id) - { - regex.emplace_back(it->str().c_str()); - } - - static_cast>*>(rule.get())->acceptableValue = - [regex](const std::vector& val) -> bool { - if (val.size() != regex.size()) - { - return false; - } - for (std::size_t i = 0; i < regex.size(); ++i) - { - if (!std::regex_match(val[i], regex[i])) - { - return false; - } - } - return true; - }; - } -}; } // namespace filter } // namespace resource } // namespace smtk diff --git a/smtk/resource/testing/cxx/TestResourceFilter.cxx b/smtk/resource/testing/cxx/TestResourceFilter.cxx index 3bab80f5e..6e7b00276 100644 --- a/smtk/resource/testing/cxx/TestResourceFilter.cxx +++ b/smtk/resource/testing/cxx/TestResourceFilter.cxx @@ -15,6 +15,7 @@ #include "smtk/resource/PersistentObject.h" #include "smtk/resource/filter/Filter.h" +#include "smtk/resource/filter/ResourceActions.h" #include "smtk/common/testing/cxx/helpers.h" From 956163353c324537e8933fad596ce8d1f4d2185a Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Thu, 8 Aug 2024 11:15:41 -0500 Subject: [PATCH 38/42] Changed operation helper to use BaseKey The helper implementation should use BaseKey type instead of the legacy Key type so that the behavior of operations that run in the JSON serializer/deserializer can be changed (locking, observers, etc...). --- smtk/operation/Helper.cxx | 4 ++-- smtk/operation/Helper.h | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/smtk/operation/Helper.cxx b/smtk/operation/Helper.cxx index 99158beca..ba50888e1 100644 --- a/smtk/operation/Helper.cxx +++ b/smtk/operation/Helper.cxx @@ -28,12 +28,12 @@ Helper& Helper::instance() { if (g_instanceStack.empty()) { - g_instanceStack.emplace_back(std::unique_ptr(new Helper)); + g_instanceStack.emplace_back(new Helper); } return *(g_instanceStack.back()); } -Helper& Helper::pushInstance(Operation::Key* opKey) +Helper& Helper::pushInstance(Operation::BaseKey* opKey) { g_instanceStack.emplace_back(new Helper); g_instanceStack.back()->m_key = opKey; diff --git a/smtk/operation/Helper.h b/smtk/operation/Helper.h index 93314d1cd..38d5b8436 100644 --- a/smtk/operation/Helper.h +++ b/smtk/operation/Helper.h @@ -45,7 +45,7 @@ class SMTKCORE_EXPORT Helper /// /// The returned \a Helper will have the same managers as /// the previous (if any) helper. - static Helper& pushInstance(Operation::Key* opsKey); + static Helper& pushInstance(Operation::BaseKey* opsKey); /// Pop a helper instance off the local thread's stack. static void popInstance(); @@ -59,11 +59,12 @@ class SMTKCORE_EXPORT Helper bool topLevel() const { return m_topLevel; } /// Return the key currently being used. - const Operation::Key* key() const { return m_key; } + const Operation::BaseKey* key() const { return m_key; } protected: Helper(); - Operation::Key* m_key; + Operation::BaseKey* m_key{ nullptr }; + /// m_topLevel indicates whether pushInstance() (false) or instance() (true) /// was used to create this helper. bool m_topLevel = true; From a0e1d98216991ff944544dfb9a1daa4c0863984b Mon Sep 17 00:00:00 2001 From: David Thompson Date: Wed, 7 Aug 2024 14:52:27 -0400 Subject: [PATCH 39/42] Remove mesh/model-worker docs. We no longer use Remus to communicate with external mesh/model processes. --- doc/userguide/administration.rst | 125 +------------------------------ 1 file changed, 2 insertions(+), 123 deletions(-) diff --git a/doc/userguide/administration.rst b/doc/userguide/administration.rst index b5bf91949..89067a3d2 100644 --- a/doc/userguide/administration.rst +++ b/doc/userguide/administration.rst @@ -14,9 +14,8 @@ Previous sections covered the concepts and tools for using SMTK. This section is for system administrators who wish to make SMTK available to users -* as a Python module and command-line utilities for end users of SMTK, -* as a library for people developing SMTK-based applications, and/or -* as a remote model and mesh server for end users and applications. +* as a Python module and command-line utilities for end users of SMTK, and/or +* as a library for people developing SMTK-based applications. End-user tool installation ========================== @@ -39,123 +38,3 @@ follow the instructions for building and installing SMTK in the toplevel :file:`ReadMe.md` file. .. todo:: Expand on details of installation and configuration. - -Configuration as a modeling and meshing server -============================================== - -SMTK uses Remus_ to start model and mesh workers. -In order for SMTK to discover available workers, -you must place Remus worker files somewhere that SMTK -is configured to search for them. -These worker files identify the executable to -run when a user requests a modeling or meshing operation -to be performed. -Their format is covered further below, but first we -focus on how the files are discovered. - -Worker file search paths ------------------------- - -The default locations that SMTK searches for these -worker files varies by operating system: - -Linux - SMTK searches the current working directory of the - process, followed by the :file:`var/smtk/workers` subdirectory - of the toplevel installation directory. - For example, if SMTK is installed into :file:`/usr` - with the worker at :file:`/usr/bin/smtk-model-worker`, - then it will search :file:`/usr/var/smtk/workers`. - - If the :cxx:`SMTK_WORKER_DIR` environment variable is set - to a valid path, then it is searched as well. - -Mac OS X - SMTK searches the current working directory of the - process, followed by the :file:`var/workers` subdirectory - of the toplevel installation directory if SMTK is not part of a bundle. - For example, if SMTK is installed into :file:`/usr` - with the worker at :file:`/usr/bin/smtk-model-worker`, - then it will search :file:`/usr/var/smtk/workers`. - - If an application built with SMTK is part of a bundle (such as an app), - then SMTK will search the :file:`Contents/Resources/workers` directory - of the bundle. - - If the :cxx:`SMTK_WORKER_DIR` environment variable is set - to a valid path, then it is searched as well. - -Windows - SMTK searches the current working directory of the process - followed by the directory containing the process executable - (when provided to SMTK by the application). - - If the :cxx:`SMTK_WORKER_DIR` environment variable is set - to a valid path, then it is searched as well. - -.. _Remus: https://github.com/robertmaynard/Remus - -Creating a Remus worker file for solid modeling ------------------------------------------------ - -When SMTK is built with Remus support enabled, it will include a -command-line utility named :file:`smtk-model-worker`. -This program can be run manually or directly by SMTK in order -to perform modeling operations in a different process. -It is also the program you can run to generate a worker file -that makes it discoverable to SMTK for direct use. -You can run - -.. code:: sh - - smtk-model-worker -help - -to obtain reference information on the command-line arguments. -It will also print a list of available modeling kernels. - -Each model worker exposes a single modeling kernel (via -:smtk:`smtk::session::remote::Session` on the client, which talks to -a :smtk:`RemusRPCWorker` in the worker process). -Normally, the model worker executable expects to be given the -following command-line arguments: - -* A Remus server to connect to as its first argument, formatted - as a URL (e.g., :arg:`tcp://cadserver.kitware.com:50510`). -* A solid modeling kernel to advertise (e.g., :arg:`-kernel=cgm`). -* A default modeling engine for the kernel to use (e.g., :arg:`-engine=OpenCascade`). -* A Remus worker file to read (when invoked without :arg:`-generate`) or - write (when invoked with :arg:`-generate`). -* A directory in the filesystem to make available to users for reading and - writing CAD model files (e.g., :arg:`-root=/usr/local/var/smtk/data`). - The root directory is not made available to end users for security - purposes; all paths are relative to the root directory. - SMTK does not currently prevent access to other portions of the - filesystem but it will in the future. -* A "site name" describing the combination of host and/or filesystem - made available to SMTK by the model worker. This label is presented - to end users by applications so that users can differentiate between - workers providing the same modeling kernels but on different machines. -* A Remus worker file to read or write - (e.g., :arg:`-rwfile=/usr/local/var/smtk/workers/cgm-OCC.rw`). - -If you pass the :arg:`-generate` option to the model worker, -then it will generate a model worker file which you can then -customize. -When you generate a model worker file, -two files are normally written: -the first, specified by the :arg:`-rwfile` argument is the -actual Remus worker file and is formatted as a JSON object. -The second has the same filename with a :file:`.requirements` -suffix appended and is formatted as an XML attribute resource -describing the modeling operations available. - -You should generate a separate Remus worker file for each combination -of modeling kernel, engine, and root directory you wish to make -available. -Once these files have been generated, -you can edit the worker file and change them to suit your site's needs. -You may specifically wish to change the WorkerName setting to be more -descriptive. -Be careful when editing the Tag data as it is used by SMTK -to decide which engine and kernel combination may load a given -file. From cdac86a135b240061235be00c3ce64c5c2b2cdd2 Mon Sep 17 00:00:00 2001 From: David Thompson Date: Fri, 9 Aug 2024 15:54:32 -0400 Subject: [PATCH 40/42] Fix the `testCloneResource` test. --- doc/release/notes/graph-clone.rst | 9 +++++++ smtk/graph/Resource.h | 44 ++++++++++++++++++++++++------- smtk/markup/Resource.cxx | 8 ++++++ smtk/markup/Resource.h | 5 ++++ 4 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 doc/release/notes/graph-clone.rst diff --git a/doc/release/notes/graph-clone.rst b/doc/release/notes/graph-clone.rst new file mode 100644 index 000000000..70a2c6d1e --- /dev/null +++ b/doc/release/notes/graph-clone.rst @@ -0,0 +1,9 @@ +Graph Resource +-------------- + +An issue with the :smtk:`smtk::graph::Resource` template's ``clone()`` method +has been identified. When the source resource does not override ``clone()`` and +does not have a resource-manager, the base ``clone()`` implementation will no +longer attempt to create a resource. If you encounter this situation, you are +now expected to override the ``clone()`` method in your subclass and call the +new ``prepareClone()`` method with the resource you create. diff --git a/smtk/graph/Resource.h b/smtk/graph/Resource.h index ca52cdac7..2035c1fd1 100644 --- a/smtk/graph/Resource.h +++ b/smtk/graph/Resource.h @@ -250,9 +250,30 @@ class SMTK_ALWAYS_EXPORT Resource /// The output copy will not have any nodes or arcs but will have any run-time arc types /// registered to match the originating resource if \a options has copyTemplateData() /// set to true. + /// + /// Note that unless concrete subclasses override this method, this will return + /// a null pointer if no resource manager exists on \a this. + /// This graph resource templated on a Traits object is typically not identical + /// to a full implementation of a new graph-based resource. This implementation + /// has no way to create a duplicate resource of the same type without the resource + /// manager. + /// + /// If you choose to override this method, you may call prepareClone() described + /// below with a bare resource to populate. std::shared_ptr clone( smtk::resource::CopyOptions& options) const override; + /// Copy graph arc-types and other data that is part of the schema of a graph + /// resource (rather than the content of the resource). + /// + /// This method is called by clone() if a \a result resource can be created. + /// If your subclass of the graph resource overrides clone(), you are expected + /// to call this method from within your override (or perform the equivalent + /// initialization yourself). + virtual void prepareClone( + smtk::resource::CopyOptions& options, + const std::shared_ptr& result) const; + /// Implement copyInitialize() to copy arcs and nodes from a non-empty resource of the same type. bool copyInitialize( const std::shared_ptr& source, @@ -448,22 +469,28 @@ template std::shared_ptr Resource::clone( smtk::resource::CopyOptions& options) const { - using smtk::resource::CopyOptions; - std::shared_ptr result; if (auto rsrcMgr = this->manager()) { - std::cerr << "Clone: Asking Resource Manager\n"; result = std::dynamic_pointer_cast(rsrcMgr->create(this->typeName())); } - if (!result) + if (result) { - std::cerr << "Clone: Creating it myself\n"; - result = Resource::create(); + this->prepareClone(options, result); } + return result; +} + +template +void Resource::prepareClone( + smtk::resource::CopyOptions& options, + const std::shared_ptr& result) const +{ + using smtk::resource::CopyOptions; + if (!result) { - return result; + return; } // Insert the source of the original into the object mapping @@ -492,10 +519,7 @@ std::shared_ptr Resource::clone( } // TODO: Copy any run-time arc types. - return result; } - - return result; } template diff --git a/smtk/markup/Resource.cxx b/smtk/markup/Resource.cxx index 36c36d9db..336a31222 100644 --- a/smtk/markup/Resource.cxx +++ b/smtk/markup/Resource.cxx @@ -78,6 +78,14 @@ bool Resource::setLocation(const std::string& location) return true; } +std::shared_ptr Resource::clone( + smtk::resource::CopyOptions& options) const +{ + auto result = std::shared_ptr(new Resource); + this->prepareClone(options, result); + return result; +} + bool Resource::copyInitialize( const std::shared_ptr& source, smtk::resource::CopyOptions& options) diff --git a/smtk/markup/Resource.h b/smtk/markup/Resource.h index e05067ff2..f65cbd153 100644 --- a/smtk/markup/Resource.h +++ b/smtk/markup/Resource.h @@ -65,6 +65,11 @@ class SMTKMARKUP_EXPORT Resource /// we can reset resource-relative URLs. bool setLocation(const std::string& location) override; + /// Override clone() to create a new markup resource using + /// \a this resource as a template. + std::shared_ptr clone( + smtk::resource::CopyOptions& options) const override; + /// Override copyInitialize() to copy domain information. bool copyInitialize( const std::shared_ptr& source, From 7876ca59be005cd326500aaa15894dd57ba13d82 Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Mon, 19 Aug 2024 14:48:17 -0500 Subject: [PATCH 41/42] Run event loop when waiting for operation result Changed the ResultHandler implementation to run the event loop while waiting on the future to yield the async operation's result. This is to prevent operations from deadlocking when observers need to run on the main thread while the main thread is simultaneously waiting on the future to complete. --- smtk/extension/qt/qtOperationLauncher.cxx | 15 ++++++++++++++- smtk/extension/qt/qtOperationLauncher.h | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/smtk/extension/qt/qtOperationLauncher.cxx b/smtk/extension/qt/qtOperationLauncher.cxx index 7ef1da0be..c6725e562 100644 --- a/smtk/extension/qt/qtOperationLauncher.cxx +++ b/smtk/extension/qt/qtOperationLauncher.cxx @@ -11,6 +11,7 @@ #include "smtk/extension/qt/qtOperationLauncher.h" #include +#include // To enable SINGLE_THREAD, set CMake variable SMTK_ENABLE_OPERATION_THREADS to OFF. #ifdef SINGLE_THREAD @@ -140,9 +141,21 @@ ResultHandler::ResultHandler() { } -smtk::operation::Operation::Result ResultHandler::waitForResult() +smtk::operation::Operation::Result ResultHandler::waitForResult() const { + // Check if the result is being waited for on the main thread so that the + // event loop can be pumped here manually. + if (QThread::currentThread() == qApp->thread()) + { + const auto timeout = std::chrono::milliseconds(100); + while (m_future.wait_for(timeout) != std::future_status::ready) + { + // Process events while waiting for the result. + QCoreApplication::processEvents(); + } + } return m_future.get(); } + } // namespace extension } // namespace smtk diff --git a/smtk/extension/qt/qtOperationLauncher.h b/smtk/extension/qt/qtOperationLauncher.h index edc8e6a1f..6c9a964fe 100644 --- a/smtk/extension/qt/qtOperationLauncher.h +++ b/smtk/extension/qt/qtOperationLauncher.h @@ -40,7 +40,7 @@ class SMTKQTEXT_EXPORT ResultHandler : public QObject public: explicit ResultHandler(); - smtk::operation::Operation::Result waitForResult(); + smtk::operation::Operation::Result waitForResult() const; std::shared_future& future() { return m_future; } Q_SIGNALS: From 411d354146536cf25e794c2f5b525c549658cd30 Mon Sep 17 00:00:00 2001 From: Justin Wilson Date: Fri, 23 Aug 2024 14:51:16 -0500 Subject: [PATCH 42/42] Export explicit template instantiations on Windows. Fixes an issue on Windows where tasks and adaptors could not be serialized or deserialized because each DLL linking to SMTK will create a unique template instatiation for Configurator and Configurator which causes issues for any container that looks up by type id. Exporting explicit instantiations causes every DLL to share instantiations provided by the SMTK core library. --- smtk/task/json/Configurator.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/smtk/task/json/Configurator.h b/smtk/task/json/Configurator.h index 06c0f77db..5f72bcacf 100644 --- a/smtk/task/json/Configurator.h +++ b/smtk/task/json/Configurator.h @@ -10,6 +10,7 @@ #ifndef smtk_task_json_Configurator_h #define smtk_task_json_Configurator_h +#include "smtk/task/Adaptor.h" #include "smtk/task/Task.h" #include "smtk/common/Managers.h" @@ -150,6 +151,15 @@ class SMTK_ALWAYS_EXPORT Configurator static HelperTypeMap s_types; }; +#ifdef SMTK_MSVC +// MSVC requires explicit template instantiations to be exported. Otherwise, +// multiple instantiations will occur for each consuming DLL which will cause +// issues with type containers since each instance of what is effectively the +// same type will have a different type ID in each DLL. +template class SMTKCORE_EXPORT Configurator; +template class SMTKCORE_EXPORT Configurator; +#endif + } // namespace json } // namespace task } // namespace smtk