You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1127 lines
48 KiB
C++
1127 lines
48 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
#include "EditorCommon.h"
|
|
#include <AzCore/Component/Entity.h>
|
|
#include <AzCore/Component/EntityUtils.h>
|
|
#include <AzCore/Component/ComponentApplicationBus.h>
|
|
#include <AzCore/Component/TransformBus.h>
|
|
#include <AzCore/Component/ComponentExport.h>
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
#include <AzCore/IO/ByteContainerStream.h>
|
|
#include <AzCore/IO/FileIO.h>
|
|
#include <AzCore/Serialization/Utils.h>
|
|
#include <AzCore/Math/Transform.h>
|
|
#include <AzCore/Asset/AssetManager.h>
|
|
|
|
#include <AzFramework/API/ApplicationAPI.h>
|
|
#include <AzFramework/Entity/EntityContext.h>
|
|
#include <AzFramework/Entity/GameEntityContextBus.h>
|
|
#include <AzFramework/Asset/AssetCatalogBus.h>
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
#include <AzToolsFramework/Slice/SliceCompilation.h>
|
|
#include <AzToolsFramework/ToolsComponents/EditorOnlyEntityComponent.h>
|
|
|
|
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
|
|
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
|
|
|
|
#include <LyShine/Bus/UiElementBus.h>
|
|
#include <LyShine/Bus/UiTransformBus.h>
|
|
#include <LyShine/Bus/Tools/UiSystemToolsBus.h>
|
|
#include <LyShine/UiComponentTypes.h>
|
|
|
|
#include "UiEditorEntityContext.h"
|
|
|
|
namespace Internal
|
|
{
|
|
void RemoveIncompatibleComponents(AZ::Entity* entity)
|
|
{
|
|
const AZ::Entity::ComponentArrayType components = entity->GetComponents();
|
|
AZ::Entity::ComponentArrayType validComponents;
|
|
AZ::Entity::ComponentArrayType incompatibleComponents;
|
|
AZ::ComponentDescriptor::DependencyArrayType incompatibleServices;
|
|
AZ::ComponentDescriptor::DependencyArrayType providedServices;
|
|
AZStd::string incompatibleNames;
|
|
for (auto component : components)
|
|
{
|
|
AZ::ComponentDescriptor* testComponentDesc = nullptr;
|
|
AZ::ComponentDescriptorBus::EventResult(testComponentDesc, azrtti_typeid(component), &AZ::ComponentDescriptorBus::Events::GetDescriptor);
|
|
providedServices.clear();
|
|
testComponentDesc->GetProvidedServices(providedServices, component);
|
|
|
|
bool isIncompatible = false;
|
|
for (auto validComponent : validComponents)
|
|
{
|
|
AZ::ComponentDescriptor* validComponentDesc = nullptr;
|
|
AZ::ComponentDescriptorBus::EventResult(validComponentDesc, azrtti_typeid(validComponent), &AZ::ComponentDescriptorBus::Events::GetDescriptor);
|
|
|
|
incompatibleServices.clear();
|
|
validComponentDesc->GetIncompatibleServices(incompatibleServices, validComponent);
|
|
|
|
auto foundItr = AZStd::find_first_of(incompatibleServices.begin(), incompatibleServices.end(), providedServices.begin(), providedServices.end());
|
|
if (foundItr != incompatibleServices.end())
|
|
{
|
|
isIncompatible = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isIncompatible)
|
|
{
|
|
incompatibleComponents.push_back(component);
|
|
|
|
incompatibleNames.append(testComponentDesc->GetName());
|
|
incompatibleNames += '\n';
|
|
}
|
|
else
|
|
{
|
|
validComponents.push_back(component);
|
|
}
|
|
}
|
|
|
|
// Should be safe to remove components, because the entity hasn't been activated.
|
|
for (auto componentToRemove : incompatibleComponents)
|
|
{
|
|
entity->RemoveComponent(componentToRemove);
|
|
}
|
|
|
|
AZ_Error("UiCanvas", incompatibleComponents.empty(), "The following incompatible component(s) are removed from the entity %s:\n%s", entity->GetName().c_str(), incompatibleNames.c_str());
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiEditorEntityContext::UiEditorEntityContext(EditorWindow* editorWindow)
|
|
: m_editorWindow(editorWindow)
|
|
, m_requiredEditorComponentTypes
|
|
({
|
|
azrtti_typeid<AzToolsFramework::Components::EditorOnlyEntityComponent>()
|
|
})
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiEditorEntityContext::~UiEditorEntityContext()
|
|
{
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::HandleLoadedRootSliceEntity(AZ::Entity* rootEntity, bool remapIds, AZ::SliceComponent::EntityIdToEntityIdMap* idRemapTable)
|
|
{
|
|
AZ_Assert(m_entityOwnershipService->IsInitialized(), "The context has not been initialized.");
|
|
|
|
bool rootEntityReloadSuccessful = false;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootEntityReloadSuccessful, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::HandleRootEntityReloadedFromStream, rootEntity, remapIds, idRemapTable);
|
|
|
|
if (!rootEntityReloadSuccessful)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AZ::SliceComponent::EntityList entities;
|
|
m_entityOwnershipService->GetAllEntities(entities);
|
|
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::Event(GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequests::SetIsDynamic, true);
|
|
|
|
InitializeEntities(entities);
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::InitUiContext()
|
|
{
|
|
m_entityOwnershipService = AZStd::make_unique<AzFramework::SliceEntityOwnershipService>(GetContextId(), GetSerializeContext());
|
|
InitContext();
|
|
|
|
// Since root asset initialization happens in EntityOwnershipService and since this class is not inheriting from it,
|
|
// we need to now connect to the asset bus using the root asset id here.
|
|
AzFramework::RootSliceAsset rootSliceAsset;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSliceAsset, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootAsset);
|
|
m_rootAssetId = rootSliceAsset->GetId();
|
|
AZ::Data::AssetBus::MultiHandler::BusConnect(m_rootAssetId);
|
|
|
|
m_entityOwnershipService->InstantiateAllPrefabs();
|
|
|
|
UiEntityContextRequestBus::Handler::BusConnect(GetContextId());
|
|
|
|
UiEditorEntityContextRequestBus::Handler::BusConnect(GetContextId());
|
|
|
|
AzToolsFramework::EditorEntityContextPickingRequestBus::Handler::BusConnect(GetContextId());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::DestroyUiContext()
|
|
{
|
|
UiEditorEntityContextRequestBus::Handler::BusDisconnect();
|
|
|
|
UiEntityContextRequestBus::Handler::BusDisconnect();
|
|
|
|
AzToolsFramework::EditorEntityContextPickingRequestBus::Handler::BusDisconnect();
|
|
|
|
AZ::Data::AssetBus::MultiHandler::BusDisconnect(m_rootAssetId);
|
|
|
|
DestroyContext();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::SaveToStreamForGame(AZ::IO::GenericStream& stream, AZ::DataStream::StreamType streamType)
|
|
{
|
|
AZ::SliceComponent::EntityList sourceEntities;
|
|
m_entityOwnershipService->GetAllEntities(sourceEntities);
|
|
|
|
// Create a source slice from our editor components.
|
|
AZ::Entity* sourceSliceEntity = aznew AZ::Entity();
|
|
AZ::SliceComponent* sourceSliceData = sourceSliceEntity->CreateComponent<AZ::SliceComponent>();
|
|
AZ::Data::Asset<AZ::SliceAsset> sourceSliceAsset(aznew AZ::SliceAsset(), AZ::Data::AssetLoadBehavior::Default);
|
|
sourceSliceAsset.Get()->SetData(sourceSliceEntity, sourceSliceData);
|
|
|
|
for (AZ::Entity* sourceEntity : sourceEntities)
|
|
{
|
|
sourceSliceData->AddEntity(sourceEntity);
|
|
}
|
|
|
|
// Emulate client flags.
|
|
AZ::PlatformTagSet platformTags = { AZ_CRC("renderer", 0xf199a19c) };
|
|
|
|
// Compile the source slice into the runtime slice (with runtime components).
|
|
AzToolsFramework::UiEditorOnlyEntityHandler uiEditorOnlyEntityHandler;
|
|
AzToolsFramework::EditorOnlyEntityHandlers handlers =
|
|
{
|
|
&uiEditorOnlyEntityHandler,
|
|
};
|
|
AzToolsFramework::SliceCompilationResult sliceCompilationResult = CompileEditorSlice(sourceSliceAsset, platformTags, *m_serializeContext, handlers);
|
|
|
|
// Reclaim entities from the temporary source asset.
|
|
for (AZ::Entity* sourceEntity : sourceEntities)
|
|
{
|
|
sourceSliceData->RemoveEntity(sourceEntity, false);
|
|
}
|
|
|
|
if (!sliceCompilationResult)
|
|
{
|
|
m_errorMessage = sliceCompilationResult.GetError();
|
|
return false;
|
|
}
|
|
|
|
// Export runtime slice representing the level, which is a completely flat list of entities.
|
|
AZ::Data::Asset<AZ::SliceAsset> exportSliceAsset = sliceCompilationResult.GetValue();
|
|
AZ::Entity* exportSliceAssetEntity = exportSliceAsset.Get()->GetEntity();
|
|
const bool saveObjectSuccess = AZ::Utils::SaveObjectToStream<AZ::Entity>(stream, streamType, exportSliceAssetEntity);
|
|
|
|
AZ::SliceComponent* sliceComponent = exportSliceAssetEntity->FindComponent<AZ::SliceComponent>();
|
|
AZ::SliceComponent::EntityList sliceEntities;
|
|
|
|
const bool getEntitiesSuccess = sliceComponent->GetEntities(sliceEntities);
|
|
const bool sliceEntitiesValid = getEntitiesSuccess && sliceEntities.size() > 0;
|
|
|
|
if (!sliceEntitiesValid)
|
|
{
|
|
AZ_Error("Save Runtime Stream", false, "Failed to export entities for runtime:\n%s", sliceCompilationResult.GetError().c_str());
|
|
return false;
|
|
}
|
|
|
|
return saveObjectSuccess;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::SaveCanvasEntityToStreamForGame(AZ::Entity* canvasEntity, AZ::IO::GenericStream& stream, AZ::DataStream::StreamType streamType)
|
|
{
|
|
AZ::Entity* sourceCanvasEntity = canvasEntity;
|
|
AZ::Entity* exportCanvasEntity = aznew AZ::Entity(sourceCanvasEntity->GetName().c_str());
|
|
exportCanvasEntity->SetId(sourceCanvasEntity->GetId());
|
|
AZ_Assert(exportCanvasEntity, "Failed to create target entity \"%s\" for export.",
|
|
sourceCanvasEntity->GetName().c_str());
|
|
|
|
EBUS_EVENT(AzToolsFramework::ToolsApplicationRequests::Bus, PreExportEntity,
|
|
*sourceCanvasEntity,
|
|
*exportCanvasEntity);
|
|
|
|
// Export entity representing the canvas, which has only runtime components.
|
|
AZ::Utils::SaveObjectToStream<AZ::Entity>(stream, streamType, exportCanvasEntity);
|
|
|
|
EBUS_EVENT(AzToolsFramework::ToolsApplicationRequests::Bus, PostExportEntity,
|
|
*sourceCanvasEntity,
|
|
*exportCanvasEntity);
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Entity* UiEditorEntityContext::CreateUiEntity(const char* name)
|
|
{
|
|
AZ::Entity* entity = CreateEntity(name);
|
|
|
|
if (entity)
|
|
{
|
|
// we don't currently do anything extra here, UI entities are not automatically
|
|
// Init'ed and Activate'd when they are created. We wait until the required components
|
|
// are added before Init and Activate
|
|
}
|
|
|
|
return entity;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::SliceComponent* UiEditorEntityContext::GetUiRootSlice()
|
|
{
|
|
AZ::SliceComponent* rootSlice = nullptr;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSlice, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
|
|
return rootSlice;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::AddUiEntity(AZ::Entity* entity)
|
|
{
|
|
AZ_Assert(entity, "Supplied entity is invalid.");
|
|
|
|
AddEntity(entity);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::AddUiEntities(const AzFramework::EntityList& entities)
|
|
{
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
AZ_Assert(!AzFramework::EntityIdContextQueryBus::MultiHandler::BusIsConnectedId(entity->GetId()), "Entity already in context.");
|
|
AzFramework::RootSliceAsset rootSliceAsset;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSliceAsset, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootAsset);
|
|
rootSliceAsset->GetComponent()->AddEntity(entity);
|
|
}
|
|
|
|
m_entityOwnershipService->HandleEntitiesAdded(entities);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::CloneUiEntities(const AZStd::vector<AZ::EntityId>& sourceEntities, AzFramework::EntityList& resultEntities)
|
|
{
|
|
resultEntities.clear();
|
|
|
|
AZ::SliceComponent::InstantiatedContainer sourceObjects(false);
|
|
for (const AZ::EntityId& id : sourceEntities)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, id);
|
|
if (entity)
|
|
{
|
|
sourceObjects.m_entities.push_back(entity);
|
|
}
|
|
}
|
|
|
|
AZ::SliceComponent::EntityIdToEntityIdMap idMap;
|
|
AZ::SliceComponent::InstantiatedContainer* clonedObjects =
|
|
AZ::EntityUtils::CloneObjectAndFixEntities(&sourceObjects, idMap);
|
|
if (!clonedObjects)
|
|
{
|
|
AZ_Error("UiEntityContext", false, "Failed to clone source entities.");
|
|
return false;
|
|
}
|
|
|
|
resultEntities = clonedObjects->m_entities;
|
|
|
|
AddUiEntities(resultEntities);
|
|
|
|
clonedObjects->m_deleteEntitiesOnDestruction = false;
|
|
delete clonedObjects;
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::DestroyUiEntity(AZ::EntityId entityId)
|
|
{
|
|
return DestroyEntityById(entityId);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::SupportsViewportEntityIdPicking()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::SliceComponent::SliceInstanceAddress UiEditorEntityContext::CloneEditorSliceInstance(
|
|
[[maybe_unused]] AZ::SliceComponent::SliceInstanceAddress sourceInstance)
|
|
{
|
|
return AZ::SliceComponent::SliceInstanceAddress();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AzFramework::SliceInstantiationTicket UiEditorEntityContext::InstantiateEditorSlice(const AZ::Data::Asset<AZ::Data::AssetData>& sliceAsset, AZ::Vector2 viewportPosition)
|
|
{
|
|
return InstantiateEditorSliceAtChildIndex(sliceAsset, viewportPosition, -1);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AzFramework::SliceInstantiationTicket UiEditorEntityContext::InstantiateEditorSliceAtChildIndex(const AZ::Data::Asset<AZ::Data::AssetData>& sliceAsset,
|
|
AZ::Vector2 viewportPosition,
|
|
int childIndex)
|
|
{
|
|
if (sliceAsset.GetId().IsValid())
|
|
{
|
|
InstantiatingEditorSliceParams instantiatingSliceParams(viewportPosition, childIndex);
|
|
m_instantiatingSlices.push_back(AZStd::make_pair(sliceAsset, instantiatingSliceParams));
|
|
|
|
AzFramework::SliceInstantiationTicket ticket;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(ticket, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::InstantiateSlice, sliceAsset, nullptr, nullptr);
|
|
if (ticket.IsValid())
|
|
{
|
|
AzFramework::SliceInstantiationResultBus::MultiHandler::BusConnect(ticket);
|
|
}
|
|
|
|
return ticket;
|
|
}
|
|
|
|
return AzFramework::SliceInstantiationTicket();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::RestoreSliceEntity(AZ::Entity* entity, const AZ::SliceComponent::EntityRestoreInfo& info)
|
|
{
|
|
AZ_Error("EditorEntityContext", info.m_assetId.IsValid(), "Invalid asset Id for entity restore.");
|
|
|
|
// If asset isn't loaded when this request is made, we need to queue the load and process the request
|
|
// when the asset is ready. Otherwise we'll immediately process the request when OnAssetReady is invoked
|
|
// by the AssetBus connection policy.
|
|
|
|
AZ::Data::Asset<AZ::Data::AssetData> asset =
|
|
AZ::Data::AssetManager::Instance().GetAsset<AZ::SliceAsset>(info.m_assetId, AZ::Data::AssetLoadBehavior::Default);
|
|
|
|
SliceEntityRestoreRequest request = {entity, info, asset};
|
|
m_queuedSliceEntityRestores.emplace_back(request);
|
|
|
|
AZ::Data::AssetBus::MultiHandler::BusConnect(asset.GetId());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::QueueSliceReplacement(const char* targetPath,
|
|
const AZStd::unordered_map<AZ::EntityId, AZ::EntityId>& selectedToAssetMap,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesInSelection,
|
|
AZ::Entity* commonParent, AZ::Entity* insertBefore)
|
|
{
|
|
AZ_Error("EditorEntityContext", m_queuedSliceReplacement.m_path.empty(), "A slice replacement is already on the queue.");
|
|
|
|
m_queuedSliceReplacement.Setup(targetPath, selectedToAssetMap, entitiesInSelection, commonParent, insertBefore);
|
|
|
|
AzFramework::AssetCatalogEventBus::Handler::BusConnect();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::DeleteElements(AzToolsFramework::EntityIdList elements)
|
|
{
|
|
// Deletes the specified elements using an undoable command
|
|
if (elements.size() > 0)
|
|
{
|
|
HierarchyWidget* hierarchy = m_editorWindow->GetHierarchy();
|
|
|
|
// Get the list of currently selected entities so that we can attempt to restore that
|
|
// after the delete (the undoable command currently only works on selected entities)
|
|
QTreeWidgetItemRawPtrQList selection = hierarchy->selectedItems();
|
|
EntityHelpers::EntityIdList selectedEntities = SelectionHelpers::GetSelectedElementIds(hierarchy, selection, false);
|
|
|
|
// Make sure elements still exist. There is a situation related to "Push to Slice" where an
|
|
// element to be deleted may no longer exist. This occurs if a new child slice instance is
|
|
// pushed to its parent slice, then "undo" is performed which brings back the child instance
|
|
// that was deleted during the "Push to Slice" process, and then the recovered child instance
|
|
// is pushed to its parent slice again
|
|
elements.erase(
|
|
AZStd::remove_if(
|
|
elements.begin(), elements.end(),
|
|
[](AZ::EntityId entityId)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, entityId);
|
|
return !entity;
|
|
}),
|
|
elements.end());
|
|
|
|
if (elements.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Use an undoable command to delete the entities
|
|
// The way the command is implemented depends upon selecting the items first
|
|
HierarchyHelpers::SetSelectedItems(hierarchy, &elements);
|
|
CommandHierarchyItemDelete::Push(m_editorWindow->GetActiveStack(),
|
|
hierarchy,
|
|
hierarchy->selectedItems());
|
|
|
|
// Attempt to set the selection back to what it was but first remove any items from the selected
|
|
// list that no longer exist
|
|
selectedEntities.erase(
|
|
std::remove_if(
|
|
selectedEntities.begin(), selectedEntities.end(),
|
|
[](AZ::EntityId entityId)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, entityId);
|
|
return !entity;
|
|
}),
|
|
selectedEntities.end());
|
|
|
|
HierarchyHelpers::SetSelectedItems(hierarchy, &selectedEntities);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::HasPendingRequests()
|
|
{
|
|
if (!m_queuedSliceEntityRestores.empty())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::IsInstantiatingSlices()
|
|
{
|
|
if (!m_instantiatingSlices.empty())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::DetachSliceEntities(const AzToolsFramework::EntityIdList& entities)
|
|
{
|
|
if (entities.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (const AZ::EntityId& entityId : entities)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.IsValid())
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
AZ_Error("EditorEntityContext", entity, "Unable to find entity for EntityID %llu", entityId);
|
|
|
|
if (entity)
|
|
{
|
|
if (sliceAddress.GetReference()->GetSliceComponent()->RemoveEntity(entityId, false)) // Remove from current slice instance without deleting
|
|
{
|
|
AZ::SliceComponent* rootSlice = nullptr;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSlice, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
|
|
rootSlice->AddEntity(entity); // Add back as loose entity
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::OnCatalogAssetAdded(const AZ::Data::AssetId& assetId)
|
|
{
|
|
if (m_queuedSliceReplacement.IsValid())
|
|
{
|
|
AZStd::string relativePath;
|
|
EBUS_EVENT_RESULT(relativePath, AZ::Data::AssetCatalogRequestBus, GetAssetPathById, assetId);
|
|
|
|
if (AZStd::string::npos != AzFramework::StringFunc::Find(m_queuedSliceReplacement.m_path.c_str(), relativePath.c_str()))
|
|
{
|
|
AzFramework::AssetCatalogEventBus::Handler::BusDisconnect();
|
|
|
|
AZStd::unordered_set<AZ::EntityId> topLevelEntities;
|
|
GetTopLevelEntities(m_queuedSliceReplacement.m_entitiesInSelection, topLevelEntities);
|
|
|
|
// Request the slice instantiation.
|
|
AZ::Data::Asset<AZ::Data::AssetData> asset =
|
|
AZ::Data::AssetManager::Instance().FindOrCreateAsset<AZ::SliceAsset>(assetId, AZ::Data::AssetLoadBehavior::Default);
|
|
AZ::Vector2 viewportPosition(-1.0f, -1.0f);
|
|
m_queuedSliceReplacement.m_ticket = InstantiateEditorSlice(asset, viewportPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::ResetContext()
|
|
{
|
|
// First deactivate all the entities, before calling the base class ResetContext which will
|
|
// delete them all.
|
|
// This helps us know that we do not need to maintain the cached pointers between the UiElementComponents
|
|
// as individual elements are destroyed.
|
|
AZ::SliceComponent::EntityList entities;
|
|
bool result = m_entityOwnershipService->GetAllEntities(entities);
|
|
if (result)
|
|
{
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
if (entity->GetState() == AZ::Entity::State::Active)
|
|
{
|
|
entity->Deactivate();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now reset the context which will destroy all the entities
|
|
EntityContext::ResetContext();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::OnSlicePreInstantiate([[maybe_unused]] const AZ::Data::AssetId& sliceAssetId, [[maybe_unused]] const AZ::SliceComponent::SliceInstanceAddress& sliceAddress)
|
|
{
|
|
// For UI slices we don't need to do anything here. The main EditorEntityContextComponent
|
|
// changes the transforms here. But we need the entities to be initialized and activated
|
|
// before recalculating offsets so we do it in OnSliceInstantiated.
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::OnSliceInstantiated(const AZ::Data::AssetId& sliceAssetId, const AZ::SliceComponent::SliceInstanceAddress& sliceAddress)
|
|
{
|
|
AZ::Data::AssetBus::MultiHandler::BusConnect(sliceAssetId);
|
|
const AzFramework::SliceInstantiationTicket ticket = *AzFramework::SliceInstantiationResultBus::GetCurrentBusId();
|
|
|
|
// If we got here by creating a new slice then we have extra work to do (deleting the old entities etc)
|
|
AZ::Entity* insertBefore = nullptr;
|
|
if (ticket == m_queuedSliceReplacement.m_ticket)
|
|
{
|
|
m_queuedSliceReplacement.Finalize(sliceAddress, m_editorWindow);
|
|
|
|
// Select the common parent (the call to Finalize will have deleted the elements that were selected)
|
|
m_editorWindow->GetHierarchy()->SetUniqueSelectionHighlight(m_queuedSliceReplacement.m_commonParent);
|
|
insertBefore = m_queuedSliceReplacement.m_insertBefore;
|
|
}
|
|
|
|
AzFramework::SliceInstantiationResultBus::MultiHandler::BusDisconnect(ticket);
|
|
|
|
// Close out the next ticket corresponding to this asset.
|
|
for (auto instantiatingIter = m_instantiatingSlices.begin(); instantiatingIter != m_instantiatingSlices.end(); ++instantiatingIter)
|
|
{
|
|
if (instantiatingIter->first.GetId() == sliceAssetId)
|
|
{
|
|
const AZ::SliceComponent::EntityList& entities = sliceAddress.GetInstance()->GetInstantiated()->m_entities;
|
|
|
|
if (entities.size() == 0)
|
|
{
|
|
// if there are no entities there was an error with the instantiation
|
|
AZ::Data::AssetBus::MultiHandler::BusDisconnect(sliceAssetId);
|
|
UiEditorEntityContextNotificationBus::Broadcast(&UiEditorEntityContextNotificationBus::Events::OnSliceInstantiationFailed, sliceAssetId, ticket);
|
|
m_instantiatingSlices.erase(instantiatingIter);
|
|
break;
|
|
}
|
|
|
|
// Initialize the new entities and create a set of all the top-level entities.
|
|
AZStd::unordered_set<AZ::Entity*> topLevelEntities;
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
if (entity->GetState() == AZ::Entity::State::Constructed)
|
|
{
|
|
entity->Init();
|
|
}
|
|
if (entity->GetState() == AZ::Entity::State::Init)
|
|
{
|
|
entity->Activate();
|
|
}
|
|
|
|
topLevelEntities.insert(entity);
|
|
}
|
|
|
|
// remove anything from the topLevelEntities set that is referenced as the child of another element in the list
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
LyShine::EntityArray children;
|
|
EBUS_EVENT_ID_RESULT(children, entity->GetId(), UiElementBus, GetChildElements);
|
|
|
|
for (auto child : children)
|
|
{
|
|
topLevelEntities.erase(child);
|
|
}
|
|
}
|
|
|
|
// This can be null if nothing is selected. That is OK, the usage of it below treats that as meaning
|
|
// add as a child of the root element.
|
|
AZ::Entity* parent = m_editorWindow->GetHierarchy()->CurrentSelectedElement();
|
|
|
|
int childIndex = instantiatingIter->second.m_childIndex;
|
|
if (!insertBefore && childIndex >= 0)
|
|
{
|
|
if (parent)
|
|
{
|
|
EBUS_EVENT_ID_RESULT(insertBefore, parent->GetId(), UiElementBus, GetChildElement, childIndex);
|
|
}
|
|
else
|
|
{
|
|
EBUS_EVENT_ID_RESULT(insertBefore, m_editorWindow->GetCanvas(), UiCanvasBus, GetChildElement, childIndex);
|
|
}
|
|
}
|
|
|
|
// Now topLevelElements contains all of the top-level elements in the set of newly instantiated entities
|
|
// Copy the topLevelEntities set into a list
|
|
LyShine::EntityArray entitiesToInit;
|
|
for (auto entity : topLevelEntities)
|
|
{
|
|
entitiesToInit.push_back(entity);
|
|
}
|
|
|
|
// There must be at least one element
|
|
AZ_Assert(entitiesToInit.size() >= 1, "There must be at least one top-level entity in a UI slice.");
|
|
|
|
// Initialize the internal parent pointers and the canvas pointer in the elements
|
|
// We do this before adding the elements, otherwise the GetUniqueChildName code in FixupCreatedEntities will
|
|
// already see the new elements and think the names are not unique
|
|
EBUS_EVENT_ID(m_editorWindow->GetCanvas(), UiCanvasBus, FixupCreatedEntities, entitiesToInit, true, parent);
|
|
|
|
// Add all of the top-level entities as children of the parent
|
|
for (auto entity : topLevelEntities)
|
|
{
|
|
EBUS_EVENT_ID(m_editorWindow->GetCanvas(), UiCanvasBus, AddElement, entity, parent, insertBefore);
|
|
}
|
|
|
|
// Here we adjust the position of the instantiated entities so that if the slice was instantiated from the
|
|
// viewport menu we instantiate it at the mouse position
|
|
AZ::Vector2 desiredViewportPosition = instantiatingIter->second.m_viewportPosition;
|
|
if (desiredViewportPosition != AZ::Vector2(-1.0f, -1.0f))
|
|
{
|
|
// This is the same behavior as the old "Add elements from prefab" had.
|
|
|
|
AZ::Entity* rootElement = entitiesToInit[0];
|
|
|
|
// Transform pivot position to canvas space
|
|
AZ::Vector2 pivotPos;
|
|
EBUS_EVENT_ID_RESULT(pivotPos, rootElement->GetId(), UiTransformBus, GetCanvasSpacePivotNoScaleRotate);
|
|
|
|
// Transform destination position to canvas space
|
|
AZ::Matrix4x4 transformFromViewport;
|
|
EBUS_EVENT_ID(rootElement->GetId(), UiTransformBus, GetTransformFromViewport, transformFromViewport);
|
|
AZ::Vector3 destPos3 = transformFromViewport * AZ::Vector3(desiredViewportPosition.GetX(), desiredViewportPosition.GetY(), 0.0f);
|
|
AZ::Vector2 destPos(destPos3.GetX(), destPos3.GetY());
|
|
|
|
AZ::Vector2 offsetDelta = destPos - pivotPos;
|
|
|
|
// Adjust offsets on all top level elements
|
|
for (auto entity : entitiesToInit)
|
|
{
|
|
UiTransform2dInterface::Offsets offsets;
|
|
EBUS_EVENT_ID_RESULT(offsets, entity->GetId(), UiTransform2dBus, GetOffsets);
|
|
EBUS_EVENT_ID(entity->GetId(), UiTransform2dBus, SetOffsets, offsets + offsetDelta);
|
|
}
|
|
}
|
|
|
|
// the entities have already been created but we need to make an undo command that can undo/redo that action
|
|
HierarchyWidget* hierarchyWidget = m_editorWindow->GetHierarchy();
|
|
|
|
QTreeWidgetItemRawPtrQList selectedItems = hierarchyWidget->selectedItems();
|
|
|
|
// use an undoable command to create the elements from the slice
|
|
CommandHierarchyItemCreateFromData::Push(m_editorWindow->GetActiveStack(),
|
|
hierarchyWidget,
|
|
selectedItems,
|
|
true,
|
|
[ topLevelEntities ]([[maybe_unused]] HierarchyItem* parent, LyShine::EntityArray& listOfNewlyCreatedTopLevelElements)
|
|
{
|
|
for (AZ::Entity* entity : topLevelEntities)
|
|
{
|
|
listOfNewlyCreatedTopLevelElements.push_back(entity);
|
|
}
|
|
},
|
|
"Instantiate Slice");
|
|
|
|
m_instantiatingSlices.erase(instantiatingIter);
|
|
|
|
EBUS_EVENT(UiEditorEntityContextNotificationBus, OnSliceInstantiated, sliceAssetId, sliceAddress, ticket);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::OnSliceInstantiationFailed(const AZ::Data::AssetId& sliceAssetId)
|
|
{
|
|
const AzFramework::SliceInstantiationTicket ticket = *AzFramework::SliceInstantiationResultBus::GetCurrentBusId();
|
|
|
|
AzFramework::SliceInstantiationResultBus::MultiHandler::BusDisconnect(ticket);
|
|
|
|
for (auto instantiatingIter = m_instantiatingSlices.begin(); instantiatingIter != m_instantiatingSlices.end(); ++instantiatingIter)
|
|
{
|
|
if (instantiatingIter->first.GetId() == sliceAssetId)
|
|
{
|
|
AZ::Data::AssetBus::MultiHandler::BusDisconnect(sliceAssetId);
|
|
EBUS_EVENT(UiEditorEntityContextNotificationBus, OnSliceInstantiationFailed, sliceAssetId, ticket);
|
|
|
|
m_instantiatingSlices.erase(instantiatingIter);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::OnAssetReady(AZ::Data::Asset<AZ::Data::AssetData> asset)
|
|
{
|
|
// We want to stay connected to the asset bus for root uicontext asset to listen to any changes to prefab assets in the ui canvas.
|
|
if (m_rootAssetId.IsValid() && asset.GetId() == m_rootAssetId)
|
|
{
|
|
return;
|
|
}
|
|
AZ::Data::AssetBus::MultiHandler::BusDisconnect(asset.GetId());
|
|
|
|
for (auto iter = m_queuedSliceEntityRestores.begin(); iter != m_queuedSliceEntityRestores.end(); )
|
|
{
|
|
SliceEntityRestoreRequest& request = *iter;
|
|
if (asset.GetId() == request.m_asset.GetId())
|
|
{
|
|
AZ::SliceComponent* rootSlice = nullptr;
|
|
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSlice, GetContextId(),
|
|
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
|
|
AZ::SliceComponent::SliceInstanceAddress address = rootSlice->RestoreEntity(request.m_entity, request.m_restoreInfo);
|
|
|
|
// Note that we do not add the entity to the context/rootSlice using AddEntity here.
|
|
// This is because it has already been added to the root slice as a prefab instance.
|
|
// Instead we call HandleEntitiesAdded which just adds it to the context
|
|
if (address.IsValid())
|
|
{
|
|
m_entityOwnershipService->HandleEntitiesAdded({request.m_entity});
|
|
}
|
|
else
|
|
{
|
|
AZ_Error("EditorEntityContext", false, "Failed to restore entity \"%s\" [%llu]",
|
|
request.m_entity->GetName().c_str(), request.m_entity->GetId());
|
|
delete request.m_entity;
|
|
}
|
|
|
|
iter = m_queuedSliceEntityRestores.erase(iter);
|
|
}
|
|
else
|
|
{
|
|
++iter;
|
|
}
|
|
}
|
|
|
|
// Pass on to base Entity Ownership Service.
|
|
m_entityOwnershipService->OnAssetReady(asset);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Root slice (or its dependents) has been reloaded.
|
|
void UiEditorEntityContext::OnAssetReloaded(AZ::Data::Asset<AZ::Data::AssetData> asset)
|
|
{
|
|
bool isActive = false;
|
|
if (m_editorWindow->GetEntityContext() && m_editorWindow->GetEntityContext()->GetContextId() == GetContextId())
|
|
{
|
|
isActive = true;
|
|
}
|
|
|
|
HierarchyWidget* hierarchy = nullptr;
|
|
EntityHelpers::EntityIdList selectedEntities;
|
|
if (isActive)
|
|
{
|
|
hierarchy = m_editorWindow->GetHierarchy();
|
|
const QTreeWidgetItemRawPtrQList& selection = hierarchy->selectedItems();
|
|
selectedEntities = SelectionHelpers::GetSelectedElementIds(hierarchy, selection, false);
|
|
|
|
// This ensures there's no "current item".
|
|
hierarchy->SetUniqueSelectionHighlight((QTreeWidgetItem*)nullptr);
|
|
|
|
// IMPORTANT: This is necessary to indirectly trigger detach()
|
|
// in the PropertiesWidget.
|
|
hierarchy->SetUserSelection(nullptr);
|
|
}
|
|
|
|
m_entityOwnershipService->OnAssetReloaded(asset);
|
|
|
|
EBUS_EVENT_ID(m_editorWindow->GetCanvasForEntityContext(GetContextId()), UiCanvasBus, ReinitializeElements);
|
|
|
|
if (isActive)
|
|
{
|
|
// Ensure selection set is preserved after applying the new level slice.
|
|
// But make sure we don't add any EntityId to selection that no longer exists as that cause a crash later
|
|
selectedEntities.erase(
|
|
std::remove_if(
|
|
selectedEntities.begin(), selectedEntities.end(),
|
|
[](AZ::EntityId entityId)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, entityId);
|
|
return !entity;
|
|
}),
|
|
selectedEntities.end());
|
|
|
|
// Refresh the Hierarchy pane
|
|
LyShine::EntityArray childElements;
|
|
EBUS_EVENT_ID_RESULT(childElements, m_editorWindow->GetCanvas(), UiCanvasBus, GetChildElements);
|
|
hierarchy->RecreateItems(childElements);
|
|
|
|
HierarchyHelpers::SetSelectedItems(hierarchy, &selectedEntities);
|
|
}
|
|
|
|
// We want to update the status for any tabs being used to edit slices.
|
|
// If that tab has just done a push, we want to check at this point whether there are any differences between the
|
|
// reloaded asset and the instance.
|
|
m_editorWindow->UpdateChangedStatusOnAssetChange(GetContextId(), asset);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::OnContextEntitiesAdded(const AzFramework::EntityList& entities)
|
|
{
|
|
EntityContext::OnContextEntitiesAdded(entities);
|
|
|
|
InitializeEntities(entities);
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::ValidateEntitiesAreValidForContext(const AzFramework::EntityList& entities)
|
|
{
|
|
// All entities in a slice being instantiated in the UI editor should
|
|
// have the UiElementComponent on them.
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
if (!entity->FindComponent(LyShine::UiElementComponentUuid))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::SetupUiEntity(AZ::Entity* entity)
|
|
{
|
|
InitializeEntities({ entity });
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::InitializeEntities(const AzFramework::EntityList& entities)
|
|
{
|
|
// UI entities are now automatically activated on creation
|
|
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
if (entity->GetState() == AZ::Entity::State::Constructed)
|
|
{
|
|
entity->Init();
|
|
}
|
|
}
|
|
|
|
// Add required editor components to entities
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
for (const auto& componentType : m_requiredEditorComponentTypes)
|
|
{
|
|
if (!entity->FindComponent(componentType))
|
|
{
|
|
entity->CreateComponent(componentType);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (AZ::Entity* entity : entities)
|
|
{
|
|
if (entity->GetState() == AZ::Entity::State::Init)
|
|
{
|
|
// Always invalidate the entity dependencies when loading in the editor
|
|
// (we don't know what code has changed since the last time the editor was run and the services provided/required
|
|
// by entities might have changed)
|
|
entity->InvalidateDependencies();
|
|
|
|
// Because we automatically add the EditorOnlyEntityComponent if it doesn't exist, we can encounter a situation
|
|
// where an entity has duplicate EditorOnlyEntityComponents if an old canvas is resaved and an old slice it uses
|
|
// is also resaved. See LY-90580
|
|
// In the main editor this is handled by disabling the duplicate components, but the UI Editor doesn't use that
|
|
// method (the world editor allows the user to manually add incompatible components and then disable and enable
|
|
// them in the entity, the UI Editor still works how the world editor used to - it doesn't allow users to add
|
|
// incompatible components and has no way to disable/enable components in the property pane).
|
|
// So we do automatic recovery in the case where there are duplicate EditorOnlyEntityComponents. We have to do this
|
|
// before activating in order to avoid errors being reported.
|
|
AZ::Entity::ComponentArrayType editorOnlyEntityComponents =
|
|
entity->FindComponents(AzToolsFramework::Components::EditorOnlyEntityComponent::TYPEINFO_Uuid());
|
|
if (editorOnlyEntityComponents.size() > 1)
|
|
{
|
|
// There are duplicate EditorOnlyEntityComponents. If any of them have m_isEditorOnly set to true we will
|
|
// set the one we keep to true. The reasoning is that these duplicates only happen when canvases and slices
|
|
// are being gradually resaved to the new version with EditorOnlyEntityComponents. Since the default is false,
|
|
// if we find one set to true this is more likely to be one that the user specifically set that way.
|
|
bool isEditorOnly = false;
|
|
for (int i = 0; i < editorOnlyEntityComponents.size(); ++i)
|
|
{
|
|
AzToolsFramework::Components::EditorOnlyEntityComponent* thisComponent =
|
|
static_cast<AzToolsFramework::Components::EditorOnlyEntityComponent*>(editorOnlyEntityComponents[i]);
|
|
if (thisComponent->IsEditorOnlyEntity())
|
|
{
|
|
isEditorOnly = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We are going to keep the first one, ensure that its value of m_isEditorOnly is set the right way
|
|
if (isEditorOnly)
|
|
{
|
|
AzToolsFramework::Components::EditorOnlyEntityComponent* firstComponent =
|
|
static_cast<AzToolsFramework::Components::EditorOnlyEntityComponent*>(editorOnlyEntityComponents[0]);
|
|
if (!firstComponent->IsEditorOnlyEntity())
|
|
{
|
|
firstComponent->SetIsEditorOnlyEntity(true);
|
|
}
|
|
}
|
|
|
|
// Now remove all the components except the first one. The first one will be the one from the most deeply nested
|
|
// slice. It is best to keep that one, otherwise we end up with local slice overrides deleting the components from
|
|
// the instanced slices which means we could ignore changes from the slice when we should not.
|
|
for (int i = 1; i < editorOnlyEntityComponents.size(); ++i)
|
|
{
|
|
AZ::Component* duplicateComponent = editorOnlyEntityComponents[i];
|
|
entity->RemoveComponent(duplicateComponent);
|
|
delete duplicateComponent;
|
|
}
|
|
}
|
|
|
|
// This is a temporary solution to remove incompatible components so that the entity can
|
|
// activate properly, otherwise all sorts of bad things will happen.
|
|
//
|
|
// We do have formal way to handle invalid components for Editor entities (see EditorEntityActionComponent::ScrubEntities()).
|
|
// But it requires components being derived from EditorComponentBase. UiCanvas doesn't seem to distinguish between game-time
|
|
// and editor-time components, so we can't use the existing scrubbing method.
|
|
Internal::RemoveIncompatibleComponents(entity);
|
|
|
|
entity->Activate();
|
|
}
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::GetTopLevelEntities(const AZStd::unordered_set<AZ::EntityId>& entities, AZStd::unordered_set<AZ::EntityId>& topLevelEntities)
|
|
{
|
|
for (auto entityId : entities)
|
|
{
|
|
// if this entities parent is not in the set then it is a top-level
|
|
AZ::Entity* parentElement = nullptr;
|
|
EBUS_EVENT_ID_RESULT(parentElement, entityId, UiElementBus, GetParent);
|
|
|
|
if (!parentElement || entities.count(parentElement->GetId()) == 0)
|
|
{
|
|
topLevelEntities.insert(entityId);
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiEditorEntityContext::QueuedSliceReplacement::IsValid() const
|
|
{
|
|
return !m_path.empty();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::QueuedSliceReplacement::Reset()
|
|
{
|
|
m_path.clear();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiEditorEntityContext::QueuedSliceReplacement::Finalize(
|
|
const AZ::SliceComponent::SliceInstanceAddress& instanceAddress,
|
|
EditorWindow* editorWindow)
|
|
{
|
|
AZ::SliceComponent::EntityAncestorList ancestors;
|
|
AZStd::unordered_map<AZ::EntityId, AZ::EntityId> remapIds;
|
|
|
|
const auto& newEntities = instanceAddress.GetInstance()->GetInstantiated()->m_entities;
|
|
|
|
// Store mapping between live Ids we're out to remove, and the ones now provided by
|
|
// the slice instance, so we can fix up references on any still-external entities.
|
|
for (const AZ::Entity* newEntity : newEntities)
|
|
{
|
|
ancestors.clear();
|
|
instanceAddress.GetReference()->GetInstanceEntityAncestry(newEntity->GetId(), ancestors, 1);
|
|
|
|
AZ_Error("EditorEntityContext", !ancestors.empty(), "Failed to locate ancestor for newly created slice entity.");
|
|
if (!ancestors.empty())
|
|
{
|
|
for (const auto& pair : m_selectedToAssetMap)
|
|
{
|
|
const AZ::EntityId& ancestorId = ancestors.front().m_entity->GetId();
|
|
if (pair.second == ancestorId)
|
|
{
|
|
remapIds[pair.first] = newEntity->GetId();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
|
|
|
|
// Remap references on any entities left out of the slice, to any entities in the slice instance.
|
|
for (const AZ::EntityId& selectedId : m_entitiesInSelection)
|
|
{
|
|
if (m_selectedToAssetMap.find(selectedId) != m_selectedToAssetMap.end())
|
|
{
|
|
// Entity is included in the slice; no need to patch.
|
|
continue;
|
|
}
|
|
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, selectedId);
|
|
|
|
AZ_Error("EditorEntityContext", entity, "Failed to locate live entity during slice replacement.");
|
|
|
|
if (entity)
|
|
{
|
|
entity->Deactivate();
|
|
|
|
AZ::EntityUtils::ReplaceEntityRefs(entity,
|
|
[&remapIds](const AZ::EntityId& originalId, bool /*isEntityId*/) -> AZ::EntityId
|
|
{
|
|
auto iter = remapIds.find(originalId);
|
|
if (iter == remapIds.end())
|
|
{
|
|
return originalId;
|
|
}
|
|
else
|
|
{
|
|
return iter->second;
|
|
}
|
|
}, serializeContext);
|
|
|
|
entity->Activate();
|
|
}
|
|
}
|
|
|
|
// Delete the entities from the world that were used to create the slice, since the slice
|
|
// will be instantiated to replace them.
|
|
|
|
AZStd::vector<AZ::EntityId> deleteEntityIds;
|
|
deleteEntityIds.reserve(m_selectedToAssetMap.size());
|
|
for (const auto& pair : m_selectedToAssetMap)
|
|
{
|
|
deleteEntityIds.push_back(pair.first);
|
|
}
|
|
|
|
// Use an undoable command to delete the entities
|
|
HierarchyWidget* hierarchy = editorWindow->GetHierarchy();
|
|
|
|
CommandHierarchyItemDelete::Push(editorWindow->GetActiveStack(),
|
|
hierarchy,
|
|
hierarchy->selectedItems());
|
|
|
|
// This ensures there's no "current item".
|
|
hierarchy->SetUniqueSelectionHighlight((QTreeWidgetItem*)nullptr);
|
|
|
|
// IMPORTANT: This is necessary to indirectly trigger detach()
|
|
// in the PropertiesWidget.
|
|
hierarchy->SetUserSelection(nullptr);
|
|
|
|
Reset();
|
|
}
|