Commit 9bc606b7 authored by Bob Obara's avatar Bob Obara

ENH:: Adding the concept of ForceRequired

An item, whose Definition indicates that it is optional, may be forced to be required
based on external factors in a simulation workflow.  This merge request adds the following to Item:

* setForceRequired()
* forceRequired()

Updated IO, JSON, Pybind, and Qt code to support the change.

IO, JSON, Pybind, and Qt Widgets have been updated to support this new functionality.
parent df54e35f
......@@ -125,6 +125,13 @@ Added a class for utility methods. The current ones include:
* removedValue() - will now remove a value regardless of what it's index is. Previously, if the index was less than the NumberOfRequiredValues it would not be removed but simply unset. Now, as long as the resulting number of values is greater than or equal to the number of required values, it will be removed.
Else it will be unset.
### Changes to Item
* Added the concept of **ForceRequired**. Some work-flows may need to force an optional item to be required base on external factors. If
an item's ForceRequired is set then even if it's definition indicates it should be optional, Item::isOptional will return false. Note if the item's definition states it is required, setting ForceRequied is basically a no op.
* New API
* void Item::setForceRequired(bool)
* bool Item::forceRequired()
### Custom attribute item and definition types
SMTK's attribute system now supports the registration of user-defined attribute items and definitions by overloading `smtk::attribute::CustomItem` and `smtk::attribute::CustomItemDefinition`, respectively. The registration of custom item definition types must occur before the attribute is serialized. Custom item definitions can be listed in plugins' Registrar implementations as follows:
......
......@@ -22,6 +22,7 @@ Item::Item(Attribute* owningAttribute, int itemPosition)
, m_owningItem(nullptr)
, m_position(itemPosition)
, m_isEnabled(true)
, m_forceRequired(false)
{
m_hasLocalAdvanceLevelInfo[0] = false;
m_hasLocalAdvanceLevelInfo[1] = false;
......@@ -122,7 +123,7 @@ bool Item::isOptional() const
{
return false;
}
return m_definition->isOptional();
return ((!m_forceRequired) && m_definition->isOptional());
}
bool Item::isEnabled() const
......
......@@ -139,12 +139,12 @@ public:
int subGroupPosition() const { return m_subGroupPosition; }
/// Returns true if the item is optional
/// \brief Returns true if the item is optional
bool isOptional() const;
/// An item is enabled under the following coditions:
/// An item is enabled under the following conditions:
/// 1. If it is not owned by another item (such as a group), and either
/// it is not optional or it has been explicitly enabled
/// it is not optional or it has been explicitly enabled
/// 2. If it's owning item is enabled and either
/// it is not optional or it has been explicitly enabled
bool isEnabled() const;
......@@ -154,6 +154,20 @@ public:
/// Set the instance's local enabled state
void setIsEnabled(bool isEnabledValue) { m_isEnabled = isEnabledValue; }
/// @{
/// \brief Controls if an item should be forced to be required regardless of
/// its local enable property.
///
/// There are cases within a work-flow when an item which can be considered
/// optional in general must be required based on the state of the overall
/// work-flow. These methods allow the programmer to explicitly force an
/// optional item to be required. If the Definition states that the item
/// is required naturally, this will have no effect.
/// By default forceRequired is false.
void setForceRequired(bool val) { m_forceRequired = val; }
bool forceRequired() const { return m_forceRequired; }
/// @}
///\brief return the categories associated with the item (via its Definition)
const smtk::attribute::Categories& categories() const;
......@@ -240,6 +254,7 @@ protected:
private:
bool m_hasLocalAdvanceLevelInfo[2];
unsigned int m_localAdvanceLevel[2];
bool m_forceRequired;
};
inline smtk::simulation::UserDataPtr Item::userData(const std::string& key) const
......
......@@ -10,6 +10,7 @@
#include "jsonItem.h"
#include "smtk/PublicPointerDefs.h"
#include "smtk/attribute/Item.h"
#include "smtk/attribute/ItemDefinition.h"
#include "nlohmann/json.hpp"
#include "smtk/CoreExports.h"
......@@ -26,9 +27,10 @@ namespace attribute
SMTKCORE_EXPORT void to_json(json& j, const smtk::attribute::ItemPtr& itemPtr)
{
j["Name"] = itemPtr->name();
if (itemPtr->isOptional())
if (itemPtr->definition()->isOptional())
{
j["Enabled"] = itemPtr->isEnabled();
j["Enabled"] = itemPtr->localEnabledState();
j["ForceRequired"] = itemPtr->forceRequired();
}
// Does the item have explicit advance level information
if (itemPtr->hasLocalAdvanceLevelInfo(0))
......@@ -49,16 +51,20 @@ SMTKCORE_EXPORT void from_json(const json& j, smtk::attribute::ItemPtr& itemPtr)
{
return;
}
if (itemPtr->isOptional())
auto result = j.find("Enabled");
if (result != j.end())
{
itemPtr->setIsEnabled(*result);
}
result = j.find("ForceRequired");
if (result != j.end())
{
auto enabled = j.find("Enabled");
if (enabled != j.end())
{
itemPtr->setIsEnabled(*enabled);
}
itemPtr->setForceRequired(*result);
}
auto result = j.find("AdvanceReadLevel");
result = j.find("AdvanceReadLevel");
if (result != j.end())
{
itemPtr->setLocalAdvanceLevel(0, *result);
......
......@@ -41,6 +41,8 @@ PySharedPtrClass< smtk::attribute::Item > pybind11_init_smtk_attribute_Item(py::
.def("isEnabled", &smtk::attribute::Item::isEnabled)
.def("setIsEnabled", &smtk::attribute::Item::setIsEnabled, py::arg("isEnabledValue"))
.def("localEnabledState", &smtk::attribute::Item::localEnabledState)
.def("setForceRequired", &smtk::attribute::Item::setForceRequired, py::arg("forceRequiredMode"))
.def("forceRequired", &smtk::attribute::Item::forceRequired)
// NOTE that the Python form of this method is returning a copy since Python
// doesn't support const references
.def("categories", &smtk::attribute::Item::categories)
......
......@@ -88,6 +88,7 @@ set(unit_tests
unitDefinitionTags
unitAttributeExclusiveAnalysis
unitJsonItemDefinitions.cxx
unitOptionalItems
unitPassCategories
unitPathGrammar.cxx
)
......
......@@ -241,7 +241,7 @@ int unitComponentItemConstraints(int /*unused*/, char* /*unused*/ [])
{
// ----
// I. Let's create an attribute resource and some definitions
// The first definition will have three compomemt items:
// The first definition will have three component items:
// comp0 will have an unique role and have 2 values so we
// can uniqueness when trying to assign to the same item
......
//=========================================================================
// 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/Definition.h"
#include "smtk/attribute/FileItem.h"
#include "smtk/attribute/GroupItem.h"
#include "smtk/attribute/GroupItemDefinition.h"
#include "smtk/attribute/IntItem.h"
#include "smtk/attribute/ReferenceItem.h"
#include "smtk/attribute/Resource.h"
#include "smtk/attribute/ResourceItem.h"
#include "smtk/attribute/StringItem.h"
#include "smtk/attribute/StringItemDefinition.h"
#include "smtk/attribute/VoidItemDefinition.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 testItem(const ItemPtr& item, bool isOptional, bool isEnabled, bool isLocalEnabled)
{
bool status = true;
std::cerr << "\t" << item->name() << " : ";
// for non-optional items we only check enabled
if (!item->isOptional())
{
if (isOptional)
{
std::cerr << " isOptional(FAILED) ";
status = false;
}
if (item->isEnabled() != isEnabled)
{
std::cerr << " isEnabled(FAILED) ";
status = false;
}
}
else
{
if (!isOptional)
{
std::cerr << " isOptional(FAILED) ";
status = false;
}
if (item->isEnabled() != isEnabled)
{
std::cerr << " isEnabled(FAILED) ";
status = false;
}
if (item->localEnabledState() != isLocalEnabled)
{
std::cerr << " isLocalEnabled(FAILED) ";
status = false;
}
}
if (status)
{
std::cerr << " - PASSED\n";
}
else
{
std::cerr << "\n";
}
return status;
}
bool testResource(const attribute::ResourcePtr& attRes, const std::string& prefix)
{
// Lets find the Attributes and test their Items
bool status = true;
AttributePtr a = attRes->findAttribute("a");
smtkTest(a != nullptr, "Could not find attribute a!");
std::cerr << prefix << "Testing Attribute a: \n";
ItemPtr item;
// We expect the following:
// s0 - not optional, enabled
// s1 - optional, enabled, localEnabled
// s2 - optional, not enabled, not local enabled
// g0 - optional, enabled, localEnabled
// g0s0 - optional, enabled, localEnabled
// g0s1 - optional, not enabled, not local enabled
// g1 - optional, not enabled, not local enabled
// g1s0 - optional, not enabled, local enabled
// g1s1 - not optional, not enabled
item = a->find("s0");
// note that for non-optional the third arg is ignored
status = status && testItem(item, false, true, true);
item = a->find("s1");
status = status && testItem(item, true, true, true);
item = a->find("s2");
status = status && testItem(item, true, false, false);
GroupItemPtr gitem = a->findAs<GroupItem>("g0");
status = status && testItem(gitem, true, true, true);
item = gitem->find("g0s0");
status = status && testItem(item, true, true, true);
item = gitem->find("g0s1");
status = status && testItem(item, true, false, false);
gitem = a->findAs<GroupItem>("g1");
status = status && testItem(gitem, true, false, false);
item = gitem->find("g1s0");
status = status && testItem(item, true, false, true);
item = gitem->find("g1s1");
status = status && testItem(item, false, false, false);
AttributePtr b = attRes->findAttribute("b");
smtkTest(b != nullptr, "Could not find attribute b!");
std::cerr << prefix << "Testing Attribute b: \n";
// We only need to find s2 since that one was forceRequired
item = b->find("s2");
if (!item->forceRequired())
{
std::cerr << "\ts2 is not marked Force Required\n";
status = false;
}
status = status && testItem(item, false, true, false);
return status;
}
void setupAttributeResource(attribute::ResourcePtr& attRes)
{
// Lets create a definition with the following content:
// s0 - string
// s1 - optional string which is by default is enabled
// s2 - optional string which is by default is not enabled
// g0 - optional group which is by default is enabled with the following
// g0s0 - optional string which is by default is enabled
// g0s1 - optional string which is by default is not enabled
// g1 - optional group which is by default is not enabled with the following
// g1s0 - optional string which is by default is enabled
// g1s1 - string
DefinitionPtr A = attRes->createDefinition("A");
auto sItemDef0 = A->addItemDefinition<StringItemDefinition>("s0");
auto sItemDef1 = A->addItemDefinition<StringItemDefinition>("s1");
sItemDef1->setIsOptional(true);
sItemDef1->setIsEnabledByDefault(true);
auto sItemDef2 = A->addItemDefinition<StringItemDefinition>("s2");
sItemDef2->setIsOptional(true);
auto gItemDef0 = A->addItemDefinition<GroupItemDefinition>("g0");
gItemDef0->setIsOptional(true);
gItemDef0->setIsEnabledByDefault(true);
auto sItemDefg0s0 = gItemDef0->addItemDefinition<StringItemDefinition>("g0s0");
sItemDefg0s0->setIsOptional(true);
sItemDefg0s0->setIsEnabledByDefault(true);
auto sItemDefg0s1 = gItemDef0->addItemDefinition<StringItemDefinition>("g0s1");
sItemDefg0s1->setIsOptional(true);
auto gItemDef1 = A->addItemDefinition<GroupItemDefinition>("g1");
gItemDef1->setIsOptional(true);
auto sItemDefg1s0 = gItemDef1->addItemDefinition<StringItemDefinition>("g1s0");
sItemDefg1s0->setIsOptional(true);
sItemDefg1s0->setIsEnabledByDefault(true);
auto sItemDefg1s1 = gItemDef1->addItemDefinition<StringItemDefinition>("g1s1");
attRes->finalizeDefinitions();
// Now create 2 attributes and set the second's s2 to be force required
auto att = attRes->createAttribute("a", "A");
auto att1 = attRes->createAttribute("b", "A");
ItemPtr item = att1->find("s2");
item->setForceRequired(true);
}
}
int unitOptionalItems(int /*unused*/, char* /*unused*/ [])
{
//
// I. Let's create an attribute resource and some definitions
attribute::ResourcePtr attRes = attribute::Resource::create();
setupAttributeResource(attRes);
smtkTest(testResource(attRes, "First Pass - "), "Failed testing Optional in First Pass");
io::AttributeWriter writer;
io::AttributeReader reader;
io::Logger logger;
std::string writeRroot(SMTK_SCRATCH_DIR);
std::string fname = writeRroot + "/unitAttributeOptionalItemsTest.sbi";
std::string rname = writeRroot + "/unitAttributeOptionalItemsTest.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<int>(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<int>(smtk::operation::Operation::Outcome::SUCCEEDED),
"JSON Read operation failed\n"
<< writeOp->log().convertToString());
attRes = std::dynamic_pointer_cast<smtk::attribute::Resource>(
opresult->findResource("resource")->value());
//Test the resource created using JSON
smtkTest(testResource(attRes, "JSON Pass - "), "Failed testing Optional in JSON Pass");
//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
smtkTest(testResource(attRes, "XML Pass - "), "Failed testing Optional in XML Pass");
return 0;
}
......@@ -87,6 +87,7 @@ public:
QList<QPointer<QFrame> > m_editFrames;
QPointer<QToolButton> AddItemButton;
QPointer<QFrame> Contents;
QPointer<QCheckBox> OptionalCheck;
};
qtItem* qtFileItem::createItemWidget(const qtAttributeItemInfo& info)
......@@ -736,6 +737,18 @@ void qtFileItem::createWidget()
void qtFileItem::updateItemData()
{
auto item = m_itemInfo.itemAs<FileSystemItem>();
if (item->isOptional())
{
m_internals->OptionalCheck->setVisible(true);
this->setOutputOptional(item->localEnabledState() ? 1 : 0);
}
else if (m_internals->OptionalCheck)
{
m_internals->OptionalCheck->setVisible(false);
m_internals->Contents->setVisible(true);
}
int i, n = m_internals->m_editors.size();
for (i = 0; i < n; i++)
{
......@@ -883,15 +896,24 @@ void qtFileItem::updateUI()
labelLayout->setSpacing(0);
labelLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
int padding = 0;
if (item->isOptional())
{
QCheckBox* optionalCheck = new QCheckBox(m_itemInfo.parentWidget());
optionalCheck->setChecked(item->localEnabledState());
optionalCheck->setText(" ");
optionalCheck->setSizePolicy(sizeFixedPolicy);
padding = optionalCheck->iconSize().width() + 3; // 6 is for layout spacing
QObject::connect(optionalCheck, SIGNAL(stateChanged(int)), this, SLOT(setOutputOptional(int)));
labelLayout->addWidget(optionalCheck);
// Note that the definition could be optional but the item maybe forced
// to be required. We need to still create the check box in case
// the item's force required state is changed
if (itemDef->isOptional())
{
m_internals->OptionalCheck = new QCheckBox(m_itemInfo.parentWidget());
m_internals->OptionalCheck->setChecked(item->localEnabledState());
m_internals->OptionalCheck->setText(" ");
m_internals->OptionalCheck->setSizePolicy(sizeFixedPolicy);
padding = m_internals->OptionalCheck->iconSize().width() + 3; // 6 is for layout spacing
QObject::connect(
m_internals->OptionalCheck, SIGNAL(stateChanged(int)), this, SLOT(setOutputOptional(int)));
labelLayout->addWidget(m_internals->OptionalCheck);
if (!item->isOptional())
{
m_internals->OptionalCheck->setVisible(false);
m_internals->Contents->setVisible(true);
}
}
QString labelText;
......
......@@ -41,6 +41,7 @@ public:
QList<QToolButton*> MinusButtonIndices;
QPointer<QToolButton> AddItemButton;
QPointer<QTableWidget> ItemsTable;
QPointer<QGroupBox> GroupBox;
std::map<std::string, qtAttributeItemInfo> m_itemViewMap;
};
......@@ -57,8 +58,8 @@ qtItem* qtGroupItem::createItemWidget(const qtAttributeItemInfo& info)
qtGroupItem::qtGroupItem(const qtAttributeItemInfo& info)
: qtItem(info)
{
this->Internals = new qtGroupItemInternals;
m_itemInfo.createNewDictionary(this->Internals->m_itemViewMap);
m_internals = new qtGroupItemInternals;
m_itemInfo.createNewDictionary(m_internals->m_itemViewMap);
m_isLeafItem = true;
std::string insertMode;
auto item = m_itemInfo.itemAs<attribute::GroupItem>();
......@@ -75,7 +76,7 @@ qtGroupItem::qtGroupItem(const qtAttributeItemInfo& info)
qtGroupItem::~qtGroupItem()
{
this->clearChildItems();
delete this->Internals;
delete m_internals;
}
void qtGroupItem::setLabelVisible(bool visible)
......@@ -90,8 +91,7 @@ void qtGroupItem::setLabelVisible(bool visible)
return;
}
QGroupBox* groupBox = qobject_cast<QGroupBox*>(m_widget);
groupBox->setTitle(visible ? item->label().c_str() : "");
m_internals->GroupBox->setTitle(visible ? item->label().c_str() : "");
}
void qtGroupItem::createWidget()
......@@ -108,8 +108,8 @@ void qtGroupItem::createWidget()
}
QString title = item->label().c_str();
QGroupBox* groupBox = new QGroupBox(title, m_itemInfo.parentWidget());
m_widget = groupBox;
m_internals->GroupBox = new QGroupBox(title, m_itemInfo.parentWidget());
m_widget = m_internals->GroupBox;
if (this->isReadOnly())
{
......@@ -121,11 +121,11 @@ void qtGroupItem::createWidget()
// leak because the layout instance is parented by the widget.)
new QVBoxLayout(m_widget);
m_widget->layout()->setMargin(0);
this->Internals->ChildrensFrame = new QFrame(groupBox);
this->Internals->ChildrensFrame->setObjectName("groupitemFrame");
new QVBoxLayout(this->Internals->ChildrensFrame);
m_internals->ChildrensFrame = new QFrame(m_internals->GroupBox);
m_internals->ChildrensFrame->setObjectName("groupitemFrame");
new QVBoxLayout(m_internals->ChildrensFrame);
m_widget->layout()->addWidget(this->Internals->ChildrensFrame);
m_widget->layout()->addWidget(m_internals->ChildrensFrame);
if (m_itemInfo.parentWidget())
{
......@@ -134,21 +134,21 @@ void qtGroupItem::createWidget()
}
this->updateItemData();
// If the group is optional, we need a checkbox
// If the group is optional, we need a check box
if (item->isOptional())
{
groupBox->setCheckable(true);
groupBox->setChecked(item->localEnabledState());
m_internals->GroupBox->setCheckable(true);
m_internals->GroupBox->setChecked(item->localEnabledState());
//Hides empty frame when not enabled.
groupBox->setStyleSheet("QGroupBox::unchecked {border: none;}");
this->Internals->ChildrensFrame->setVisible(item->isEnabled());
connect(groupBox, SIGNAL(toggled(bool)), this, SLOT(setEnabledState(bool)));
m_internals->GroupBox->setStyleSheet("QGroupBox::unchecked {border: none;}");
m_internals->ChildrensFrame->setVisible(item->isEnabled());
connect(m_internals->GroupBox, SIGNAL(toggled(bool)), this, SLOT(setEnabledState(bool)));
}
}
void qtGroupItem::setEnabledState(bool checked)
{
this->Internals->ChildrensFrame->setVisible(checked);
m_internals->ChildrensFrame->setVisible(checked);
auto item = m_itemInfo.item();
if (item == nullptr)
{
......@@ -169,14 +169,25 @@ void qtGroupItem::setEnabledState(bool checked)
void qtGroupItem::updateItemData()
{
// Since an item's optional status can change (using
// forceRequired) we need to reevaluate the optional status
auto item = m_itemInfo.itemAs<attribute::GroupItem>();
if (item->isOptional())
{
m_internals->GroupBox->setCheckable(true);
m_internals->GroupBox->setChecked(item->localEnabledState());
}
else
{
m_internals->GroupBox->setCheckable(false);
}
this->clearChildItems();
auto myChildren = this->Internals->ChildrensFrame->findChildren<QWidget*>("groupitem_frame");
auto myChildren = m_internals->ChildrensFrame->findChildren<QWidget*>("groupitem_frame");
for (auto myChild : myChildren)
{
myChild->deleteLater();
}
auto item = m_itemInfo.itemAs<attribute::GroupItem>();
if (!item || (!item->numberOfGroups() && !item->isExtensible()))
{
return;
......@@ -186,31 +197,31 @@ void qtGroupItem::updateItemData()
if (item->isExtensible())
{
//clear mapping
this->Internals->ExtensibleMap.clear();
this->Internals->MinusButtonIndices.clear();
if (this->Internals->ItemsTable)
m_internals->ExtensibleMap.clear();
m_internals->MinusButtonIndices.clear();
if (m_internals->ItemsTable)
{
this->Internals->ItemsTable->blockSignals(true);
this->Internals->ItemsTable->clear();
this->Internals->ItemsTable->setRowCount(0);
this->Internals->ItemsTable->setColumnCount(1);
this->Internals->ItemsTable->setHorizontalHeaderItem(0, new QTableWidgetItem(" "));
this->Internals->ItemsTable->blockSignals(false);
m_internals->ItemsTable->blockSignals(true);
m_internals->ItemsTable->clear();
m_internals->ItemsTable->setRowCount(0);
m_internals->ItemsTable->setColumnCount(1);
m_internals->ItemsTable->setHorizontalHeaderItem(0, new QTableWidgetItem(" "));
m_internals->ItemsTable->blockSignals(false);
}
// The new item button
if (!this->Internals->AddItemButton)
if (!m_internals->AddItemButton)
{
this->Internals->AddItemButton = new QToolButton(this->Internals->ChildrensFrame);