diff --git a/Assets/Editor/Icons/Components/NonUniformScale.svg b/Assets/Editor/Icons/Components/NonUniformScale.svg new file mode 100644 index 0000000000..f377232d62 --- /dev/null +++ b/Assets/Editor/Icons/Components/NonUniformScale.svg @@ -0,0 +1,27 @@ + + + Icons / Toolbar / Non Uniform Scaling + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Code/Framework/AzCore/AzCore/Serialization/EditContextConstants.inl b/Code/Framework/AzCore/AzCore/Serialization/EditContextConstants.inl index 90b9ba5afd..1016027966 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/EditContextConstants.inl +++ b/Code/Framework/AzCore/AzCore/Serialization/EditContextConstants.inl @@ -53,6 +53,10 @@ namespace AZ //! RemoveableByUser : A bool which determines if the component can be removed by the user. //! Setting this to false prevents the user from removing this component. Default behavior is removeable by user. const static AZ::Crc32 RemoveableByUser = AZ_CRC("RemoveableByUser", 0x32c7fd50); + //! An int which, if specified, causes a component to be forced to a particular position in the sorted list of + //! components on an entity, and prevents dragging or moving operations which would affect that position. + const static AZ::Crc32 FixedComponentListIndex = AZ_CRC_CE("FixedComponentListIndex"); + const static AZ::Crc32 AppearsInAddComponentMenu = AZ_CRC("AppearsInAddComponentMenu", 0x53790e31); const static AZ::Crc32 ForceAutoExpand = AZ_CRC("ForceAutoExpand", 0x1a5c79d2); // Ignores expansion state set by user, enforces expansion. const static AZ::Crc32 AutoExpand = AZ_CRC("AutoExpand", 0x306ff5c0); // Expands automatically unless user changes expansion state. diff --git a/Code/Framework/AzFramework/AzFramework/Components/NonUniformScaleComponent.cpp b/Code/Framework/AzFramework/AzFramework/Components/NonUniformScaleComponent.cpp index 095d986fa1..57f14ddb38 100644 --- a/Code/Framework/AzFramework/AzFramework/Components/NonUniformScaleComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Components/NonUniformScaleComponent.cpp @@ -36,6 +36,8 @@ namespace AzFramework void NonUniformScaleComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { + incompatible.push_back(AZ_CRC_CE("NonUniformScaleService")); + incompatible.push_back(AZ_CRC_CE("DebugDrawObbService")); incompatible.push_back(AZ_CRC_CE("DebugDrawService")); incompatible.push_back(AZ_CRC_CE("EMotionFXActorService")); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EntityPropertyEditorRequestsBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EntityPropertyEditorRequestsBus.h index 35b2e485b5..1959183fa0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EntityPropertyEditorRequestsBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EntityPropertyEditorRequestsBus.h @@ -31,6 +31,10 @@ namespace AzToolsFramework //! Allows a component to get the list of selected entities //! \param selectedEntityIds the return vector holding the entities required virtual void GetSelectedEntities(EntityIdList& selectedEntityIds) = 0; + + //! Explicitly sets a component as having been the most recently added. + //! This means that the next time the UI refreshes, that component will be ensured to be visible. + virtual void SetNewComponentId(AZ::ComponentId componentId) = 0; }; using EntityPropertyEditorRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp index 5e928a382a..989398f196 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/EditorNonUniformScaleComponent.cpp @@ -39,9 +39,10 @@ namespace AzToolsFramework editContext->Class("Non-uniform Scale", "Non-uniform scale for this entity only (does not propagate through hierarchy)") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") - ->Attribute(AZ::Edit::Attributes::Category, "Non-uniform Scale") - ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")) - ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->Attribute(AZ::Edit::Attributes::FixedComponentListIndex, 1) + ->Attribute(AZ::Edit::Attributes::RemoveableByUser, true) + ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/NonUniformScale.svg") + ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/NonUniformScale.svg") ->DataElement( AZ::Edit::UIHandlers::Default, &EditorNonUniformScaleComponent::m_scale, "Non-uniform Scale", "Non-uniform scale for this entity only (does not propagate through hierarchy)") @@ -61,6 +62,8 @@ namespace AzToolsFramework void EditorNonUniformScaleComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { + incompatible.push_back(AZ_CRC_CE("NonUniformScaleService")); + incompatible.push_back(AZ_CRC_CE("DebugDrawObbService")); incompatible.push_back(AZ_CRC_CE("DebugDrawService")); incompatible.push_back(AZ_CRC_CE("EMotionFXActorService")); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.cpp index dcced5b705..f8d02b6581 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.cpp @@ -25,11 +25,15 @@ #include #include #include +#include +#include #include #include #include #include #include +#include +#include #include #include @@ -1196,6 +1200,66 @@ namespace AzToolsFramework destinationComponent->SetWorldTM(const_cast(sourceComponent)->GetWorldTM()); } + AZ::Component* TransformComponent::FindPresentOrPendingComponent(AZ::Uuid componentUuid) + { + // first check if the component is present and valid + if (AZ::Component* foundComponent = GetEntity()->FindComponent(componentUuid)) + { + return foundComponent; + } + + // then check to see if there's a component pending because it's in an invalid state + AZStd::vector pendingComponents; + AzToolsFramework::EditorPendingCompositionRequestBus::Event(GetEntityId(), + &AzToolsFramework::EditorPendingCompositionRequests::GetPendingComponents, pendingComponents); + + for (const auto pendingComponent : pendingComponents) + { + if (pendingComponent->RTTI_IsTypeOf(componentUuid)) + { + return pendingComponent; + } + } + + return nullptr; + } + + bool TransformComponent::IsAddNonUniformScaleButtonReadOnly() + { + return FindPresentOrPendingComponent(EditorNonUniformScaleComponent::TYPEINFO_Uuid()) != nullptr; + } + + AZ::Crc32 TransformComponent::OnAddNonUniformScaleButtonPressed() + { + // if there is already a non-uniform scale component, do nothing + if (FindPresentOrPendingComponent(EditorNonUniformScaleComponent::TYPEINFO_Uuid())) + { + return AZ::Edit::PropertyRefreshLevels::None; + } + + const AZStd::vector entityList = { GetEntityId() }; + const AZ::ComponentTypeList componentsToAdd = { EditorNonUniformScaleComponent::TYPEINFO_Uuid() }; + + AzToolsFramework::EntityCompositionRequests::AddComponentsOutcome addComponentsOutcome; + AzToolsFramework::EntityCompositionRequestBus::BroadcastResult(addComponentsOutcome, + &AzToolsFramework::EntityCompositionRequests::AddComponentsToEntities, entityList, componentsToAdd); + + const auto nonUniformScaleComponent = FindPresentOrPendingComponent(EditorNonUniformScaleComponent::RTTI_Type()); + AZ::ComponentId nonUniformScaleComponentId = + nonUniformScaleComponent ? nonUniformScaleComponent->GetId() : AZ::InvalidComponentId; + + if (!addComponentsOutcome.IsSuccess() || !nonUniformScaleComponent) + { + AZ_Warning("Transform component", false, "Failed to add non-uniform scale component."); + return AZ::Edit::PropertyRefreshLevels::None; + } + + AzToolsFramework::EntityPropertyEditorRequestBus::Broadcast( + &AzToolsFramework::EntityPropertyEditorRequests::SetNewComponentId, nonUniformScaleComponentId); + + return AZ::Edit::PropertyRefreshLevels::EntireTree; + } + void TransformComponent::Reflect(AZ::ReflectContext* context) { // reflect data for script, serialization, editing.. @@ -1211,6 +1275,7 @@ namespace AzToolsFramework serializeContext->Class()-> Field("Parent Entity", &TransformComponent::m_parentEntityId)-> Field("Transform Data", &TransformComponent::m_editorTransform)-> + Field("AddNonUniformScaleButton", &TransformComponent::m_addNonUniformScaleButton)-> Field("Cached World Transform", &TransformComponent::m_cachedWorldTransform)-> Field("Cached World Transform Parent", &TransformComponent::m_cachedWorldTransformParent)-> Field("Parent Activation Transform Mode", &TransformComponent::m_parentActivationTransformMode)-> @@ -1224,6 +1289,7 @@ namespace AzToolsFramework { ptrEdit->Class("Transform", "Controls the placement of the entity in the world in 3d")-> ClassElement(AZ::Edit::ClassElements::EditorData, "")-> + Attribute(AZ::Edit::Attributes::FixedComponentListIndex, 0)-> Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/Transform.svg")-> Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/Transform.png")-> Attribute(AZ::Edit::Attributes::AutoExpand, true)-> @@ -1234,6 +1300,10 @@ namespace AzToolsFramework DataElement(AZ::Edit::UIHandlers::Default, &TransformComponent::m_editorTransform, "Values", "")-> Attribute(AZ::Edit::Attributes::ChangeNotify, &TransformComponent::TransformChanged)-> Attribute(AZ::Edit::Attributes::AutoExpand, true)-> + DataElement(AZ::Edit::UIHandlers::Button, &TransformComponent::m_addNonUniformScaleButton, "", "")-> + Attribute(AZ::Edit::Attributes::ButtonText, "Add non-uniform scale")-> + Attribute(AZ::Edit::Attributes::ReadOnly, &TransformComponent::IsAddNonUniformScaleButtonReadOnly)-> + Attribute(AZ::Edit::Attributes::ChangeNotify, &TransformComponent::OnAddNonUniformScaleButtonPressed)-> DataElement(AZ::Edit::UIHandlers::ComboBox, &TransformComponent::m_parentActivationTransformMode, "Parent activation", "Configures relative transform behavior when parent activates.")-> EnumAttribute(AZ::TransformConfig::ParentActivationTransformMode::MaintainOriginalRelativeTransform, "Original relative transform")-> diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.h index 8327c5f128..3d1e1ed672 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ToolsComponents/TransformComponent.h @@ -23,6 +23,7 @@ #include #include #include +#include #include "EditorComponentBase.h" #include "TransformComponentBus.h" @@ -228,6 +229,10 @@ namespace AzToolsFramework void CheckApplyCachedWorldTransform(const AZ::Transform& parentWorld); + AZ::Component* FindPresentOrPendingComponent(AZ::Uuid componentUuid); + bool IsAddNonUniformScaleButtonReadOnly(); + AZ::Crc32 OnAddNonUniformScaleButtonPressed(); + // Drives transform behavior when parent activates. See AZ::TransformConfig::ParentActivationTransformMode for details. AZ::TransformConfig::ParentActivationTransformMode m_parentActivationTransformMode; @@ -260,6 +265,10 @@ namespace AzToolsFramework bool m_worldTransformDirty = true; bool m_isStatic = false; + // This is a workaround for a bug which causes the button to appear with incorrect placement if a UI + // element is used rather than a data element. + bool m_addNonUniformScaleButton = false; + // Deprecated AZ::InterpolationMode m_interpolatePosition; AZ::InterpolationMode m_interpolateRotation; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp index 181f5b9a9d..bfb2ff4256 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.cpp @@ -63,6 +63,7 @@ AZ_POP_DISABLE_WARNING #include #include #include +#include #include #include #include @@ -494,6 +495,11 @@ namespace AzToolsFramework } } + void EntityPropertyEditor::SetNewComponentId(AZ::ComponentId componentId) + { + m_newComponentId = componentId; + } + void EntityPropertyEditor::SetOverrideEntityIds(const AzToolsFramework::EntityIdSet& entities) { m_overrideSelectedEntityIds = entities; @@ -1039,15 +1045,23 @@ namespace AzToolsFramework sortedComponents.end(), [=](const OrderedSortComponentEntry& component1, const OrderedSortComponentEntry& component2) { - // Transform component must be first, always - // If component 1 is a transform component, it is sorted earlier - if (component1.m_component->RTTI_IsTypeOf(AZ::EditorTransformComponentTypeId)) + AZStd::optional fixedComponentListIndex1 = GetFixedComponentListIndex(component1.m_component); + AZStd::optional fixedComponentListIndex2 = GetFixedComponentListIndex(component2.m_component); + + // If both components have fixed list indices, sort based on those indices + if (fixedComponentListIndex1.has_value() && fixedComponentListIndex2.has_value()) + { + return fixedComponentListIndex1.value() < fixedComponentListIndex2.value(); + } + + // If component 1 has a fixed list index, sort it first + if (fixedComponentListIndex1.has_value()) { return true; } - // If component 2 is a transform component, component 1 is never sorted earlier - if (component2.m_component->RTTI_IsTypeOf(AZ::EditorTransformComponentTypeId)) + // If component 2 has a fixed list index, component 1 should not be sorted before it + if (fixedComponentListIndex2.has_value()) { return false; } @@ -1128,10 +1142,7 @@ namespace AzToolsFramework { if (auto attributeData = azdynamic_cast*>(attribute)) { - if (!attributeData->Get(nullptr)) - { - return false; - } + return attributeData->Get(nullptr); } } } @@ -1166,6 +1177,36 @@ namespace AzToolsFramework return true; } + AZStd::optional EntityPropertyEditor::GetFixedComponentListIndex(const AZ::Component* component) + { + auto componentClassData = component ? GetComponentClassData(component) : nullptr; + if (componentClassData && componentClassData->m_editData) + { + if (auto editorDataElement = componentClassData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData)) + { + if (auto attribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::FixedComponentListIndex)) + { + if (auto attributeData = azdynamic_cast*>(attribute)) + { + return { attributeData->Get(nullptr) }; + } + } + } + } + return {}; + } + + bool EntityPropertyEditor::IsComponentDraggable(const AZ::Component* component) + { + return !GetFixedComponentListIndex(component).has_value(); + } + + bool EntityPropertyEditor::AreComponentsDraggable(const AZ::Entity::ComponentArrayType& components) const + { + return AZStd::all_of( + components.begin(), components.end(), [](AZ::Component* component) { return IsComponentDraggable(component); }); + } + bool EntityPropertyEditor::AreComponentsCopyable(const AZ::Entity::ComponentArrayType& components) const { return AreComponentsCopyable(components, m_componentFilter); @@ -3367,7 +3408,9 @@ namespace AzToolsFramework sourceComponents.size() == m_selectedEntityIds.size() && targetComponents.size() == m_selectedEntityIds.size() && AreComponentsRemovable(sourceComponents) && - AreComponentsRemovable(targetComponents); + AreComponentsRemovable(targetComponents) && + AreComponentsDraggable(sourceComponents) && + AreComponentsDraggable(targetComponents); } bool EntityPropertyEditor::IsMoveComponentsUpAllowed() const @@ -3681,14 +3724,38 @@ namespace AzToolsFramework void EntityPropertyEditor::ScrollToNewComponent() { - //force new components to be visible, assuming they are added to the end of the list and layout - auto componentEditor = GetComponentEditorsFromIndex(m_componentEditorsUsed - 1); + // force new components to be visible + // if no component has been explicitly set at the most recently added, + // assume new components are added to the end of the list and layout + AZ::s32 newComponentIndex = m_componentEditorsUsed - 1; + + // if there is a component id explicitly set as the most recently added, try to find it and make sure it is visible + if (m_newComponentId.has_value() && m_newComponentId.value() != AZ::InvalidComponentId) + { + AZ::ComponentId newComponentId = m_newComponentId.value(); + for (AZ::s32 componentIndex = 0; componentIndex < m_componentEditorsUsed; ++componentIndex) + { + if (m_componentEditors[componentIndex]) + { + for (const auto component : m_componentEditors[componentIndex]->GetComponents()) + { + if (component->GetId() == newComponentId) + { + newComponentIndex = componentIndex; + } + } + } + } + } + + auto componentEditor = GetComponentEditorsFromIndex(newComponentIndex); if (componentEditor) { m_gui->m_componentList->ensureWidgetVisible(componentEditor); } m_shouldScrollToNewComponents = false; m_shouldScrollToNewComponentsQueued = false; + m_newComponentId.reset(); } void EntityPropertyEditor::QueueScrollToNewComponent() @@ -4073,7 +4140,8 @@ namespace AzToolsFramework { if (!componentEditor || !componentEditor->isVisible() || - !AreComponentsRemovable(componentEditor->GetComponents())) + !AreComponentsRemovable(componentEditor->GetComponents()) || + !AreComponentsDraggable(componentEditor->GetComponents())) { return false; } @@ -4223,6 +4291,7 @@ namespace AzToolsFramework while (targetComponentEditor && (targetComponentEditor->IsDragged() || !AreComponentsRemovable(targetComponentEditor->GetComponents()) + || !AreComponentsDraggable(targetComponentEditor->GetComponents()) || (globalRect.center().y() > GetWidgetGlobalRect(targetComponentEditor).center().y()))) { if (targetItr == m_componentEditors.end() || targetComponentEditor == m_componentEditors.back() || !targetComponentEditor->isVisible()) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx index 677dc98277..9cd380ae06 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx @@ -211,6 +211,7 @@ namespace AzToolsFramework // EntityPropertEditorRequestBus void GetSelectedAndPinnedEntities(EntityIdList& selectedEntityIds) override; void GetSelectedEntities(EntityIdList& selectedEntityIds) override; + void SetNewComponentId(AZ::ComponentId componentId) override; bool IsEntitySelected(const AZ::EntityId& id) const; bool IsSingleEntitySelected(const AZ::EntityId& id) const; @@ -237,6 +238,9 @@ namespace AzToolsFramework static bool DoesComponentPassFilter(const AZ::Component* component, const ComponentFilter& filter); static bool IsComponentRemovable(const AZ::Component* component); bool AreComponentsRemovable(const AZ::Entity::ComponentArrayType& components) const; + static AZStd::optional GetFixedComponentListIndex(const AZ::Component* component); + static bool IsComponentDraggable(const AZ::Component* component); + bool AreComponentsDraggable(const AZ::Entity::ComponentArrayType& components) const; bool AreComponentsCopyable(const AZ::Entity::ComponentArrayType& components) const; void AddMenuOptionsForComponents(QMenu& menu, const QPoint& position); @@ -568,6 +572,9 @@ namespace AzToolsFramework void ConnectToEntityBuses(const AZ::EntityId& entityId); void DisconnectFromEntityBuses(const AZ::EntityId& entityId); + //! Stores a component id to be focused on next time the UI updates. + AZStd::optional m_newComponentId; + private slots: void OnPropertyRefreshRequired(); // refresh is needed for a property. void UpdateContents(); diff --git a/Code/Framework/AzToolsFramework/Tests/UI/EntityPropertyEditorTests.cpp b/Code/Framework/AzToolsFramework/Tests/UI/EntityPropertyEditorTests.cpp index 3017df737d..d0f93bfdf8 100644 --- a/Code/Framework/AzToolsFramework/Tests/UI/EntityPropertyEditorTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/UI/EntityPropertyEditorTests.cpp @@ -17,13 +17,13 @@ #include #include #include +#include #include #include #include #include #include -#include #include #include @@ -55,7 +55,7 @@ namespace UnitTest TEST(EntityPropertyEditorTests, PrioritySort_NonTransformAsFirstItem_TransformMovesToTopRemainderUnchanged) { - ComponentApplication app; + ToolsApplication app; AZ::Entity::ComponentArrayType unorderedComponents; AZ::Entity::ComponentArrayType orderedComponents; @@ -68,12 +68,18 @@ namespace UnitTest Entity* systemEntity = app.Create(desc, startupParams); + // Need to reflect the components so that edit attribute used for sorting, such as FixedComponentListIndex, get set. + app.RegisterComponentDescriptor(AzToolsFramework::Components::TransformComponent::CreateDescriptor()); + app.RegisterComponentDescriptor(AzToolsFramework::Components::ScriptEditorComponent::CreateDescriptor()); + app.RegisterComponentDescriptor(AZ::AssetManagerComponent::CreateDescriptor()); + // Add more than 31 components, as we are testing the case where the sort fails when there are 32 or more items. const int numFillerItems = 32; for (int commentIndex = 0; commentIndex < numFillerItems; commentIndex++) { - unorderedComponents.insert(unorderedComponents.begin(), systemEntity->CreateComponent(AZ::StreamerComponent::RTTI_Type())); + unorderedComponents.insert(unorderedComponents.begin(), systemEntity->CreateComponent( + AzToolsFramework::Components::ScriptEditorComponent::RTTI_Type())); } // Add a TransformComponent at the end which should be sorted to the beginning by the priority sort.