diff --git a/Engine/source/console/consoleObject.cpp b/Engine/source/console/consoleObject.cpp
index dfa17bca2..df71c2b21 100644
--- a/Engine/source/console/consoleObject.cpp
+++ b/Engine/source/console/consoleObject.cpp
@@ -356,6 +356,72 @@ static char* suppressSpaces(const char* in_pname)
return replacebuf;
}
+void ConsoleObject::registerField(const char* name, U32 type, dsize_t offset, const FieldDescriptor& desc)
+{
+ AbstractClassRep::Field f;
+ // Remove spaces.
+
+ const bool isCollection = desc.isArrayBegin || desc.isArrayEnd || desc.isGroupBegin || desc.isGroupEnd;
+ if (isCollection)
+ {
+ char* pFieldNameBuf = suppressSpaces(name);
+ if (desc.isGroupBegin)
+ {
+ // Append group begin type to fieldname.
+ dStrcat(pFieldNameBuf, "_begingroup", 1024);
+ f.type = AbstractClassRep::StartGroupFieldType;
+ }
+
+ if (desc.isGroupEnd)
+ {
+ // Append group end type to fieldname.
+ dStrcat(pFieldNameBuf, "_endgroup", 1024);
+ f.type = AbstractClassRep::EndGroupFieldType;
+ }
+
+ if (desc.isArrayBegin)
+ {
+ // Append array type to fieldname.
+ dStrcat(pFieldNameBuf, "_beginarray", 1024);
+ f.type = AbstractClassRep::StartArrayFieldType;
+ }
+
+ if (desc.isArrayEnd)
+ {
+ // Append array end type to fieldname.
+ dStrcat(pFieldNameBuf, "_endarray", 1024);
+ f.type = AbstractClassRep::EndArrayFieldType;
+ }
+
+ f.pFieldname = StringTable->insert(pFieldNameBuf);
+ f.pGroupname = StringTable->insert(name);
+ }
+ else
+ {
+ ConsoleBaseType* conType = ConsoleBaseType::getType(desc._type);
+ AssertFatal(conType, avar("ConsoleObject::addProtectedField[%s] - invalid console type", name));
+ f.pFieldname = StringTable->insert(name);
+ f.type = desc._type;
+ f.table = conType->getEnumTable();
+ }
+
+ if (desc.docs)
+ f.pFieldDocs = desc.docs;
+
+ f.offset = desc._offset;
+ f.validator = desc.validator;
+ f.setDataFn = desc.setFn;
+ f.getDataFn = desc.getFn;
+ f.writeDataFn = desc.writeFn;
+ f.visibilityFn = desc.visibilityFn;
+ f.elementCount = desc.elementCount;
+ f.groupExpand = desc.isExpanded;
+ f.networkMask = desc.networkMask;
+ f.flag = desc.flags;
+
+ sg_tempFieldList.push_back(f);
+}
+
void ConsoleObject::addGroup(const char* in_pGroupname, const char* in_pGroupDocs)
{
// Remove spaces.
@@ -1130,3 +1196,23 @@ DefineEngineFunction(linkNamespaces, bool, ( String childNSName, String parentNS
return true;
}
+FieldDescriptor::~FieldDescriptor()
+{
+ if (!_active)
+ return;
+
+ if (_name == NULL || _name[0] == '\0')
+ return;
+
+ _active = false;
+
+ // if we are a collection, sanitize some properties.
+ if (isGroupBegin || isGroupEnd || isArrayBegin || isArrayEnd)
+ {
+ elementCount = isArrayBegin ? elementCount : 0;
+ validator = NULL;
+ networkMask = 0;
+ }
+
+ ConsoleObject::registerField(_name, _type, _offset, *this);
+}
diff --git a/Engine/source/console/consoleObject.h b/Engine/source/console/consoleObject.h
index 2d5586613..bdfde53c3 100644
--- a/Engine/source/console/consoleObject.h
+++ b/Engine/source/console/consoleObject.h
@@ -456,6 +456,11 @@ public:
/// This is a function pointer typedef to support optional writing for fields.
typedef bool(*WriteDataNotify)(void* obj, StringTableEntry pFieldName);
+ ///
+ /// This is a function pointer typedef to support optional visibility of fields.
+ ///
+ typedef bool(*VisibilityNotify)(void* obj, const char* array);
+
/// These are special field type values used to mark
/// groups and arrays in the field list.
/// @see Field::type
@@ -517,27 +522,29 @@ public:
setDataFn( NULL ),
getDataFn( NULL ),
writeDataFn(NULL),
+ visibilityFn(NULL),
networkMask(0)
{
doNotSubstitute = keepClearSubsOnly = false;
}
- StringTableEntry pFieldname; ///< Name of the field.
- const char* pGroupname; ///< Optionally filled field containing the group name.
- ///
- /// This is filled when type is StartField or EndField
+ StringTableEntry pFieldname; ///< Name of the field.
+ const char* pGroupname; ///< Optionally filled field containing the group name.
+ ///
+ /// This is filled when type is StartField or EndField
- const char* pFieldDocs; ///< Documentation about this field; see consoleDoc.cc.
- bool groupExpand; ///< Flag to track expanded/not state of this group in the editor.
- U32 type; ///< A data type ID or one of the special custom fields. @see ACRFieldTypes
- U32 offset; ///< Memory offset from beginning of class for this field.
- S32 elementCount; ///< Number of elements, if this is an array.
- const EnumTable * table; ///< If this is an enum, this points to the table defining it.
- BitSet32 flag; ///< Stores various flags
- TypeValidator *validator; ///< Validator, if any.
- SetDataNotify setDataFn; ///< Set data notify Fn
- GetDataNotify getDataFn; ///< Get data notify Fn
- WriteDataNotify writeDataFn; ///< Function to determine whether data should be written or not.
+ const char* pFieldDocs; ///< Documentation about this field; see consoleDoc.cc.
+ bool groupExpand; ///< Flag to track expanded/not state of this group in the editor.
+ U32 type; ///< A data type ID or one of the special custom fields. @see ACRFieldTypes
+ U32 offset; ///< Memory offset from beginning of class for this field.
+ S32 elementCount; ///< Number of elements, if this is an array.
+ const EnumTable * table; ///< If this is an enum, this points to the table defining it.
+ BitSet32 flag; ///< Stores various flags
+ TypeValidator *validator; ///< Validator, if any.
+ SetDataNotify setDataFn; ///< Set data notify Fn
+ GetDataNotify getDataFn; ///< Get data notify Fn
+ WriteDataNotify writeDataFn; ///< Function to determine whether data should be written or not.
+ VisibilityNotify visibilityFn; ///< Function to determine this field is visibile or not.
bool doNotSubstitute;
bool keepClearSubsOnly;
@@ -764,9 +771,173 @@ template< typename T > EnginePropertyTable& ConcreteAbstractClassRep< T >::smPro
//------------------------------------------------------------------------------
// Forward declaration of this function so it can be used in the class
+bool defaultProtectedSetFn(void* object, const char* index, const char* data);
const char *defaultProtectedGetFn( void *obj, const char *data );
bool defaultProtectedWriteFn(void* obj, StringTableEntry pFieldName);
+struct FieldDescriptor {
+ // Context for auto-registration
+ const char* _name = NULL;
+ U32 _type = 0;
+ dsize_t _offset = 0;
+ bool isExpanded = false;
+ bool isGroupBegin = false;
+ bool isGroupEnd = false;
+ bool isArrayBegin = false;
+ bool isArrayEnd = false;
+ bool _active = true;
+
+ // Field properties
+ const char* docs = NULL;
+ AbstractClassRep::SetDataNotify setFn = &defaultProtectedSetFn;
+ AbstractClassRep::GetDataNotify getFn = &defaultProtectedGetFn;
+ AbstractClassRep::WriteDataNotify writeFn = &defaultProtectedWriteFn;
+ TypeValidator* validator = NULL;
+ U32 elementCount = 1;
+ U32 flags = 0;
+ U32 networkMask = 0;
+ AbstractClassRep::VisibilityNotify visibilityFn = NULL;
+
+ // Add documentation to this field.
+ FieldDescriptor& doc(const char* d) { docs = d; return *this; }
+ // The number of elements for this field (U32 number)
+ FieldDescriptor& elements(U32 n) { elementCount = n; return *this; }
+ // The validator for this field.
+ FieldDescriptor& validate(TypeValidator* v) { validator = v; return *this; }
+ // The set data function.
+ FieldDescriptor& onSet(AbstractClassRep::SetDataNotify fn) { setFn = fn; return *this; }
+ // The get data function.
+ FieldDescriptor& onGet(AbstractClassRep::GetDataNotify fn) { getFn = fn; return *this; }
+ // The on write function
+ FieldDescriptor& onWrite(AbstractClassRep::WriteDataNotify fn) { writeFn = fn; return *this; }
+ // The visibility function
+ FieldDescriptor& showWhen(AbstractClassRep::VisibilityNotify fn) { visibilityFn = fn; return *this; }
+ // The network mask changes will trigger.
+ FieldDescriptor& network(U32 mask) { networkMask = mask; return *this; }
+ // The flags for this field.
+ FieldDescriptor& withFlags(U32 f) { flags = f; return *this; }
+ // This field marks the start of a group.
+ FieldDescriptor& startGroup() { isGroupBegin = true; return *this; }
+ // This field marks the end of a group.
+ FieldDescriptor& endGroup() { isGroupEnd = true; return *this; }
+ // This field marks the start of an array.
+ FieldDescriptor& startArray() { isArrayBegin = true; return *this; }
+ // This field marks the end of an array.
+ FieldDescriptor& endArray() { isArrayEnd = true; return *this; }
+ // This field should start expanded in the inspector.
+ FieldDescriptor& expanded() { isExpanded = true; return *this; }
+
+ FieldDescriptor(const char* name, U32 type = 0, dsize_t offset = 0)
+ : _name(name), _type(type), _offset(offset)
+ {}
+
+ FieldDescriptor(const FieldDescriptor&) = delete;
+ FieldDescriptor& operator=(const FieldDescriptor&) = delete;
+
+ FieldDescriptor(FieldDescriptor&& other) noexcept
+ {
+ moveFrom(std::move(other));
+ }
+
+ FieldDescriptor& operator=(FieldDescriptor&& other) noexcept
+ {
+ if (this != &other)
+ moveFrom(std::move(other));
+ return *this;
+ }
+
+ void moveFrom(FieldDescriptor&& other)
+ {
+ // transfer all data
+ _name = other._name;
+ _type = other._type;
+ _offset = other._offset;
+
+ docs = other.docs;
+ setFn = other.setFn;
+ getFn = other.getFn;
+ writeFn = other.writeFn;
+ validator = other.validator;
+ elementCount = other.elementCount;
+ flags = other.flags;
+ networkMask = other.networkMask;
+ visibilityFn = other.visibilityFn;
+
+ isExpanded = other.isExpanded;
+ isGroupBegin = other.isGroupBegin;
+ isGroupEnd = other.isGroupEnd;
+ isArrayBegin = other.isArrayBegin;
+ isArrayEnd = other.isArrayEnd;
+
+ // transfer ownership of "registration responsibility"
+ _active = other._active;
+
+ // CRITICAL: disable the source so its destructor does NOTHING
+ other._active = false;
+ }
+
+ ~FieldDescriptor();
+
+};
+
+
+/// Registers a standard field.
+/// Usage:
+/// FIELD("health", TypeS32, Offset(mHealth, Player)).doc("Player health");
+#define ADD_FIELD(name, type, offset) \
+ FieldDescriptor(name, type, offset)
+
+/// Field with fixed element count.
+/// Usage:
+/// FIELD_ARRAY("color", TypeColorF, Offset(mColor, GuiControl), 4);
+#define FIELD_ARRAY(name, type, offset, count) \
+ FieldDescriptor(name, type, offset).elements(count)
+
+/// Begins a field group in the inspector.
+/// All fields declared after this will belong to the group until GROUP_END().
+/// Usage:
+/// GROUP_BEGIN("Rendering").expanded();
+#define GROUP_BEGIN(name) \
+ FieldDescriptor(name).startGroup()
+
+/// Ends the current field group.
+/// Must match a preceding GROUP_BEGIN().
+/// Usage:
+/// GROUP_END("Rendering");
+#define GROUP_END(name) \
+ FieldDescriptor(name).endGroup()
+
+/// Begins an array block in the inspector.
+/// Usage:
+/// ARRAY_BEGIN("Waypoints");
+#define ARRAY_BEGIN(name, count) \
+ FieldDescriptor(name).startArray().elements(count)
+
+/// Ends an array block.
+/// Usage:
+/// ARRAY_END("Waypoints");
+#define ARRAY_END(name) \
+ FieldDescriptor(name).endArray()
+
+/// Declares a scoped inspector group.
+///
+/// Provides a structured grouping of fields in the inspector UI using a scoped
+/// block. Internally, this macro guarantees that a matching group end marker
+/// is emitted, preventing mismatched begin/end pairs.
+#define GROUP(name) \
+ for (bool _once = true; _once; _once = false) \
+ for (FieldDescriptor(name).startGroup(); _once; FieldDescriptor(name).endGroup(), _once = false)
+
+/// Declares a scoped inspector array block.
+///
+/// Defines a logical array grouping in the inspector UI. Fields declared within
+/// the block are treated as elements of the array. Like GROUP, this macro
+/// ensures that the array begin/end markers are correctly paired using scoped
+/// execution.
+#define ARRAY(name, count) \
+ for (bool _once = true; _once; _once = false) \
+ for (FieldDescriptor(name).startArray().elements(count); _once; FieldDescriptor(name).endArray(), _once = false)
+
//=============================================================================
// ConsoleObject.
//=============================================================================
@@ -871,6 +1042,7 @@ public:
/// @name Fields
/// @{
+ static void registerField(const char* name, U32 type, dsize_t offset, const FieldDescriptor& desc);
/// Mark the beginning of a group of fields.
///
diff --git a/Engine/source/gui/editor/guiInspector.cpp b/Engine/source/gui/editor/guiInspector.cpp
index 3089e38c7..5596523d4 100644
--- a/Engine/source/gui/editor/guiInspector.cpp
+++ b/Engine/source/gui/editor/guiInspector.cpp
@@ -785,6 +785,59 @@ void GuiInspector::refresh()
//-----------------------------------------------------------------------------
+void GuiInspector::updateVisibility()
+{
+ const U32 numTargets = getNumInspectObjects();
+
+ for (U32 i = 0; i < numTargets; i++)
+ {
+
+ SimObject* target = getInspectObject(i);
+ if (!target) return;
+
+ for (GuiInspectorGroup* group : mGroups)
+ {
+ const AbstractClassRep::Field* g = target->findField(group->getGroupName().c_str());
+
+ // if group has its own visibility function let it control it.
+ bool group_visible = (!g || !g->visibilityFn) ? true : g->visibilityFn(target, "0");
+ if (!group_visible)
+ {
+ group->setVisible(group_visible);
+ return;
+ }
+
+ bool anyVisible = false;
+ for (GuiInspectorField* field : group->mChildren)
+ {
+ const AbstractClassRep::Field* f = field->getField();
+ StringTableEntry idx = field->getArrayIndex();
+ bool visible = (!f || !f->visibilityFn) ? true : f->visibilityFn(target, idx);
+
+ field->setVisible(visible);
+ if (visible)
+ anyVisible = true;
+ }
+
+ // Per-array-element rollout visibility
+ for (GuiInspectorGroup::ArrayElementEntry& elem : group->mArrayElements)
+ {
+ bool visible = false;
+ if (!elem.arrayField || !elem.arrayField->visibilityFn || elem.elementIndex == 0)
+ visible = true;
+
+ if (!visible)
+ visible = elem.arrayField->visibilityFn(target, String::ToString(elem.elementIndex));
+
+ elem.rollout->setVisible(visible);
+ if (visible) anyVisible = true;
+ }
+
+ group->setVisible(anyVisible);
+ }
+ }
+}
+
void GuiInspector::sendInspectPreApply()
{
const U32 numObjects = getNumInspectObjects();
diff --git a/Engine/source/gui/editor/guiInspector.h b/Engine/source/gui/editor/guiInspector.h
index 6e8a9363b..25d328bf0 100644
--- a/Engine/source/gui/editor/guiInspector.h
+++ b/Engine/source/gui/editor/guiInspector.h
@@ -90,6 +90,8 @@ public:
return NULL;
}
+ void updateVisibility();
+
S32 getComponentGroupTargetId() { return mComponentGroupTargetId; }
void setComponentGroupTargetId(S32 compId) { mComponentGroupTargetId = compId; }
diff --git a/Engine/source/gui/editor/inspector/field.cpp b/Engine/source/gui/editor/inspector/field.cpp
index 4871a0049..b237c7278 100644
--- a/Engine/source/gui/editor/inspector/field.cpp
+++ b/Engine/source/gui/editor/inspector/field.cpp
@@ -835,6 +835,8 @@ void GuiInspectorField::updateValue()
}
else
setValue( getData() );
+
+ mInspector->updateVisibility();
}
//-----------------------------------------------------------------------------
diff --git a/Engine/source/gui/editor/inspector/field.h b/Engine/source/gui/editor/inspector/field.h
index 30dd7442b..2f2a11968 100644
--- a/Engine/source/gui/editor/inspector/field.h
+++ b/Engine/source/gui/editor/inspector/field.h
@@ -164,6 +164,7 @@ class GuiInspectorField : public GuiControl
///
StringTableEntry getArrayIndex() const { return mFieldArrayIndex; }
+ AbstractClassRep::Field* getField() const { return mField; }
/// Called from within setData to allow child classes
/// to perform their own verification.
diff --git a/Engine/source/gui/editor/inspector/group.cpp b/Engine/source/gui/editor/inspector/group.cpp
index 5d4a292e9..18a3d3e57 100644
--- a/Engine/source/gui/editor/inspector/group.cpp
+++ b/Engine/source/gui/editor/inspector/group.cpp
@@ -79,6 +79,7 @@ GuiInspectorGroup::GuiInspectorGroup( const String& groupName,
mChildren.clear();
mMargin.set(0,0,4,0);
+ VECTOR_SET_ASSOCIATION(mArrayElements);
}
//-----------------------------------------------------------------------------
@@ -229,6 +230,7 @@ void GuiInspectorGroup::clearFields()
// that we keep for our own convenience.
mArrayCtrls.clear();
mChildren.clear();
+ mArrayElements.clear();
}
//-----------------------------------------------------------------------------
@@ -336,7 +338,7 @@ bool GuiInspectorGroup::inspectGroup()
GuiControlProfile* elementRolloutProfile = dynamic_cast(Sim::findObject("GuiInspectorRolloutProfile0"));
char buf[256];
- dSprintf(buf, 256, " [%i]", i);
+ dSprintf(buf, 256, " [%i/%i]", i, field->elementCount);
elementRollout->setControlProfile(elementRolloutProfile);
elementRollout->setCaption(buf);
@@ -349,6 +351,8 @@ bool GuiInspectorGroup::inspectGroup()
elementRollout->instantCollapse();
arrayStack->addObject(elementRollout);
+
+ mArrayElements.push_back({ elementRollout, (S32)i, field });
}
pArrayRollout = arrayRollout;
diff --git a/Engine/source/gui/editor/inspector/group.h b/Engine/source/gui/editor/inspector/group.h
index 11ca3eee5..498e4243f 100644
--- a/Engine/source/gui/editor/inspector/group.h
+++ b/Engine/source/gui/editor/inspector/group.h
@@ -44,6 +44,13 @@ private:
typedef GuiRolloutCtrl Parent;
public:
// Members
+ struct ArrayElementEntry
+ {
+ GuiRolloutCtrl* rollout;
+ S32 elementIndex;
+ const AbstractClassRep::Field* arrayField; // the StartArrayFieldType field
+ };
+ Vector mArrayElements;
SimObjectPtr mParent;
Vector mChildren;
GuiStackControl* mStack;