Files
o3de/Gems/Atom/Tools/AtomToolsFramework/Code/Source/DynamicProperty/DynamicProperty.cpp
T
Guthrie Adams c9c172794e Atom Tools: Document system exposes reflected object data
• This removes a direct dependency on dynamic property groups and data from the document system.
• Added support for names, descriptions, and nesting to dynamic property groups.
• Moved property related functions from base document classes into material editor document classes because dynamic property groups are an implementation detail of the material editor document to support material type flexible data format.
• Change material document to use a table of dynamic property groups instead of a map of properties.
• Added functions to traverse groups and properties.
• This keeps groups and properties organized consistently with the material type file as well as what’s expected in the UI.
• Document data can now be maps directly to the inspector reflective property editors instead of copying one property at a time out of the document and keeping those synchronized.

Signed-off-by: Guthrie Adams <guthadam@amazon.com>
2022-02-03 21:42:27 -06:00

438 lines
17 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Serialization/SerializeContext.h>
#include <Atom/RPI.Edit/Common/ColorUtils.h>
#include <AtomToolsFramework/DynamicProperty/DynamicProperty.h>
namespace AtomToolsFramework
{
// DynamicProperty uses AZStd::any and some other template container types like assets for editable values.
// DynamicProperty uses a single dynamic edit data object to apply to all contained instances in its data hierarchy.
// The dynamic edit data is not read directly from DynamicProperty but copied whenever the RPE rebuilds its tree.
// Whenever attributes are refreshed, new values are read from the dynamic edit data copy. Updating the source values has no effect
// unless the tree is rebuilt. We want to avoid rebuilding the RPE tree because it is a distracting and terrible UI experience.
// The edit context and RPE allow binding functions and methods to attribute to support dynamic edit data changes.
// If attributes are bound to functions the edit data can be copied and functions will be called each time attributes are refreshed.
// The pre existing AttributeMemberFunction expects the instance data pointer to be the object pointer for the member function.
// The pre existing AttributeMemberFunction will not work for DynamicProperty because it shares one dynamic edit data
// object throughout its hierarchy. The instance data pointer will only be the same as DynamicProperty at the root.
// AttributeFixedMemberFunction (based on AttributeMemberFunction) addresses these issues by binding member functions with
// a fixed object pointer.
template<class T>
class AttributeFixedMemberFunction;
template<class R, class C, class... Args>
class AttributeFixedMemberFunction<R(C::*)(Args...) const>
: public AZ::AttributeFunction<R(Args...)>
{
public:
AZ_RTTI((AtomToolsFramework::AttributeFixedMemberFunction<R(C::*)(Args...) const>, "{78511F1E-58AD-4670-8440-1FE4C9BD1C21}", R, C, Args...), AZ::AttributeFunction<R(Args...)>);
AZ_CLASS_ALLOCATOR(AttributeFixedMemberFunction<R(C::*)(Args...) const>, AZ::SystemAllocator, 0);
typedef R(C::* FunctionPtr)(Args...) const;
explicit AttributeFixedMemberFunction(C* o, FunctionPtr f)
: AZ::AttributeFunction<R(Args...)>(nullptr)
, m_object(o)
, m_memFunction(f)
{}
R Invoke(void* /*instance*/, const Args&... args) override
{
return (m_object->*m_memFunction)(args...);
}
AZ::Uuid GetInstanceType() const override
{
return AZ::Uuid::CreateNull();
}
private:
C* m_object = nullptr;
FunctionPtr m_memFunction;
};
void DynamicProperty::Reflect(AZ::ReflectContext* context)
{
if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<DynamicProperty>()
->Field("value", &DynamicProperty::m_value)
;
if (auto editContext = serializeContext->GetEditContext())
{
editContext->Class<DynamicProperty>(
"DynamicProperty", "")
->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->Attribute(AZ::Edit::Attributes::Visibility, &DynamicProperty::GetVisibility)
->SetDynamicEditDataProvider(&DynamicProperty::GetPropertyEditData)
->DataElement(AZ::Edit::UIHandlers::Default, &DynamicProperty::m_value, "Value", "")
// AZStd::any is treated like a container type so we hide it and pass attributes to the child element
->Attribute(AZ::Edit::Attributes::AutoExpand, true)
->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly)
;
}
}
}
const AZ::Edit::ElementData* DynamicProperty::GetPropertyEditData(const void* handlerPtr, [[maybe_unused]] const void* elementPtr, [[maybe_unused]] const AZ::Uuid& elementType)
{
const DynamicProperty* owner = reinterpret_cast<const DynamicProperty*>(handlerPtr);
return owner->GetEditData();
}
DynamicProperty::DynamicProperty(const DynamicPropertyConfig& config)
: m_value(config.m_originalValue)
, m_config(config)
{
}
void DynamicProperty::SetValue(const AZStd::any& value)
{
AZ_Assert(!value.empty(), "DynamicProperty attempting to assign a bad value to: %s", m_config.m_id.GetCStr());
m_value = value;
}
const AZStd::any& DynamicProperty::GetValue() const
{
return m_value;
}
void DynamicProperty::SetConfig(const DynamicPropertyConfig& config)
{
m_config = config;
m_editDataTracker = nullptr;
}
const DynamicPropertyConfig& DynamicProperty::GetConfig() const
{
return m_config;
}
void DynamicProperty::UpdateEditData()
{
if (m_editDataTracker != &m_editData)
{
m_editDataTracker = &m_editData;
CheckRangeMetaDataValues();
m_editData = {};
m_editData.m_elementId = AZ::Edit::UIHandlers::Default;
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::NameLabelOverride, &DynamicProperty::GetDisplayName);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::AssetPickerTitle, &DynamicProperty::GetAssetPickerTitle);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::DescriptionTextOverride, &DynamicProperty::GetDescription);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::ReadOnly, &DynamicProperty::IsReadOnly);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::EnumValues, &DynamicProperty::GetEnumValues);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::ChangeNotify, &DynamicProperty::OnDataChanged);
AddEditDataAttribute(AZ::Edit::Attributes::ShowProductAssetFileName, false);
AddEditDataAttribute(AZ_CRC_CE("Thumbnail"), m_config.m_showThumbnail);
switch (m_config.m_dataType)
{
case DynamicPropertyType::Int:
ApplyRangeEditDataAttributes<int32_t>();
ApplySliderEditDataAttributes<int32_t>();
break;
case DynamicPropertyType::UInt:
ApplyRangeEditDataAttributes<uint32_t>();
ApplySliderEditDataAttributes<uint32_t>();
break;
case DynamicPropertyType::Float:
ApplyRangeEditDataAttributes<float>();
ApplySliderEditDataAttributes<float>();
break;
case DynamicPropertyType::Vector2:
case DynamicPropertyType::Vector3:
case DynamicPropertyType::Vector4:
ApplyVectorLabels();
ApplyRangeEditDataAttributes<float>();
break;
case DynamicPropertyType::Color:
AddEditDataAttribute(AZ_CRC_CE("ColorEditorConfiguration"), AZ::RPI::ColorUtils::GetRgbEditorConfig());
break;
case DynamicPropertyType::Enum:
m_editData.m_elementId = AZ::Edit::UIHandlers::ComboBox;
break;
case DynamicPropertyType::String:
m_editData.m_elementId = AZ::Edit::UIHandlers::LineEdit;
break;
case DynamicPropertyType::Invalid:
break;
}
}
}
const AZ::Edit::ElementData* DynamicProperty::GetEditData() const
{
const_cast<DynamicProperty*>(this)->UpdateEditData();
return m_editDataTracker;
}
bool DynamicProperty::IsValid() const
{
return !m_value.empty();
}
const AZ::Name DynamicProperty::GetId() const
{
return m_config.m_id;
}
AZStd::string DynamicProperty::GetDisplayName() const
{
return !m_config.m_displayName.empty() ? m_config.m_displayName : m_config.m_name;
}
AZStd::string DynamicProperty::GetGroupDisplayName() const
{
return m_config.m_groupDisplayName;
}
AZStd::string DynamicProperty::GetAssetPickerTitle() const
{
return GetGroupDisplayName().empty() ? GetDisplayName() : GetGroupDisplayName() + " " + GetDisplayName();
}
AZStd::string DynamicProperty::GetDescription() const
{
return m_config.m_description;
}
AZ::Crc32 DynamicProperty::GetVisibility() const
{
return (IsValid() && m_config.m_visible) ?
AZ::Edit::PropertyVisibility::ShowChildrenOnly :
AZ::Edit::PropertyVisibility::Hide;
}
bool DynamicProperty::IsReadOnly() const
{
return !IsValid() || m_config.m_readOnly;
}
AZStd::vector<AZ::Edit::EnumConstant<uint32_t>> DynamicProperty::GetEnumValues() const
{
AZStd::vector<AZ::Edit::EnumConstant<uint32_t>> enumValues;
enumValues.reserve(m_config.m_enumValues.size());
for (const AZStd::string& name : m_config.m_enumValues)
{
enumValues.emplace_back((uint32_t)enumValues.size(), name.c_str());
}
return enumValues;
}
AZ::u32 DynamicProperty::OnDataChanged() const
{
if (m_config.m_dataChangeCallback)
{
return m_config.m_dataChangeCallback(GetValue());
}
return AZ::Edit::PropertyRefreshLevels::AttributesAndValues;
}
template<typename T>
bool DynamicProperty::CheckRangeMetaDataValuesForType() const
{
auto checkAnyType = [&](const AZ::TypeId& expectedTypeId, const AZStd::any& any, [[maybe_unused]] const char* valueName)
{
if (!any.empty() && expectedTypeId != any.type())
{
AZ_Error("AtomToolsFramework", false, "Property '%s': '%s' value data type does not match property data type.", m_config.m_id.GetCStr(), valueName);
return false;
}
return true;
};
AZ::TypeId expectedRangeTypeId = azrtti_typeid<T>();
if (!checkAnyType(expectedRangeTypeId, m_config.m_min, "Min") ||
!checkAnyType(expectedRangeTypeId, m_config.m_max, "Max") ||
!checkAnyType(expectedRangeTypeId, m_config.m_softMin, "Soft Min") ||
!checkAnyType(expectedRangeTypeId, m_config.m_softMax, "Soft Max") ||
!checkAnyType(expectedRangeTypeId, m_config.m_step, "Step"))
{
return false;
}
if (!m_config.m_min.empty() &&
!m_config.m_max.empty() &&
AZStd::any_cast<T>(m_config.m_min) == AZStd::any_cast<T>(m_config.m_max))
{
AZ_Warning("AtomToolsFramework", false, "Property '%s': Min == Max, value may be frozen in the editor.", m_config.m_id.GetCStr());
}
if (!m_config.m_step.empty() && 0 == AZStd::any_cast<T>(m_config.m_step))
{
AZ_Warning("AtomToolsFramework", false, "Property '%s': Step is 0, value may be frozen in the editor.", m_config.m_id.GetCStr());
}
return true;
}
bool DynamicProperty::CheckRangeMetaDataValues() const
{
using namespace AZ::RPI;
auto warnIfNotEmpty = [&](const AZStd::any& any, [[maybe_unused]] const char* valueName)
{
if (!any.empty())
{
AZ_Warning("AtomToolsFramework", false, "Property '%s': '%s' is not supported by this property data type.", m_config.m_id.GetCStr(), valueName);
}
};
switch (m_config.m_dataType)
{
case DynamicPropertyType::Int:
return CheckRangeMetaDataValuesForType<int32_t>();
case DynamicPropertyType::UInt:
return CheckRangeMetaDataValuesForType<uint32_t>();
case DynamicPropertyType::Float:
case DynamicPropertyType::Vector2:
case DynamicPropertyType::Vector3:
case DynamicPropertyType::Vector4:
return CheckRangeMetaDataValuesForType<float>();
default:
warnIfNotEmpty(m_config.m_min, "Min");
warnIfNotEmpty(m_config.m_max, "Max");
warnIfNotEmpty(m_config.m_step, "Step");
return true;
}
}
template<typename AttributeValueType>
void DynamicProperty::AddEditDataAttribute(AZ::Crc32 crc, AttributeValueType attribute)
{
m_editData.m_attributes.push_back(AZ::Edit::AttributePair(
crc, aznew AZ::AttributeContainerType<AttributeValueType>(attribute)));
}
template<typename AttributeMemberFunctionType>
void DynamicProperty::AddEditDataAttributeMemberFunction(AZ::Crc32 crc, AttributeMemberFunctionType memberFunction)
{
m_editData.m_attributes.push_back(AZ::Edit::AttributePair(
crc, aznew AttributeFixedMemberFunction<AttributeMemberFunctionType>(this, memberFunction)));
}
template<typename AttributeValueType>
void DynamicProperty::ApplyRangeEditDataAttributes()
{
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::Min, &DynamicProperty::GetMin<AttributeValueType>);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::Max, &DynamicProperty::GetMax<AttributeValueType>);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::SoftMin, &DynamicProperty::GetSoftMin<AttributeValueType>);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::SoftMax, &DynamicProperty::GetSoftMax<AttributeValueType>);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::Step, &DynamicProperty::GetStep<AttributeValueType>);
}
template<typename AttributeValueType>
void DynamicProperty::ApplySliderEditDataAttributes()
{
if ((m_config.m_min.is<AttributeValueType>() || m_config.m_softMin.is<AttributeValueType>())
&& (m_config.m_max.is<AttributeValueType>() || m_config.m_softMax.is<AttributeValueType>()))
{
m_editData.m_elementId = AZ::Edit::UIHandlers::Slider;
}
}
template<typename AttributeValueType>
AttributeValueType DynamicProperty::GetMin() const
{
if (m_config.m_min.is<AttributeValueType>())
{
return AZStd::any_cast<AttributeValueType>(m_config.m_min);
}
return std::numeric_limits<AttributeValueType>::lowest();
}
template<typename AttributeValueType>
AttributeValueType DynamicProperty::GetMax() const
{
if (m_config.m_max.is<AttributeValueType>())
{
return AZStd::any_cast<AttributeValueType>(m_config.m_max);
}
return std::numeric_limits<AttributeValueType>::max();
}
template<typename AttributeValueType>
AttributeValueType DynamicProperty::GetSoftMin() const
{
if (m_config.m_softMin.is<AttributeValueType>())
{
return AZStd::any_cast<AttributeValueType>(m_config.m_softMin);
}
return GetMin<AttributeValueType>();
}
template<typename AttributeValueType>
AttributeValueType DynamicProperty::GetSoftMax() const
{
if (m_config.m_softMax.is<AttributeValueType>())
{
return AZStd::any_cast<AttributeValueType>(m_config.m_softMax);
}
return GetMax<AttributeValueType>();
}
template<typename AttributeValueType>
AttributeValueType DynamicProperty::GetStep() const
{
if (m_config.m_step.is<AttributeValueType>())
{
return AZStd::any_cast<AttributeValueType>(m_config.m_step);
}
if (m_config.m_step.is<float>())
{
return aznumeric_cast<AttributeValueType>(0.001f);
}
return aznumeric_cast<AttributeValueType>(1.0f);
}
void DynamicProperty::ApplyVectorLabels()
{
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::LabelForX, &DynamicProperty::GetVectorLabelX);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::LabelForY, &DynamicProperty::GetVectorLabelY);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::LabelForZ, &DynamicProperty::GetVectorLabelZ);
AddEditDataAttributeMemberFunction(AZ::Edit::Attributes::LabelForW, &DynamicProperty::GetVectorLabelW);
}
AZStd::string DynamicProperty::GetVectorLabel(const int index) const
{
static const char* defaultLabels[] = { "X", "Y", "Z", "W" };
return index < m_config.m_vectorLabels.size() ? m_config.m_vectorLabels[index] : defaultLabels[index];
}
AZStd::string DynamicProperty::GetVectorLabelX() const
{
return GetVectorLabel(0);
}
AZStd::string DynamicProperty::GetVectorLabelY() const
{
return GetVectorLabel(1);
}
AZStd::string DynamicProperty::GetVectorLabelZ() const
{
return GetVectorLabel(2);
}
AZStd::string DynamicProperty::GetVectorLabelW() const
{
return GetVectorLabel(3);
}
} // namespace AtomToolsFramework