/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include "LyShine_precompiled.h" #include "UiElementComponent.h" #include #include #include #include #include #include #include #include "UiCanvasComponent.h" #include #include #include #include #include #include #include #include #include #include #include "UiTransform2dComponent.h" #include "IConsole.h" //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// UiElementComponent::UiElementComponent() { // This is required in order to be able to tell if the element is in the scheduled transform // recompute list (intrusive_slist doesn't initialize this except in a debug build) m_next = nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiElementComponent::~UiElementComponent() { // If this element is currently in the list of elements needing a transform recompute then // remove it from that list since the element is being destroyed if (m_next) { if (m_canvas) { m_canvas->UnscheduleElementForTransformRecompute(this); } else { m_next = nullptr; } } // In normal (correct) usage we have nothing to do here. // But if a user calls DeleteEntity or just deletes an entity pointer they can delete a UI element // and leave its parent with a dangling child pointer. // So we report an error in that case and do some recovery code. // If we were being deleted via DestroyElement m_parentId would be invalid if (m_parentId.IsValid()) { // Note we do not rely on the m_parent pointer because if the canvas is being unloaded for example the // parent entity could already have been deleted. So we use the parent entity Id to try to find the parent. AZ::Entity* parent = nullptr; EBUS_EVENT_RESULT(parent, AZ::ComponentApplicationBus, FindEntity, m_parentId); // If the parent is found and it is active that suggests something is wrong. When unloading a canvas we // deactivate all of the UI elements before any are deleted if (parent && parent->GetState() == AZ::Entity::ES_ACTIVE) { // As a final check see if this element's parent thinks that this is a child, this is almost certain to be the // case if we got here but, if not, there is nothing more to do UiElementComponent* parentElementComponent = parent->FindComponent(); if (parentElementComponent) { if (parentElementComponent->FindChildByEntityId(GetEntityId())) { // This is an error, report the error AZ_Error("UI", false, "Deleting a UI element entity directly rather than using DestroyElement. Element is named '%s'", m_entity->GetName().c_str()); // Attempt to recover by removing this element from the parent's child list parentElementComponent->RemoveChild(m_entity); // And recursively delete any child UI elements (like DestroyElement on this element would have done) auto childElementComponents = m_childElementComponents; for (auto child : childElementComponents) { // destroy the child child->DestroyElement(); } } } } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::RenderElement(LyShine::IRenderGraph* renderGraph, bool isInGame) { if (!IsFullyInitialized()) { return; } if (!m_isRenderEnabled) { return; } if (isInGame) { if (!m_isEnabled) { // Nothing to do - whole element and all children are disabled return; } } else { // We are in editing mode (not running the game) // Use the UiEditorBus to query any UiEditorComponent on this element to see if this element is // hidden in the editor bool isVisible = true; EBUS_EVENT_ID_RESULT(isVisible, GetEntityId(), UiEditorBus, GetIsVisible); if (!isVisible) { return; } } // If a component is connected to the UiRenderControl bus then we give control of rendering this element // and its children to that component, otherwise follow the standard render path if (m_renderControlInterface) { // give control of rendering this element and its children to the render control component on this element m_renderControlInterface->Render(renderGraph, this, m_renderInterface, m_childElementComponents.size(), isInGame); } else { // render any component on this element connected to the UiRenderBus if (m_renderInterface) { m_renderInterface->Render(renderGraph); } // now render child elements int numChildren = m_childElementComponents.size(); for (int i = 0; i < numChildren; ++i) { GetChildElementComponent(i)->RenderElement(renderGraph, isInGame); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::ElementId UiElementComponent::GetElementId() { return m_elementId; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::NameType UiElementComponent::GetName() { return GetEntity() ? GetEntity()->GetName() : ""; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::GetCanvasEntityId() { return m_canvas ? m_canvas->GetEntityId() : AZ::EntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::GetParent() { return m_parent; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::GetParentEntityId() { return m_parentId; } //////////////////////////////////////////////////////////////////////////////////////////////////// int UiElementComponent::GetNumChildElements() { return m_childEntityIdOrder.size(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::GetChildElement(int index) { AZ::Entity* childEntity = nullptr; if (index >= 0 && index < m_childEntityIdOrder.size()) { if (AreChildPointersValid()) { childEntity = GetChildElementComponent(index)->GetEntity(); } else { EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, m_childEntityIdOrder[index].m_entityId); } } return childEntity; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::GetChildEntityId(int index) { AZ::EntityId childEntityId; if (index >= 0 && index < m_childEntityIdOrder.size()) { childEntityId = m_childEntityIdOrder[index].m_entityId; } return childEntityId; } //////////////////////////////////////////////////////////////////////////////////////////////////// UiElementInterface* UiElementComponent::GetChildElementInterface(int index) { return GetChildElementComponent(index); } //////////////////////////////////////////////////////////////////////////////////////////////////// int UiElementComponent::GetIndexOfChild(const AZ::Entity* child) { AZ::EntityId childEntityId = child->GetId(); int numChildren = m_childEntityIdOrder.size(); for (int i = 0; i < numChildren; ++i) { if (m_childEntityIdOrder[i].m_entityId == childEntityId) { return i; } } AZ_Error("UI", false, "The given entity is not a child of this UI element"); return -1; } //////////////////////////////////////////////////////////////////////////////////////////////////// int UiElementComponent::GetIndexOfChildByEntityId(AZ::EntityId childId) { int numChildren = m_childEntityIdOrder.size(); for (int i = 0; i < numChildren; ++i) { if (m_childEntityIdOrder[i].m_entityId == childId) { return i; } } AZ_Error("UI", false, "The given entity is not a child of this UI element"); return -1; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::EntityArray UiElementComponent::GetChildElements() { int numChildren = m_childEntityIdOrder.size(); LyShine::EntityArray children; children.reserve(numChildren); // This is one of the rare functions that needs to work before FixupPostLoad has been called because it is called // from OnSliceInstantiated, so only use m_childElementComponents if it is setup if (AreChildPointersValid()) { for (int i = 0; i < numChildren; ++i) { children.push_back(GetChildElementComponent(i)->GetEntity()); } } else { for (auto& childOrderEntry : m_childEntityIdOrder) { AZ::Entity* childEntity = nullptr; EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, childOrderEntry.m_entityId); if (childEntity) { children.push_back(childEntity); } } } return children; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZStd::vector UiElementComponent::GetChildEntityIds() { AZStd::vector children; for (auto& child : m_childEntityIdOrder) { children.push_back(child.m_entityId); } return children; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::CreateChildElement(const LyShine::NameType& name) { AzFramework::EntityContextId contextId = AzFramework::EntityContextId::CreateNull(); EBUS_EVENT_ID_RESULT(contextId, GetEntityId(), AzFramework::EntityIdContextQueryBus, GetOwningContextId); AZ::Entity* child = nullptr; EBUS_EVENT_ID_RESULT(child, contextId, UiEntityContextRequestBus, CreateUiEntity, name.c_str()); AZ_Assert(child, "Failed to create child entity"); child->Deactivate(); // deactivate so that we can add components UiElementComponent* elementComponent = child->CreateComponent(); AZ_Assert(elementComponent, "Failed to create UiElementComponent"); elementComponent->m_canvas = m_canvas; elementComponent->SetParentReferences(m_entity, this); elementComponent->m_elementId = m_canvas->GenerateId(); child->Activate(); // re-activate if (AreChildPointersValid()) // must test before m_childEntityIdOrder.push_back { m_childElementComponents.push_back(elementComponent); } m_childEntityIdOrder.push_back({child->GetId(), m_childEntityIdOrder.size()}); return child; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::DestroyElement() { PrepareElementForDestroy(); DestroyElementEntity(GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::DestroyElementOnFrameEnd() { PrepareElementForDestroy(); if (m_canvas) { // Delay deletion of elements to ensure a script canvas can safely destroy its parent m_canvas->ScheduleElementDestroy(GetEntityId()); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::Reparent(AZ::Entity* newParent, AZ::Entity* insertBefore) { if (!newParent) { if (IsFullyInitialized()) { newParent = GetCanvasComponent()->GetRootElement(); } else { EmitNotInitializedWarning(); return; } } if (newParent == GetEntity()) { AZ_Warning("UI", false, "Cannot set an entity's parent to itself") return; } UiElementComponent* newParentElement = newParent->FindComponent(); AZ_Assert(newParentElement, "New parent entity has no UiElementComponent"); // check if the new parent is in a different canvas if so a reparent is not allowed // and the caller should do a CloneElement and DestroyElement if (m_canvas != newParentElement->m_canvas) { AZ_Warning("UI", false, "Reparent: Cannot reparent an element to a different canvas"); return; } if (m_parent) { // remove from parent GetParentElementComponent()->RemoveChild(GetEntity()); } newParentElement->AddChild(GetEntity(), insertBefore); SetParentReferences(newParent, newParentElement); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::ReparentByEntityId(AZ::EntityId newParent, AZ::EntityId insertBefore) { AZ::Entity* newParentEntity = nullptr; if (newParent.IsValid()) { EBUS_EVENT_RESULT(newParentEntity, AZ::ComponentApplicationBus, FindEntity, newParent); AZ_Assert(newParentEntity, "Entity for newParent not found"); } AZ::Entity* insertBeforeEntity = nullptr; if (insertBefore.IsValid()) { EBUS_EVENT_RESULT(insertBeforeEntity, AZ::ComponentApplicationBus, FindEntity, insertBefore); AZ_Assert(insertBeforeEntity, "Entity for insertBefore not found"); } Reparent(newParentEntity, insertBeforeEntity); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::AddToParentAtIndex(AZ::Entity* newParent, int index) { AZ_Assert(!m_parent, "Element already has a parent"); if (!newParent) { if (IsFullyInitialized()) { newParent = GetCanvasComponent()->GetRootElement(); } else { EmitNotInitializedWarning(); return; } } UiElementComponent* newParentElement = newParent->FindComponent(); AZ_Assert(newParentElement, "New parent entity has no UiElementComponent"); AZ::Entity* insertBefore = nullptr; if (index >= 0 && index < newParentElement->GetNumChildElements()) { insertBefore = newParentElement->GetChildElement(index); } newParentElement->AddChild(GetEntity(), insertBefore); SetParentReferences(newParent, newParentElement); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::RemoveFromParent() { if (m_parent) { // remove from parent GetParentElementComponent()->RemoveChild(GetEntity()); SetParentReferences(nullptr, nullptr); } } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::FindFrontmostChildContainingPoint(AZ::Vector2 point, bool isInGame) { if (!IsFullyInitialized()) { return nullptr; } AZ::Entity* matchElem = nullptr; // this traverses all of the elements in reverse hierarchy order and returns the first one that // is containing the point. // If necessary, this could be optimized using a spatial partitioning data structure. for (int i = m_childEntityIdOrder.size() - 1; !matchElem && i >= 0; i--) { AZ::EntityId child = m_childEntityIdOrder[i].m_entityId; if (!isInGame) { // We are in editing mode (not running the game) // Use the UiEditorBus to query any UiEditorComponent on this element to see if this element is // hidden in the editor bool isVisible = true; EBUS_EVENT_ID_RESULT(isVisible, child, UiEditorBus, GetIsVisible); if (!isVisible) { continue; } } UiElementComponent* childElementComponent = GetChildElementComponent(i); // Check children of this child first // child elements do not have to be contained in the parent element's bounds matchElem = childElementComponent->FindFrontmostChildContainingPoint(point, isInGame); if (!matchElem) { bool isSelectable = true; if (!isInGame) { // We are in editing mode (not running the game) // Use the UiEditorBus to query any UiEditorComponent on this element to see if this element // can be selected in the editor EBUS_EVENT_ID_RESULT(isSelectable, child, UiEditorBus, GetIsSelectable); } if (isSelectable) { // if no children of this child matched then check if point is in bounds of this child element bool isPointInRect = childElementComponent->GetTransform2dComponent()->IsPointInRect(point); if (isPointInRect) { matchElem = childElementComponent->GetEntity(); } } } } return matchElem; } //////////////////////////////////////////////////////////////////////////////////////////////////// LyShine::EntityArray UiElementComponent::FindAllChildrenIntersectingRect(const AZ::Vector2& bound0, const AZ::Vector2& bound1, bool isInGame) { LyShine::EntityArray result; if (!IsFullyInitialized()) { return result; } // this traverses all of the elements in hierarchy order for (int i = 0; i < m_childEntityIdOrder.size(); ++i) { AZ::EntityId child = m_childEntityIdOrder[i].m_entityId; if (!isInGame) { // We are in editing mode (not running the game) // Use the UiEditorBus to query any UiEditorComponent on this element to see if this element is // hidden in the editor bool isVisible = true; EBUS_EVENT_ID_RESULT(isVisible, child, UiEditorBus, GetIsVisible); if (!isVisible) { continue; } } UiElementComponent* childElementComponent = GetChildElementComponent(i); // Check children of this child first // child elements do not have to be contained in the parent element's bounds LyShine::EntityArray childMatches = childElementComponent->FindAllChildrenIntersectingRect(bound0, bound1, isInGame); result.push_back(childMatches); bool isSelectable = true; if (!isInGame) { // We are in editing mode (not running the game) // Use the UiEditorBus to query any UiEditorComponent on this element to see if this element // can be selected in the editor EBUS_EVENT_ID_RESULT(isSelectable, child, UiEditorBus, GetIsSelectable); } if (isSelectable) { // check if point is in bounds of this child element bool isInRect = childElementComponent->GetTransform2dComponent()->BoundsAreOverlappingRect(bound0, bound1); if (isInRect) { result.push_back(childElementComponent->GetEntity()); } } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::FindInteractableToHandleEvent(AZ::Vector2 point) { AZ::EntityId result; if (!IsFullyInitialized() || !m_isEnabled) { // Nothing to do return result; } // first check the children (in reverse order) since children are in front of parent { // if this element is masking children at this point then don't check the children bool isMasked = false; EBUS_EVENT_ID_RESULT(isMasked, GetEntityId(), UiInteractionMaskBus, IsPointMasked, point); if (!isMasked) { for (int i = m_childEntityIdOrder.size() - 1; !result.IsValid() && i >= 0; i--) { result = GetChildElementComponent(i)->FindInteractableToHandleEvent(point); } } } // if no match then check this element if (!result.IsValid()) { // if this element has an interactable component and the point is in this element's rect if (UiInteractableBus::FindFirstHandler(GetEntityId())) { bool isInRect = GetTransform2dComponent()->IsPointInRect(point); if (isInRect) { // check if this interactable component is in a state where it can handle an event at the given point bool canHandle = false; EBUS_EVENT_ID_RESULT(canHandle, GetEntityId(), UiInteractableBus, CanHandleEvent, point); if (canHandle) { result = GetEntityId(); } } } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::FindParentInteractableSupportingDrag(AZ::Vector2 point) { AZ::EntityId result; // if this element has a parent and this element is not completely disabled if (m_parent && m_isEnabled) { AZ::EntityId parentEntity = m_parent->GetId(); // if the parent supports drag hand off then return it bool supportsDragOffset = false; EBUS_EVENT_ID_RESULT(supportsDragOffset, parentEntity, UiInteractableBus, DoesSupportDragHandOff, point); if (supportsDragOffset) { // Make sure the parent is also handling events bool handlingEvents = false; EBUS_EVENT_ID_RESULT(handlingEvents, parentEntity, UiInteractableBus, IsHandlingEvents); supportsDragOffset = handlingEvents; } if (supportsDragOffset) { result = parentEntity; } else { // else keep going up the parent links result = GetParentElementComponent()->FindParentInteractableSupportingDrag(point); } } return result; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::FindChildByName(const LyShine::NameType& name) { AZ::Entity* matchElem = nullptr; if (AreChildPointersValid()) { int numChildren = m_childElementComponents.size(); for (int i = 0; i < numChildren; ++i) { AZ::Entity* childEntity = GetChildElementComponent(i)->GetEntity(); if (name == childEntity->GetName()) { matchElem = childEntity; break; } } } else { for (auto& child : m_childEntityIdOrder) { AZ::Entity* childEntity = nullptr; EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, child.m_entityId); if (childEntity && name == childEntity->GetName()) { matchElem = childEntity; break; } } } return matchElem; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::FindDescendantByName(const LyShine::NameType& name) { AZ::Entity* matchElem = nullptr; if (AreChildPointersValid()) { int numChildren = m_childElementComponents.size(); for (int i = 0; i < numChildren; ++i) { UiElementComponent* childElementComponent = GetChildElementComponent(i); AZ::Entity* childEntity = childElementComponent->GetEntity(); if (name == childEntity->GetName()) { matchElem = childEntity; break; } matchElem = childElementComponent->FindDescendantByName(name); if (matchElem) { break; } } } else { for (auto& child : m_childEntityIdOrder) { AZ::Entity* childEntity = nullptr; EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, child.m_entityId); if (childEntity && name == childEntity->GetName()) { matchElem = childEntity; break; } EBUS_EVENT_ID_RESULT(matchElem, child.m_entityId, UiElementBus, FindDescendantByName, name); if (matchElem) { break; } } } return matchElem; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::FindChildEntityIdByName(const LyShine::NameType& name) { AZ::Entity* childEntity = FindChildByName(name); return childEntity ? childEntity->GetId() : AZ::EntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::FindDescendantEntityIdByName(const LyShine::NameType& name) { AZ::Entity* childEntity = FindDescendantByName(name); return childEntity ? childEntity->GetId() : AZ::EntityId(); } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::FindChildByEntityId(AZ::EntityId id) { AZ::Entity* matchElem = nullptr; int numChildren = m_childEntityIdOrder.size(); for (int i = 0; i < numChildren; ++i) { if (id == m_childEntityIdOrder[i].m_entityId) { if (AreChildPointersValid()) { matchElem = GetChildElementComponent(i)->GetEntity(); } else { EBUS_EVENT_RESULT(matchElem, AZ::ComponentApplicationBus, FindEntity, id); } break; } } return matchElem; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::Entity* UiElementComponent::FindDescendantById(LyShine::ElementId id) { if (id == m_elementId) { return GetEntity(); } AZ::Entity* match = nullptr; if (AreChildPointersValid()) { int numChildren = m_childEntityIdOrder.size(); for (int i = 0; !match && i < numChildren; ++i) { match = GetChildElementComponent(i)->FindDescendantById(id); } } else { for (auto iter = m_childEntityIdOrder.begin(); !match && iter != m_childEntityIdOrder.end(); iter++) { EBUS_EVENT_ID_RESULT(match, iter->m_entityId, UiElementBus, FindDescendantById, id); } } return match; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::FindDescendantElements(AZStd::function predicate, LyShine::EntityArray& result) { if (AreChildPointersValid()) { int numChildren = m_childElementComponents.size(); for (int i = 0; i < numChildren; ++i) { UiElementComponent* childElementComponent = GetChildElementComponent(i); AZ::Entity* childEntity = childElementComponent->GetEntity(); if (predicate(childEntity)) { result.push_back(childEntity); } childElementComponent->FindDescendantElements(predicate, result); } } else { for (auto& child : m_childEntityIdOrder) { AZ::Entity* childEntity = nullptr; EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, child.m_entityId); if (childEntity && predicate(childEntity)) { result.push_back(childEntity); } EBUS_EVENT_ID(child.m_entityId, UiElementBus, FindDescendantElements, predicate, result); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::CallOnDescendantElements(AZStd::function callFunction) { if (AreChildPointersValid()) { int numChildren = m_childEntityIdOrder.size(); for (int i = 0; i < numChildren; ++i) { callFunction(m_childEntityIdOrder[i].m_entityId); GetChildElementComponent(i)->CallOnDescendantElements(callFunction); } } else { for (auto& child : m_childEntityIdOrder) { callFunction(child.m_entityId); EBUS_EVENT_ID(child.m_entityId, UiElementBus, CallOnDescendantElements, callFunction); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::IsAncestor(AZ::EntityId id) { UiElementComponent* parentElementComponent = GetParentElementComponent(); while (parentElementComponent) { if (parentElementComponent->GetEntityId() == id) { return true; } parentElementComponent = parentElementComponent->GetParentElementComponent(); } return false; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::IsEnabled() { return m_isEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetIsEnabled(bool isEnabled) { if (isEnabled != m_isEnabled) { m_isEnabled = isEnabled; // Tell any listeners that the enabled state has changed EBUS_EVENT_ID(GetEntityId(), UiElementNotificationBus, OnUiElementEnabledChanged, m_isEnabled); // If the ancestors are not enabled then changing the local flag has no effect on the effective // enabled state. bool areAncestorsEnabled = (m_parentElementComponent) ? m_parentElementComponent->GetAreElementAndAncestorsEnabled() : true; if (areAncestorsEnabled) { // Tell any listeners that the effective enabled state has changed EBUS_EVENT_ID(GetEntityId(), UiElementNotificationBus, OnUiElementAndAncestorsEnabledChanged, m_isEnabled); DoRecursiveEnabledNotification(m_isEnabled); } // tell the canvas to invalidate the render graph if (m_canvas) { m_canvas->MarkRenderGraphDirty(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::GetAreElementAndAncestorsEnabled() { if (!m_isEnabled) { return false; } if (m_parentElementComponent) { return m_parentElementComponent->GetAreElementAndAncestorsEnabled(); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::IsRenderEnabled() { return m_isRenderEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetIsRenderEnabled(bool isRenderEnabled) { m_isRenderEnabled = isRenderEnabled; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::GetIsVisible() { return m_isVisibleInEditor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetIsVisible(bool isVisible) { if (m_isVisibleInEditor != isVisible) { m_isVisibleInEditor = isVisible; if (m_canvas) { // we have to regenerate the graph because different elements are now visible m_canvas->MarkRenderGraphDirty(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::GetIsSelectable() { return m_isSelectableInEditor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetIsSelectable(bool isSelectable) { m_isSelectableInEditor = isSelectable; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::GetIsSelected() { return m_isSelectedInEditor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetIsSelected(bool isSelected) { m_isSelectedInEditor = isSelected; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::GetIsExpanded() { return m_isExpandedInEditor; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetIsExpanded(bool isExpanded) { m_isExpandedInEditor = isExpanded; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::AreAllAncestorsVisible() { UiElementComponent* parentElementComponent = GetParentElementComponent(); while (parentElementComponent) { bool isParentVisible = true; EBUS_EVENT_ID_RESULT(isParentVisible, parentElementComponent->GetEntityId(), UiEditorBus, GetIsVisible); if (!isParentVisible) { return false; } // Walk up the hierarchy. parentElementComponent = parentElementComponent->GetParentElementComponent(); } // there is no ancestor entity that is not visible return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::OnEntityActivated(const AZ::EntityId&) { // cache pointers to the optional render interface and render control interface to // optimize calls rather than using ebus. Both of these buses only allow single handlers. m_renderInterface = UiRenderBus::FindFirstHandler(GetEntityId()); m_renderControlInterface = UiRenderControlBus::FindFirstHandler(GetEntityId()); } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::OnEntityDeactivated(const AZ::EntityId&) { m_renderInterface = nullptr; m_renderControlInterface = nullptr; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::AddChild(AZ::Entity* child, AZ::Entity* insertBefore) { // debug check that this element is not already a child AZ_Assert(FindChildByEntityId(child->GetId()) == nullptr, "Attempting to add a duplicate child"); UiElementComponent* childElementComponent = child->FindComponent(); AZ_Assert(childElementComponent, "Attempting to add a child with no element component"); if (!childElementComponent) { return; } bool wasInserted = false; if (insertBefore) { int numChildren = m_childEntityIdOrder.size(); for (int i = 0; i < numChildren; ++i) { if (m_childEntityIdOrder[i].m_entityId == insertBefore->GetId()) { if (AreChildPointersValid()) // must test before m_childEntityIdOrder.insert { m_childElementComponents.insert(m_childElementComponents.begin() + i, childElementComponent); } m_childEntityIdOrder.insert(m_childEntityIdOrder.begin() + i, {child->GetId(), static_cast(i)}); ResetChildEntityIdSortOrders(); wasInserted = true; break; } } } // either insertBefore is null or it is not found, insert at end if (!wasInserted) { if (AreChildPointersValid()) // must test before m_childEntityIdOrder.push_back { m_childElementComponents.push_back(childElementComponent); } m_childEntityIdOrder.push_back({child->GetId(), m_childEntityIdOrder.size()}); } // Adding or removing child elements may require recomputing the // transforms of all children EBUS_EVENT_ID(GetCanvasEntityId(), UiLayoutManagerBus, MarkToRecomputeLayout, GetEntityId()); EBUS_EVENT_ID(GetCanvasEntityId(), UiLayoutManagerBus, MarkToRecomputeLayoutsAffectedByLayoutCellChange, GetEntityId(), false); // It will always require recomputing the transform for the child just added if (IsFullyInitialized()) { GetTransform2dComponent()->SetRecomputeFlags(UiTransformInterface::Recompute::RectAndTransform); } // tell the canvas to invalidate the render graph if (m_canvas) { m_canvas->MarkRenderGraphDirty(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::RemoveChild(AZ::Entity* child) { // check if the given entity is actually a child, if not then do nothing AZ::EntityId childId = child->GetId(); auto iter = AZStd::find_if(m_childEntityIdOrder.begin(), m_childEntityIdOrder.end(), [childId](ChildEntityIdOrderEntry& entry) { return entry.m_entityId == childId; }); if (iter != m_childEntityIdOrder.end()) { // remove the child from m_childEntityIdOrder m_childEntityIdOrder.erase(iter); // update the sort indices to be contiguous ResetChildEntityIdSortOrders(); UiElementComponent* elementComponent = child->FindComponent(); AZ_Assert(elementComponent, "Child element has no UiElementComponent"); // Also erase from m_childElementComponents stl::find_and_erase(m_childElementComponents, elementComponent); // Clear child's parent elementComponent->SetParentReferences(nullptr, nullptr); // Adding or removing child elements may require recomputing the // transforms of all children EBUS_EVENT_ID(GetCanvasEntityId(), UiLayoutManagerBus, MarkToRecomputeLayout, GetEntityId()); EBUS_EVENT_ID(GetCanvasEntityId(), UiLayoutManagerBus, MarkToRecomputeLayoutsAffectedByLayoutCellChange, GetEntityId(), false); // tell the canvas to invalidate the render graph if (m_canvas) { m_canvas->MarkRenderGraphDirty(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::RemoveChild(AZ::EntityId child) { AZ::EntityId childId = child; auto iter = AZStd::find_if(m_childEntityIdOrder.begin(), m_childEntityIdOrder.end(), [childId](ChildEntityIdOrderEntry& entry) { return entry.m_entityId == childId; }); if (iter != m_childEntityIdOrder.end()) { if (AreChildPointersValid()) { auto childElementiter = AZStd::find_if(m_childElementComponents.begin(), m_childElementComponents.end(), [childId](UiElementComponent* elementComponent) { return elementComponent->GetEntityId() == childId; }); UiElementComponent* elementComponent = childElementiter != m_childElementComponents.end() ? *childElementiter : nullptr; AZ_Assert(elementComponent, ""); if (elementComponent) { stl::find_and_erase(m_childElementComponents, elementComponent); // Clear child's parent elementComponent->SetParentReferences(nullptr, nullptr); } } // remove the child from m_childEntityIdOrder m_childEntityIdOrder.erase(iter); // update the sort indicies to be contiguous ResetChildEntityIdSortOrders(); // Adding or removing child elements may require recomputing the // transforms of all children EBUS_EVENT_ID(GetCanvasEntityId(), UiLayoutManagerBus, MarkToRecomputeLayout, GetEntityId()); EBUS_EVENT_ID(GetCanvasEntityId(), UiLayoutManagerBus, MarkToRecomputeLayoutsAffectedByLayoutCellChange, GetEntityId(), false); // tell the canvas to invalidate the render graph if (m_canvas) { m_canvas->MarkRenderGraphDirty(); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetCanvas(UiCanvasComponent* canvas, LyShine::ElementId elementId) { m_canvas = canvas; m_elementId = elementId; } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::FixupPostLoad(AZ::Entity* entity, UiCanvasComponent* canvas, AZ::Entity* parent, bool makeNewElementIds) { #ifdef AZ_DEBUG_BUILD // check that the m_childEntityIdOrder is ordered such that the m_sortIndex fields are in order and contiguous { int numChildren = m_childEntityIdOrder.size(); for (AZ::u64 index = 0; index < numChildren; ++index) { if (m_childEntityIdOrder[index].m_sortIndex != index) { AZ_Assert(false, "FixupPostLoad: m_childEntityIdOrder bad sort index. This should never happen."); } } } #endif if (makeNewElementIds) { m_elementId = canvas->GenerateId(); } m_canvas = canvas; if (parent) { UiElementComponent* parentElementComponent = parent->FindComponent(); AZ_Assert(parentElementComponent, "Parent element has no UiElementComponent"); SetParentReferences(parent, parentElementComponent); } else { SetParentReferences(nullptr, nullptr); } ChildEntityIdOrderArray missingChildren; for (auto& child : m_childEntityIdOrder) { AZ::Entity* childEntity = nullptr; EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, child.m_entityId); if (!childEntity) { // with slices it is possible for users to get themselves into situations where a child no // longer exists, we should report an error in this case rather than asserting AZ_Error("UI", false, "Child element with Entity ID %llu no longer exists. Data will be lost.", child); // This case could happen if a slice asset has been deleted. We should try to continue and load the // canvas with errors. missingChildren.push_back(child); continue; } UiElementComponent* elementComponent = childEntity->FindComponent(); if (!elementComponent) { // with slices it is possible for users to get themselves into situations where a child no // longer has an element component. In this case report an error and fail to load the data but do not // crash. AZ_Error("UI", false, "Child element with Entity ID %llu no longer has a UiElementComponent. Data cannot be loaded.", child); return false; } bool success = elementComponent->FixupPostLoad(childEntity, canvas, entity, makeNewElementIds); if (!success) { return false; } } // If there were any missing children remove them from the m_childEntityIdOrder list // This is recovery code for the case that a slice asset that we were using has been removed. for (auto child : missingChildren) { stl::find_and_erase(m_childEntityIdOrder, child); } // Initialize the m_childElementComponents array that is used for performance optimization m_childElementComponents.clear(); for (auto child : m_childEntityIdOrder) { AZ::Entity* childEntity = nullptr; EBUS_EVENT_RESULT(childEntity, AZ::ComponentApplicationBus, FindEntity, child.m_entityId); AZ_Assert(childEntity, "Child element not found"); UiElementComponent* childElementComponent = childEntity->FindComponent(); AZ_Assert(childElementComponent, "Child element has no UiElementComponent"); m_childElementComponents.push_back(childElementComponent); } // Tell any listeners that the canvas entity ID for the element is now set, this allows other components to // listen for messages from the canvas AZ::EntityId parentEntityId = (parent) ? parent->GetId() : AZ::EntityId(); EBUS_EVENT_ID(GetEntityId(), UiElementNotificationBus, OnUiElementFixup, canvas->GetEntityId(), parentEntityId); return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// AZ::EntityId UiElementComponent::GetSliceEntityParentId() { return GetParentEntityId(); } AZStd::vector UiElementComponent::GetSliceEntityChildren() { return GetChildEntityIds(); } //////////////////////////////////////////////////////////////////////////////////////////////////// // PUBLIC STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::Reflect(AZ::ReflectContext* context) { AZ::SerializeContext* serializeContext = azrtti_cast(context); if (serializeContext) { serializeContext->Class() // Persistent IDs for this are simply the entity id ->PersistentId([](const void* instance) -> AZ::u64 { const ChildEntityIdOrderEntry* entry = reinterpret_cast(instance); return static_cast(entry->m_entityId); }) ->Version(1) ->Field("ChildEntityId", &ChildEntityIdOrderEntry::m_entityId) ->Field("SortIndex", &ChildEntityIdOrderEntry::m_sortIndex); serializeContext->Class() ->Version(3, &VersionConverter) ->EventHandler() ->Field("Id", &UiElementComponent::m_elementId) ->Field("IsEnabled", &UiElementComponent::m_isEnabled) ->Field("IsVisibleInEditor", &UiElementComponent::m_isVisibleInEditor) ->Field("IsSelectableInEditor", &UiElementComponent::m_isSelectableInEditor) ->Field("IsSelectedInEditor", &UiElementComponent::m_isSelectedInEditor) ->Field("IsExpandedInEditor", &UiElementComponent::m_isExpandedInEditor) ->Field("ChildEntityIdOrder", &UiElementComponent::m_childEntityIdOrder); AZ::EditContext* ec = serializeContext->GetEditContext(); if (ec) { auto editInfo = ec->Class("Element", "Adds UI Element behavior to an entity"); editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiElement.png") ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiElement.png") ->Attribute(AZ::Edit::Attributes::AddableByUser, false) // Cannot be added or removed by user ->Attribute(AZ::Edit::Attributes::AutoExpand, true); editInfo->DataElement("String", &UiElementComponent::m_elementId, "Id", "This read-only ID is used to reference the element from FlowGraph") ->Attribute(AZ::Edit::Attributes::ReadOnly, true) ->Attribute(AZ::Edit::Attributes::SliceFlags, AZ::Edit::SliceFlags::NotPushable); editInfo->DataElement(0, &UiElementComponent::m_isEnabled, "Start enabled", "Determines whether the element is enabled upon creation.\n" "If an element is not enabled, neither it nor any of its children are drawn or interactive."); // These are not visible in the PropertyGrid since they are managed through the Hierarchy Pane // We do want to be able to push them to a slice though. editInfo->DataElement(0, &UiElementComponent::m_isVisibleInEditor, "IsVisibleInEditor", "") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Hide); editInfo->DataElement(0, &UiElementComponent::m_isSelectableInEditor, "IsSelectableInEditor", "") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Hide); editInfo->DataElement(0, &UiElementComponent::m_isExpandedInEditor, "IsExpandedInEditor", "") ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::Hide); } } AZ::BehaviorContext* behaviorContext = azrtti_cast(context); if (behaviorContext) { behaviorContext->EBus("UiElementBus") ->Event("GetName", &UiElementBus::Events::GetName) ->Event("GetCanvas", &UiElementBus::Events::GetCanvasEntityId) ->Event("GetParent", &UiElementBus::Events::GetParentEntityId) ->Event("GetNumChildElements", &UiElementBus::Events::GetNumChildElements) ->Event("GetChild", &UiElementBus::Events::GetChildEntityId) ->Event("GetIndexOfChildByEntityId", &UiElementBus::Events::GetIndexOfChildByEntityId) ->Event("GetChildren", &UiElementBus::Events::GetChildEntityIds) ->Event("DestroyElement", &UiElementBus::Events::DestroyElementOnFrameEnd) ->Event("Reparent", &UiElementBus::Events::ReparentByEntityId) ->Event("FindChildByName", &UiElementBus::Events::FindChildEntityIdByName) ->Event("FindDescendantByName", &UiElementBus::Events::FindDescendantEntityIdByName) ->Event("IsAncestor", &UiElementBus::Events::IsAncestor) ->Event("IsEnabled", &UiElementBus::Events::IsEnabled) ->Event("SetIsEnabled", &UiElementBus::Events::SetIsEnabled); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::Initialize() { } //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::MoveEntityAndDescendantsToListAndReplaceWithEntityId(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& elementNode, int index, AZStd::vector& entities) { // Find the UiElementComponent on this entity AZ::SerializeContext::DataElementNode* elementComponentNode = LyShine::FindComponentNode(elementNode, UiElementComponent::TYPEINFO_Uuid()); if (!elementComponentNode) { return false; } // We must process the children first so that when we make a copy of this entity to the entities list // it will already have had its child entities replaced with entity IDs // find the m_children field int childrenIndex = elementComponentNode->FindElement(AZ_CRC("Children", 0xa197b1ba)); if (childrenIndex == -1) { return false; } AZ::SerializeContext::DataElementNode& childrenNode = elementComponentNode->GetSubElement(childrenIndex); // Create the child entities member (which is a generic vector) AZ::SerializeContext::ClassData* classData = AZ::SerializeGenericTypeInfo::GetGenericInfo()->GetClassData(); int newChildrenIndex = elementComponentNode->AddElement(context, "ChildEntityIdOrder", *classData); if (newChildrenIndex == -1) { return false; } AZ::SerializeContext::DataElementNode& newChildrenNode = elementComponentNode->GetSubElement(newChildrenIndex); // iterate through children and recursively call this function int numChildren = childrenNode.GetNumSubElements(); for (int childIndex = 0; childIndex < numChildren; ++childIndex) { AZ::SerializeContext::DataElementNode& childElementNode = childrenNode.GetSubElement(childIndex); MoveEntityAndDescendantsToListAndReplaceWithEntityId(context, childElementNode, childIndex, entities); newChildrenNode.AddElement(childElementNode); } // delete the original "Children" node, we have replaced it with the "ChildEntityIdOrder" node elementComponentNode->RemoveElement(childrenIndex); // the children list has now been processed so it will now just contain entity IDs // Now copy this node (elementNode) to the list we are building and then replace it // with an Entity ID node // copy this node to the list entities.push_back(elementNode); // Remember the name of this node (it could be "element" or "RootElement" for example) AZStd::string elementFieldName = elementNode.GetNameString(); // Find the EntityId node within this entity int entityIdIndex = elementNode.FindElement(AZ_CRC("Id", 0xbf396750)); if (entityIdIndex == -1) { return false; } AZ::SerializeContext::DataElementNode& elementIdNode = elementNode.GetSubElement(entityIdIndex); // Find the sub node of the EntityID that actually stores the u64 and make a copy of it int u64Index = elementIdNode.FindElement(AZ_CRC("id", 0xbf396750)); if (u64Index == -1) { return false; } AZ::SerializeContext::DataElementNode u64Node = elementIdNode.GetSubElement(u64Index); // -1 indicates this is the root element reference if (index == -1) { // Convert this node (which was an entire Entity) into just an EntityId, keeping the same // node name as it had elementNode.Convert(context, elementFieldName.c_str()); // copy in the subNode that stores the actual u64 (that we saved a copy of above) int newEntityIdIndex = elementNode.AddElement(u64Node); } else { // Convert this node (which was an entire Entity) into just an ChildEntityIdOrderEntry, keeping the same // node name as it had elementNode.Convert(context, elementFieldName.c_str()); // add sub element from the entity Id int childOrderEntryEntityIdIndex = elementNode.AddElement(context, "ChildEntityId"); AZ::SerializeContext::DataElementNode& childOrderEntryEntityIdElementNode = elementNode.GetSubElement(childOrderEntryEntityIdIndex); // copy in the subNode that stores the actual u64 (that we saved a copy of above) int newEntityIdIndex = childOrderEntryEntityIdElementNode.AddElement(u64Node); AZ::u64 sortIndex = static_cast(index); int sortIndexElementIndex = elementNode.AddElementWithData(context, "SortIndex", sortIndex); } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// // PROTECTED MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::Activate() { UiElementBus::Handler::BusConnect(m_entity->GetId()); UiEditorBus::Handler::BusConnect(m_entity->GetId()); AZ::SliceEntityHierarchyRequestBus::Handler::BusConnect(m_entity->GetId()); AZ::EntityBus::Handler::BusConnect(m_entity->GetId()); // Once added the transform component is never removed if (!m_transformComponent) { m_transformComponent = GetEntity()->FindComponent(); } // tell the canvas to invalidate the render graph if (m_canvas) { m_canvas->MarkRenderGraphDirty(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::Deactivate() { UiElementBus::Handler::BusDisconnect(); UiEditorBus::Handler::BusDisconnect(); AZ::SliceEntityHierarchyRequestBus::Handler::BusDisconnect(); AZ::EntityBus::Handler::BusDisconnect(); // tell the canvas to invalidate the render graph if (m_canvas) { m_canvas->MarkRenderGraphDirty(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::DoRecursiveEnabledNotification(bool newIsEnabledValue) { for (UiElementComponent* child : m_childElementComponents) { // if this child element is disabled then the enabled state of the ancestors makes no difference // but if it is enabled then its effective enabled state is controlled by its ancestors if (child->m_isEnabled) { EBUS_EVENT_ID(child->GetEntityId(), UiElementNotificationBus, OnUiElementAndAncestorsEnabledChanged, newIsEnabledValue); child->DoRecursiveEnabledNotification(newIsEnabledValue); } } } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::EmitNotInitializedWarning() const { AZ_Warning("UI", false, "UiElementComponent used before fully initialized, possibly on activate before FixupPostLoad was called on this element") } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::SetParentReferences(AZ::Entity* parent, UiElementComponent* parentElementComponent) { m_parent = parent; m_parentId = (parent) ? parent->GetId() : AZ::EntityId(); m_parentElementComponent = parentElementComponent; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::OnPatchEnd(const AZ::DataPatchNodeInfo& patchInfo) { // We want to check the data patch for any patching of the "Children" element. The m_children element no // longer exists so we want to make the equivalent changes to the m_childEntityIdOrder element. // The relevant patch addresses can be either // a) a change of an element in the container // b) a removal of an element in the container (these are always higher indices than the changes) // c) an addition of an element in the container (these are not always in ascending order, it is an unordered map) // (these are always higher indices than the changes) // // For a given patch there will never be both addition and removals. // // For b and c the patch address (in childPatchLookup) will be patchinfo.address + "Children". // We could find all of those through one call to "find" on childPatchLookup with that address. // However, for the "a" case the address (in childPatchLookup) will have an additional element on // the end - since it is the "Id" field within the EntityId that is being patched. // So we have to iterate through childPatchLookup anyway, so we do that for all cases. using EntityIndexPair = AZStd::pair; using EntityIndexPairList = AZStd::vector; EntityIndexPairList elementsChanged; EntityIndexPairList elementsAdded; AZStd::vector elementsRemoved; bool oldChildrenDataPatchFound = false; const AZ::DataPatch::AddressType& address = patchInfo.address; const AZ::DataPatch::PatchMap& patch = patchInfo.patch; const AZ::DataPatch::ChildPatchMap& childPatchLookup = patchInfo.childPatchLookup; // Build the address of the "Children" element within this UiElementComponent AZ::DataPatch::AddressType childrenAddress = address; childrenAddress.push_back(AZ_CRC("Children", 0xa197b1ba)); // Get the serialize context for use in the LoadObjectFromStreamInPlace calls AZ::SerializeContext* serializeContext = nullptr; AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext); // childPatchLookup contains all addresses in the patch that are within the UiElementComponent so // it is slightly faster to iterate over that than over "patch" directly. for (auto& childPatchPair : childPatchLookup) { const AZ::DataPatch::AddressType& lookupAddress = childPatchPair.first; if (lookupAddress == childrenAddress) { // The address matches the "Children" container exactly, so get childPatches which will contain // all the additions and removals to the container. const AZStd::vector& childPatches = childPatchPair.second; for (auto& childPatchAddress : childPatches) { auto foundPatchIt = patch.find(childPatchAddress); if (foundPatchIt == patch.end()) { // this should never happen, ignore it if it does continue; } // the last part of the address is the index in the m_children array AZ::u64 index = childPatchAddress.back().GetAddressElement(); if (foundPatchIt->second.empty()) { // this is removal of element (actual patch is empty) oldChildrenDataPatchFound = true; elementsRemoved.push_back(index); } else { // This is an addition // get the EntityId out of the patch value AZ::EntityId entityId; bool entityIdLoaded = false; // If the patch originated in a Legacy DataPatch then we must first load the EntityId from the legacy stream if (foundPatchIt->second.type() == azrtti_typeid()) { const AZ::DataPatch::LegacyStreamWrapper* wrapper = AZStd::any_cast(&foundPatchIt->second); if (wrapper) { AZ::IO::MemoryStream stream(wrapper->m_stream.data(), wrapper->m_stream.size()); entityIdLoaded = AZ::Utils::LoadObjectFromStreamInPlace(stream, entityId, serializeContext); } } else { // Otherwise we can acquire the EntityId from the patch directly const AZ::EntityId* entityIdPtr = AZStd::any_cast(&foundPatchIt->second); if (entityIdPtr) { entityId = *entityIdPtr; entityIdLoaded = true; } } if (entityIdLoaded) { oldChildrenDataPatchFound = true; elementsAdded.push_back({index, entityId}); } else { AZ_Error("UI", false, "UiElement::OnPatchEnd: Failed to load a child entity Id from DataPatch"); } } } } else if (lookupAddress.size() == childrenAddress.size() + 1) { // the lookupAddress is the same length as the "Children" address plus an index // check if the address is childrenAddress plus an extra element bool match = true; for (int i = childrenAddress.size() - 1; i >= 0; --i) { if (lookupAddress[i] != childrenAddress[i]) { match = false; break; } } if (!match) { continue; } // childPatches will be any patches to this one element in the children array (should only ever be one element in the map) const AZStd::vector& childPatches = childPatchPair.second; for (auto& childPatchAddress : childPatches) { auto foundPatchIt = patch.find(childPatchAddress); if (foundPatchIt == patch.end()) { // this should never happen, ignore it if it does continue; } if (foundPatchIt->second.empty()) { // this is removal of element (actual patch is empty). Should never occur in this path. Ignore. continue; } // This should be the u64 "Id" element of the EntityId, if not ignore. if (childPatchAddress.back().GetAddressElement() == AZ_CRC("Id", 0xbf396750)) { // the second to last part of the address is the index in the m_children array AZ::u64 index = childPatchAddress[childPatchAddress.size() - 2].GetAddressElement(); // extract the u64 from the patch value AZ::u64 id = 0; bool idLoaded = false; // If the patch originated in a Legacy DataPatch then we must first load the u64 from the legacy stream if (foundPatchIt->second.type() == azrtti_typeid()) { const AZ::DataPatch::LegacyStreamWrapper* wrapper = AZStd::any_cast(&foundPatchIt->second); if (wrapper) { AZ::IO::MemoryStream stream(wrapper->m_stream.data(), wrapper->m_stream.size()); idLoaded = AZ::Utils::LoadObjectFromStreamInPlace(stream, id, serializeContext); } } else { // Otherwise we can acquire the EntityId from the patch directly const AZ::u64* idPtr = AZStd::any_cast(&foundPatchIt->second); if (idPtr) { id = *idPtr; idLoaded = true; } } if (idLoaded) { AZ::EntityId entityId(id); oldChildrenDataPatchFound = true; elementsChanged.push_back({index, entityId}); } else { AZ_Error("UI", false, "UiElement::OnPatchEnd: Failed to load a child entity Id from DataPatch"); } } } } } // if patch data for the old "Children" container was found then apply it to the new m_childEntityIdOrder vector if (oldChildrenDataPatchFound) { if (elementsAdded.size() > 0 && elementsRemoved.size() > 0) { AZ_Error("UI", false, "OnPatchEnd: can't add and remove in the same patch"); } // removing elements always removes from the end. So we just need to resize to the lowest index for (AZ::u64 index : elementsRemoved) { if (index < m_childEntityIdOrder.size()) { m_childEntityIdOrder.resize(index); } } for (auto& elementChanged : elementsChanged) { AZ::u64 index = elementChanged.first; if (index < m_childEntityIdOrder.size()) { m_childEntityIdOrder[index].m_entityId = elementChanged.second; } else { // index is off the end of m_childEntityIdOrder, this can happen because // elements could be been removed from the slice. But since this override has changed // the entityId we do not want to remove it. So add at end. m_childEntityIdOrder.push_back({elementChanged.second, m_childEntityIdOrder.size()}); } } // sort the added elements by index AZStd::sort(elementsAdded.begin(), elementsAdded.end()); for (auto& elementAdded : elementsAdded) { // elements could have been added or removed in the slice so we don't require that there must be an element 3 // to add element 4, if not we just add it at the end. m_childEntityIdOrder.push_back({elementAdded.second, m_childEntityIdOrder.size()}); } } // regardless of whether the old m_children was in the patch we always sort m_childEntityIdOrder and reassign sort indices after // patching to maintain a consecutive set of sort indices // This will sort all the entity order entries by sort index (primary) and entity id (secondary) which should never result in any collisions // This is used since slice data patching may create duplicate entries for the same sort index, missing indices and the like. // It should never result in multiple entity id entries since the serialization of this data uses a persistent id which is the entity id int numChildren = m_childEntityIdOrder.size(); if (numChildren > 0) { AZStd::sort(m_childEntityIdOrder.begin(), m_childEntityIdOrder.end()); ResetChildEntityIdSortOrders(); } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::ResetChildEntityIdSortOrders() { // Set the sortIndex on each child to match the order in the vector for (AZ::u64 childIndex = 0; childIndex < m_childEntityIdOrder.size(); ++childIndex) { m_childEntityIdOrder[childIndex].m_sortIndex = childIndex; } } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::PrepareElementForDestroy() { // destroy child elements, this is complicated by the fact that the child elements // will attempt to remove themselves from the m_childEntityIdOrder list in their DestroyElement method. // But, if the entities are not initialized yet the child parent pointer will be null. // So the child may or may not remove itself from the list. // So make a local copy of the list and iterate on that if (AreChildPointersValid()) { auto childElementComponents = m_childElementComponents; for (auto child : childElementComponents) { // destroy the child child->DestroyElement(); } } else { auto children = m_childEntityIdOrder; // need a copy for (auto& child : children) { // destroy the child UiElementBus::Event(child.m_entityId, &UiElementBus::Events::DestroyElement); } } // remove this element from parent if (m_parent) { GetParentElementComponent()->RemoveChild(GetEntity()); } // Notify listeners that the element is being destroyed UiElementNotificationBus::Event(GetEntityId(), &UiElementNotificationBus::Events::OnUiElementBeingDestroyed); } //////////////////////////////////////////////////////////////////////////////////////////////////// // PRIVATE STATIC MEMBER FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////// bool UiElementComponent::VersionConverter(AZ::SerializeContext& context, AZ::SerializeContext::DataElementNode& classElement) { // conversion from version 1 to 2: if (classElement.GetVersion() < 2) { // No need to actually convert anything because the CanvasFileObject takes care of it // But it makes sense to bump the version number because m_children is now a container // of EntityId rather than Entity* } // conversion from version 2 to 3: // m_children replaced with m_childEntityIdOrder // NOTE: We do not go through here if version is 1 since m_children will be an array of Entity* // rather than EntityId. That complex conversion is handled in the recursive function // MoveEntityAndDescendantsToListAndReplaceWithEntityId if (classElement.GetVersion() == 2) { // Version 3 added the persistent member m_childEntityIdOrder with replaces m_children // Find the "Children" element that we will be replacing. int childrenIndex = classElement.FindElement(AZ_CRC("Children")); if (childrenIndex != -1) { AZ::SerializeContext::DataElementNode& childrenElementNode = classElement.GetSubElement(childrenIndex); // add the new "ChildEntityIdOrder" element, this is a container int childOrderIndex = classElement.AddElement(context, "ChildEntityIdOrder"); AZ::SerializeContext::DataElementNode& childOrderElementNode = classElement.GetSubElement(childOrderIndex); int numChildren = childrenElementNode.GetNumSubElements(); // for each EntityId in the Children container create a ChildEntityIdOrderEntry in the ChildEntityIdOrder container for (int childIndex = 0; childIndex < numChildren; ++childIndex) { AZ::SerializeContext::DataElementNode& childElementNode = childrenElementNode.GetSubElement(childIndex); // add the entry in the container (or type ChildEntityIdOrderEntry which is a struct of EntityId and u64) int childOrderEntryIndex = childOrderElementNode.AddElement(context, "element"); AZ::SerializeContext::DataElementNode& childOrderEntryElementNode = childOrderElementNode.GetSubElement(childOrderEntryIndex); // copy the EntityId node from the Children container and change its name int childOrderEntryEntityIdIndex = childOrderEntryElementNode.AddElement(childElementNode); AZ::SerializeContext::DataElementNode& childOrderEntryEntityIdElementNode = childOrderEntryElementNode.GetSubElement(childOrderEntryEntityIdIndex); childOrderEntryEntityIdElementNode.SetName("ChildEntityId"); // add the the sort index - which is just the position in the container when we are converting old data. AZ::u64 sortIndex = childIndex; childOrderEntryElementNode.AddElementWithData(context, "SortIndex", sortIndex); } // remove the old m_children persistent member classElement.RemoveElementByName(AZ_CRC("Children")); } } return true; } //////////////////////////////////////////////////////////////////////////////////////////////////// void UiElementComponent::DestroyElementEntity(AZ::EntityId entityId) { AzFramework::EntityContextId contextId = AzFramework::EntityContextId::CreateNull(); AzFramework::EntityIdContextQueryBus::EventResult(contextId, entityId, &AzFramework::EntityIdContextQueryBus::Events::GetOwningContextId); UiEntityContextRequestBus::Event(contextId, &UiEntityContextRequestBus::Events::DestroyUiEntity, entityId); }