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.
o3de/Gems/LyShine/Code/Editor/UiSliceManager.cpp

1019 lines
44 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 <AzToolsFramework/Slice/SliceUtilities.h>
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/Component/EntityUtils.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Asset/AssetManagerBus.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/Math/Quaternion.h>
#include <AzCore/std/sort.h>
#include <AzCore/std/smart_ptr/make_shared.h>
#include <AzFramework/Asset/AssetSystemBus.h>
#include <AzFramework/Entity/EntityContextBus.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzToolsFramework/ToolsComponents/GenericComponentWrapper.h>
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
#include <AzToolsFramework/SourceControl/SourceControlAPI.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/Slice/SliceTransaction.h>
#include <AzToolsFramework/UI/UICore/ProgressShield.hxx>
#include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
#include <AzToolsFramework/UI/Slice/SlicePushWidget.hxx>
#include <QtWidgets/QWidget>
#include <QtWidgets/QDialog>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMessageBox>
#include <QtWidgets/QErrorMessage>
#include <QtWidgets/QVBoxLayout>
#include <QtCore/QThread>
#include <LyShine/Bus/UiElementBus.h>
#include <LyShine/Bus/Tools/UiSystemToolsBus.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/AssetBrowser/Search/Filter.h>
#include <AzToolsFramework/AssetBrowser/AssetSelectionModel.h>
//////////////////////////////////////////////////////////////////////////
UiSliceManager::UiSliceManager(AzFramework::EntityContextId entityContextId)
: m_entityContextId(entityContextId)
{
UiEditorEntityContextNotificationBus::Handler::BusConnect();
}
//////////////////////////////////////////////////////////////////////////
UiSliceManager::~UiSliceManager()
{
UiEditorEntityContextNotificationBus::Handler::BusDisconnect();
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::OnSliceInstantiationFailed(const AZ::Data::AssetId&, const AzFramework::SliceInstantiationTicket&)
{
QMessageBox::warning(QApplication::activeWindow(),
QStringLiteral("Cannot Instantiate UI Slice"),
QString("Slice cannot be instantiated. Check that it is a slice containing UI elements."),
QMessageBox::Ok);
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::InstantiateSlice(const AZ::Data::AssetId& assetId, AZ::Vector2 viewportPosition, int childIndex)
{
AZ::Data::Asset<AZ::SliceAsset> sliceAsset;
sliceAsset.Create(assetId, true);
EBUS_EVENT_ID(m_entityContextId, UiEditorEntityContextRequestBus, InstantiateEditorSliceAtChildIndex, sliceAsset, viewportPosition, childIndex);
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::InstantiateSliceUsingBrowser([[maybe_unused]] HierarchyWidget* hierarchy, AZ::Vector2 viewportPosition)
{
AssetSelectionModel selection = AssetSelectionModel::AssetTypeSelection("Slice");
AzToolsFramework::EditorRequests::Bus::Broadcast(&AzToolsFramework::EditorRequests::BrowseForAssets, selection);
if (!selection.IsValid())
{
return;
}
auto product = azrtti_cast<const ProductAssetBrowserEntry*>(selection.GetResult());
AZ_Assert(product, "Selection is invalid.");
InstantiateSlice(product->GetAssetId(), viewportPosition);
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::MakeSliceFromSelectedItems(HierarchyWidget* hierarchy, bool inheritSlices)
{
QTreeWidgetItemRawPtrQList selectedItems(hierarchy->selectedItems());
HierarchyItemRawPtrList items = SelectionHelpers::GetSelectedHierarchyItems(hierarchy,
selectedItems);
AzToolsFramework::EntityIdList selectedEntities;
for (auto item : items)
{
selectedEntities.push_back(item->GetEntityId());
}
MakeSliceFromEntities(selectedEntities, inheritSlices);
}
bool UiSliceManager::IsRootEntity([[maybe_unused]] const AZ::Entity& entity) const
{
// This is only used by IsNodePushable. For the UI system, we allow the root slice
// to be pushed updates, so we always return false here to allow that. If the UI
// system ever wants to leverage NotPushableOnSliceRoot, we'll need to revisit this.
return false;
}
AZ::SliceComponent* UiSliceManager::GetRootSlice() const
{
AZ::SliceComponent* rootSlice = nullptr;
EBUS_EVENT_ID_RESULT(rootSlice, m_entityContextId, UiEditorEntityContextRequestBus, GetUiRootSlice);
return rootSlice;
}
//////////////////////////////////////////////////////////////////////////
// PRIVATE MEMBER FUNCTIONS
//////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::MakeSliceFromEntities(AzToolsFramework::EntityIdList& entities, bool inheritSlices)
{
// expand the list of entities to include all child entities
AzToolsFramework::EntityIdSet entitiesAndDescendants = GatherEntitiesAndAllDescendents(entities);
const AZStd::string slicesAssetsPath = "@projectroot@/UI/Slices";
if (!gEnv->pFileIO->Exists(slicesAssetsPath.c_str()))
{
gEnv->pFileIO->CreatePath(slicesAssetsPath.c_str());
}
char path[AZ_MAX_PATH_LEN] = { 0 };
gEnv->pFileIO->ResolvePath(slicesAssetsPath.c_str(), path, AZ_MAX_PATH_LEN);
MakeNewSlice(entitiesAndDescendants, path, inheritSlices);
}
//////////////////////////////////////////////////////////////////////////
bool UiSliceManager::MakeNewSlice(
const AzToolsFramework::EntityIdSet& entities,
const char* targetDirectory,
bool inheritSlices,
AZ::SerializeContext* serializeContext)
{
AZ_PROFILE_FUNCTION(AzToolsFramework);
if (entities.empty())
{
return false;
}
if (!serializeContext)
{
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Failed to retrieve application serialize context.");
}
// Save a reference to our currently active window since it will be
// temporarily null after QFileDialogs close, which we need in order to
// be able to parent our message dialogs properly
QWidget* activeWindow = QApplication::activeWindow();
//
// Check for entity references outside of selected entities - we don't allow this in UI slices
//
AzToolsFramework::EntityIdSet entitiesToInclude = entities;
{
AzToolsFramework::EntityIdSet allReferencedEntities;
bool hasExternalReferences = false;
AzToolsFramework::SliceUtilities::GatherAllReferencedEntitiesAndCompare(entitiesToInclude, allReferencedEntities, hasExternalReferences, *serializeContext);
if (hasExternalReferences)
{
const AZStd::string message = AZStd::string::format(
"Some of the selected entities reference entities not contained in the selection and its children.\n"
"UI slices cannot contain references to outside of the slice.\n");
QMessageBox::warning(activeWindow, QStringLiteral("Create Slice"),
QString(message.c_str()), QMessageBox::Ok);
return false;
}
}
//
// Verify single root and generate an ordered entity list
//
AzToolsFramework::EntityIdList orderedEntityList;
AZ::Entity* insertBefore = nullptr;
AZ::Entity* commonParent = nullptr;
{
commonParent = ValidateSingleRootAndGenerateOrderedEntityList(entitiesToInclude, orderedEntityList, insertBefore);
if (!commonParent)
{
QMessageBox::warning(activeWindow,
QStringLiteral("Cannot Create UI Slice"),
QString("The slice cannot be created because there is no single element in the selection that is parent "
"to all other elements in the selection."
"Please make sure your slice contains only one root entity.\n\n"
"You may want to create a new entity, and assign it as the parent of your existing root entities."),
QMessageBox::Ok);
return false;
}
AZ_Assert(!orderedEntityList.empty(), "Empty orderedEntityList during UI slice creation!");
}
//
// Determine slice asset file name/path - default to name of root entity, ask user
//
AZStd::string sliceName;
AZStd::string sliceFilePath;
{
AZStd::string suggestedName = "UISlice";
UiElementBus::EventResult(suggestedName, orderedEntityList[0], &UiElementBus::Events::GetName);
if (!AzToolsFramework::SliceUtilities::QueryUserForSliceFilename(suggestedName, targetDirectory, AZ_CRC("UISliceUserSettings", 0x4f30f608), activeWindow, sliceName, sliceFilePath))
{
// User cancelled slice creation or error prevented continuation (related warning dialog boxes, if necessary, already done at this point)
return false;
}
}
//
// Setup and execute transaction for the new slice.
//
{
AZ_PROFILE_SCOPE(AzToolsFramework, "UiSliceManager::MakeNewSlice:SetupAndExecuteTransaction");
using AzToolsFramework::SliceUtilities::SliceTransaction;
// PostSaveCallback for slice creation: kick off async replacement of source entities with an instance of the new slice.
SliceTransaction::PostSaveCallback postSaveCallback =
[this, &entitiesToInclude, &commonParent, &insertBefore]
(SliceTransaction::TransactionPtr transaction, const char* fullPath, const SliceTransaction::SliceAssetPtr& /*asset*/) -> void
{
AZ_PROFILE_SCOPE(AzToolsFramework, "UiSliceManager::MakeNewSlice:PostSaveCallback");
// Once the asset is processed and ready, we can replace the source entities with an instance of the new slice.
UiEditorEntityContextRequestBus::Event(m_entityContextId,
&UiEditorEntityContextRequestBus::Events::QueueSliceReplacement,
fullPath, transaction->GetLiveToAssetEntityIdMap(), entitiesToInclude, commonParent, insertBefore);
};
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginNewSlice(nullptr, serializeContext);
// Add entities
{
AZ_PROFILE_SCOPE(AzToolsFramework, "UiSliceManager::MakeNewSlice:SetupAndExecuteTransaction:AddEntities");
for (const AZ::EntityId& entityId : orderedEntityList)
{
SliceTransaction::Result addResult = transaction->AddEntity(entityId, !inheritSlices ? SliceTransaction::SliceAddEntityFlags::DiscardSliceAncestry : 0);
if (!addResult)
{
QMessageBox::warning(activeWindow, QStringLiteral("Slice Save Failed"),
QString(addResult.GetError().c_str()), QMessageBox::Ok);
return false;
}
}
}
SliceTransaction::Result result = transaction->Commit(
sliceFilePath.c_str(),
nullptr,
postSaveCallback,
AzToolsFramework::SliceUtilities::SliceTransaction::SliceCommitFlags::DisableUndoCapture);
if (!result)
{
QMessageBox::warning(activeWindow, QStringLiteral("Slice Save Failed"),
QString(result.GetError().c_str()), QMessageBox::Ok);
return false;
}
return true;
}
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::GetTopLevelEntities(const AZ::SliceComponent::EntityList& entities, AZ::SliceComponent::EntityList& topLevelEntities)
{
AZStd::unordered_set<AZ::Entity*> allEntities;
allEntities.insert(entities.begin(), entities.end());
for (auto entity : 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, entity->GetId(), UiElementBus, GetParent);
if (parentElement)
{
if (allEntities.count(parentElement) == 0)
{
topLevelEntities.push_back(entity);
}
}
}
}
//////////////////////////////////////////////////////////////////////////
// This is similar to ToolsApplicationRequests::GatherEntitiesAndAllDescendents
// except that function assumes that the entities are supporting the the AZ::TransformBus
// for hierarchy. This UI-specific version uses the UiElementBus
AzToolsFramework::EntityIdSet UiSliceManager::GatherEntitiesAndAllDescendents(const AzToolsFramework::EntityIdList& inputEntities)
{
AzToolsFramework::EntityIdSet output;
AzToolsFramework::EntityIdList tempList;
for (const AZ::EntityId& id : inputEntities)
{
output.insert(id);
LyShine::EntityArray descendants;
EBUS_EVENT_ID(id, UiElementBus, FindDescendantElements, [](const AZ::Entity*) { return true; }, descendants);
for (auto descendant : descendants)
{
output.insert(descendant->GetId());
}
}
return output;
}
//////////////////////////////////////////////////////////////////////////
// PreSaveCallback for SliceTransactions in Slice Pushes
// Fails pushes if:
// - referenced entities are not included in the slice
// - added entities in push are not referenced as children of entities in slice
// - any entities have become orphaned with selected push options
// - there's more than one root entity
AzToolsFramework::SliceUtilities::SliceTransaction::Result SlicePreSaveCallbackForUiEntities(
AzToolsFramework::SliceUtilities::SliceTransaction::TransactionPtr transaction,
[[maybe_unused]] const char* fullPath,
AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset)
{
AZ_PROFILE_SCOPE(AzToolsFramework, "SlicePreSaveCallbackForUiEntities");
// we want to ensure that "bad" data never gets pushed to a slice
// This mostly relates to the m_childEntityIdOrder array since this is something that
// the UI Editor manages closely and requires to be consistent.
AZ::SerializeContext* serializeContext = nullptr;
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
AZ_Assert(serializeContext, "Failed to retrieve application serialize context.");
auto& assetDb = AZ::Data::AssetManager::Instance();
AZ::Data::Asset<AZ::SliceAsset> currentAsset =
assetDb.FindAsset<AZ::SliceAsset>(transaction->GetTargetAsset().GetId(), AZ::Data::AssetLoadBehavior::Default);
AZ::SliceComponent* clonedSliceComponent = asset.Get()->GetComponent();
AZ::SliceComponent* currentSliceComponent = currentAsset.Get()->GetComponent();
AZ::SliceComponent::EntityList clonedEntities;
clonedSliceComponent->GetEntities(clonedEntities);
AZ::SliceComponent::EntityList currentEntities;
currentSliceComponent->GetEntities(currentEntities);
// store a set of pairs which are the EntityId being referenced and the Entity that is referencing it
using ReferencedEntityPair = AZStd::pair<AZ::EntityId, AZ::Entity*>;
AZStd::unordered_set<ReferencedEntityPair> referencedEntities;
AZStd::unordered_set<AZ::EntityId> referencedChildEntities;
AZStd::unordered_set<AZ::EntityId> clonedEntityIds;
AZStd::unordered_set<AZ::EntityId> addedEntities;
for (auto clonedEntity : clonedEntities)
{
clonedEntityIds.insert(clonedEntity->GetId());
auto iter = AZStd::find_if(currentEntities.begin(), currentEntities.end(),
[clonedEntity](AZ::Entity* entity) -> bool
{
return entity->GetId() == clonedEntity->GetId();
});
if (iter == currentEntities.end())
{
// this clonedEntity is an addition to the slice
addedEntities.insert(clonedEntity->GetId());
}
AZ::EntityUtils::EnumerateEntityIds(clonedEntity,
[clonedEntity, &referencedEntities, &referencedChildEntities]
(const AZ::EntityId& id, bool isEntityId, const AZ::SerializeContext::ClassElement* elementData) -> void
{
if (!isEntityId && id.IsValid())
{
// Include this id.
referencedEntities.insert({ id, clonedEntity });
// Check if this is a child reference. We can detect that because the EntityId is in the "ChildEntityId"
// member of the ChildEntityIdOrderEntry struct.
if (elementData && !elementData->m_editData)
{
if (strcmp(elementData->m_name, "ChildEntityId") == 0)
{
referencedChildEntities.insert(id);
}
}
}
}, serializeContext);
}
// Issue a warning if any referenced entities are not in the slice being created
for (auto referencedEntityPair : referencedEntities)
{
const AZ::EntityId& referencedEntityId = referencedEntityPair.first;
if (clonedEntityIds.count(referencedEntityId) == 0)
{
const AZ::SliceComponent::EntityIdToEntityIdMap& entityIdMap = transaction->GetLiveToAssetEntityIdMap();
AZ::Entity* referencingEntity = referencedEntityPair.second;
AZ::EntityId referencingEntityId = referencingEntity->GetId();
// in order to get the hierarchical name of the referencing entity we need to find the live version of the entity
// this requires a reverse look up in the entityIdMap
AZ::EntityId liveReferencingEntityId;
for (auto entry : entityIdMap)
{
if (entry.second == referencingEntityId)
{
liveReferencingEntityId = entry.first;
break;
}
}
AZStd::string referencingEntityName;
if (liveReferencingEntityId.IsValid())
{
referencingEntityName = EntityHelpers::GetHierarchicalElementName(liveReferencingEntityId);
}
else
{
// this should not happen, if it does just use the non-hierarchical name
referencingEntityName = referencingEntity->GetName();
}
// Ideally we could find a hierarchical field name like "UiButtonComponent/State Actions/Hover[2]/Color/Target" but
// this just finds "Target" in that example.
AZStd::string fieldName;
AZ::EntityUtils::EnumerateEntityIds(referencingEntity,
[&referencedEntityId, &fieldName]
(const AZ::EntityId& id, bool isEntityId, const AZ::SerializeContext::ClassElement* elementData) -> void
{
if (!isEntityId && id.IsValid() && id == referencedEntityId)
{
// We have found the reference to this external or deleted EntityId
if (elementData)
{
if (elementData->m_editData)
{
fieldName = elementData->m_editData->m_name;
}
else
{
fieldName = elementData->m_name;
}
}
else
{
fieldName = "<Unknown>";
}
}
}, serializeContext);
// see if the entity has been deleted
AZ::Entity* referencedEntity = nullptr;
EBUS_EVENT_RESULT(referencedEntity, AZ::ComponentApplicationBus, FindEntity, referencedEntityId);
if (referencedEntity)
{
AZStd::string referencedEntityName = EntityHelpers::GetHierarchicalElementName(referencedEntityId);
return AZ::Failure(AZStd::string::format("There are external references. "
"Entity '%s' in the slice being pushed references another entity that will not be in the slice after the push. "
"Referenced entity is '%s'. The name of the property field referencing it is '%s'.",
referencingEntityName.c_str(), referencedEntityName.c_str(), fieldName.c_str()));
}
else
{
return AZ::Failure(AZStd::string::format("There are external references. "
"Entity '%s' in the slice being pushed references another entity that will not be in the slice after the push. "
"Referenced entity no longer exists, it's ID was '%s'. The name of the property field referencing it is '%s'.",
referencingEntityName.c_str(), referencedEntityId.ToString().c_str(), fieldName.c_str()));
}
}
}
// Issue a warning if there are any added entities that are not referenced as children of entities in the slice
for (auto entityId : addedEntities)
{
if (referencedChildEntities.count(entityId) == 0)
{
AZStd::string name = EntityHelpers::GetHierarchicalElementName(entityId);
return AZ::Failure(AZStd::string::format("There are added entities that are unreferenced. "
"An entity is being added to the slice but it is not referenced as "
"the child of another entity in the slice."
"The added entity that is unreferenced is '%s'.", name.c_str()));
}
}
// Check for any entities in the slice that have become orphaned. This can happen is a remove if pushed
// but the entity removal is unchecked while the removal from the m_childEntityIdOrder array is checked
int parentlessEntityCount = 0;
for (auto entityId : clonedEntityIds)
{
if (referencedChildEntities.count(entityId) == 0)
{
// this entity is not a child of any entity
++parentlessEntityCount;
}
}
// There can only be one "root" entity in a slice - i.e. one entity which is not referenced as a child of another
// entity in the slice
if (parentlessEntityCount > 1)
{
return AZ::Failure(AZStd::string::format("There is more than one root entity. "
"Possibly a child reference is being removed in this push but the child entity is not."));
}
return AZ::Success();
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::PushEntitiesModal(const AzToolsFramework::EntityIdList& entities,
AZ::SerializeContext* serializeContext)
{
// Use same SlicePushWidget as world entities do
AzToolsFramework::SlicePushWidgetConfigPtr config = AZStd::make_shared<AzToolsFramework::SlicePushWidgetConfig>();
config->m_defaultAddedEntitiesCheckState = true;
config->m_defaultRemovedEntitiesCheckState = true;
config->m_rootSlice = GetRootSlice();
AZ_Warning("UiSlicePush", config->m_rootSlice != nullptr, "Could not find root slice for Slice Push!");
config->m_preSaveCB = SlicePreSaveCallbackForUiEntities;
config->m_postSaveCB = nullptr;
config->m_deleteEntitiesCB = [this](const AzToolsFramework::EntityIdList& entitiesToRemove) -> void
{
EBUS_EVENT_ID(this->GetEntityContextId(), UiEditorEntityContextRequestBus, DeleteElements, entitiesToRemove);
};
config->m_isRootEntityCB = [this](const AZ::Entity* entity) -> bool
{
return this->IsRootEntity(*entity);
};
QDialog* dialog = new QDialog();
QVBoxLayout* mainLayout = new QVBoxLayout();
mainLayout->setContentsMargins(0, 0, 0, 0);
AzToolsFramework::SlicePushWidget* widget = new AzToolsFramework::SlicePushWidget(entities, config, serializeContext);
mainLayout->addWidget(widget);
dialog->setWindowTitle(widget->tr("Save Slice Overrides - Advanced"));
dialog->setMinimumSize(QSize(800, 300));
dialog->resize(QSize(1200, 600));
dialog->setLayout(mainLayout);
QWidget::connect(widget, &AzToolsFramework::SlicePushWidget::OnFinished, dialog,
[dialog]()
{
dialog->accept();
}
);
QWidget::connect(widget, &AzToolsFramework::SlicePushWidget::OnCanceled, dialog,
[dialog]()
{
dialog->reject();
}
);
dialog->exec();
delete dialog;
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::DetachSliceEntities(const AzToolsFramework::EntityIdList& entities)
{
if (!entities.empty())
{
QString title;
QString body;
if (entities.size() == 1)
{
title = QObject::tr("Detach Slice Entity");
body = QObject::tr("A detached entity will no longer receive pushes from its slice. The entity will be converted into a non-slice entity. This action cannot be undone.\n\n"
"Are you sure you want to detach the selected entity?");
}
else
{
title = QObject::tr("Detach Slice Entities");
body = QObject::tr("Detached entities no longer receive pushes from their slices. The entities will be converted into non-slice entities. This action cannot be undone.\n\n"
"Are you sure you want to detach the selected entities and their descendants?");
}
if (ConfirmDialog_Detach(title, body))
{
EBUS_EVENT_ID(m_entityContextId, UiEditorEntityContextRequestBus, DetachSliceEntities, entities);
}
}
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::DetachSliceInstances(const AzToolsFramework::EntityIdList& entities)
{
if (!entities.empty())
{
// Get all slice instances for given entities
AZStd::vector<AZ::SliceComponent::SliceInstanceAddress> sliceInstances;
for (const AZ::EntityId& entityId : entities)
{
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
if (sliceAddress.IsValid())
{
if (sliceInstances.end() == AZStd::find(sliceInstances.begin(), sliceInstances.end(), sliceAddress))
{
sliceInstances.push_back(sliceAddress);
}
}
}
QString title;
QString body;
if (sliceInstances.size() == 1)
{
title = QObject::tr("Detach Slice Instance");
body = QObject::tr("A detached instance will no longer receive pushes from its slice. All entities in the slice instance will be converted into non-slice entities. This action cannot be undone.\n\n"
"Are you sure you want to detach the selected instance?");
}
else
{
title = QObject::tr("Detach Slice Instances");
body = QObject::tr("Detached instances no longer receive pushes from their slices. All entities in the slice instances will be converted into non-slice entities. This action cannot be undone.\n\n"
"Are you sure you want to detach the selected instances?");
}
if (ConfirmDialog_Detach(title, body))
{
// Get all instantiated entities for the slice instances
AzToolsFramework::EntityIdList entitiesToDetach = entities;
for (const AZ::SliceComponent::SliceInstanceAddress& sliceInstance : sliceInstances)
{
const AZ::SliceComponent::InstantiatedContainer* instantiated = sliceInstance.GetInstance()->GetInstantiated();
if (instantiated)
{
for (AZ::Entity* entityInSlice : instantiated->m_entities)
{
entitiesToDetach.push_back(entityInSlice->GetId());
}
}
}
// Detach the entities
EBUS_EVENT_ID(m_entityContextId, UiEditorEntityContextRequestBus, DetachSliceEntities, entitiesToDetach);
}
}
}
//////////////////////////////////////////////////////////////////////////
AZ::Entity* UiSliceManager::ValidateSingleRootAndGenerateOrderedEntityList(const AzToolsFramework::EntityIdSet& liveEntities, AzToolsFramework::EntityIdList& outOrderedEntityList, AZ::Entity*& insertBefore)
{
// The low-level slice component code has no limit on there being a single root element
// in a slice. It does make it simpler to do so though. Also this is the same limitation
// that we had with the old Prefabs in the UI Editor.
AZStd::unordered_set<AZ::EntityId> childrenOfCommonParent;
AZ::Entity* commonParent = nullptr;
for (auto entity : liveEntities)
{
AZ::Entity* parentElement = nullptr;
EBUS_EVENT_ID_RESULT(parentElement, entity, UiElementBus, GetParent);
if (parentElement)
{
// if this entities parent is not in the set then it is a top-level
if (liveEntities.count(parentElement->GetId()) == 0)
{
// this is a top level element
if (commonParent)
{
if (commonParent != parentElement)
{
// we have already found a parent
return nullptr;
}
else
{
childrenOfCommonParent.insert(entity);
}
}
else
{
commonParent = parentElement;
childrenOfCommonParent.insert(entity);
}
}
}
}
// At present there must be a single UI element that is the root element of the slice
// This means that there should only be one child of the commonParent (the commonParent is always outside
// of the slice)
if (childrenOfCommonParent.size() != 1)
{
return nullptr;
}
// ensure that the top level entities are in the order that they are children of the common parent
// without this check they would be in the order that they were selected
outOrderedEntityList.clear();
LyShine::EntityArray allChildrenOfCommonParent;
EBUS_EVENT_ID_RESULT(allChildrenOfCommonParent, commonParent->GetId(), UiElementBus, GetChildElements);
bool justFound = false;
for (auto entity : allChildrenOfCommonParent)
{
// if this child is in the set of top level elements to go in the prefab
// then add it to the vectors so that we have an ordered list in child order
if (childrenOfCommonParent.count(entity->GetId()) > 0)
{
outOrderedEntityList.push_back(entity->GetId());
// we are actually only supporting one child of the common parent
// If this is it, set a flag so we can record the child immediately after it.
// This is used later to insert the slice instance before this child
justFound = true;
}
else
{
if (justFound)
{
insertBefore = entity;
justFound = false;
}
}
}
// now add the rest of the entities (that are not top-level) to the list in any order
for (auto entity : liveEntities)
{
if (childrenOfCommonParent.count(entity) == 0)
{
outOrderedEntityList.push_back(entity);
}
}
return commonParent;
}
//////////////////////////////////////////////////////////////////////////
void UiSliceManager::SetEntityContextId(AzFramework::EntityContextId entityContextId)
{
m_entityContextId = entityContextId;
}
//////////////////////////////////////////////////////////////////////////
AZ::Outcome<void, AZStd::string> UiSliceManager::PushEntitiesBackToSlice(const AzToolsFramework::EntityIdList& entityIdList, const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset)
{
return AzToolsFramework::SliceUtilities::PushEntitiesBackToSlice(entityIdList, sliceAsset, SlicePreSaveCallbackForUiEntities);
}
//////////////////////////////////////////////////////////////////////////
AZ::Outcome<void, AZStd::string> UiSliceManager::QuickPushSliceInstance(const AZ::SliceComponent::SliceInstanceAddress& sliceAddress,
const AzToolsFramework::EntityIdList& entityIdList)
{
// we cannot use SliceUtilities::PushEntitiesBackToSlice because that does not handle adds or deletes
using AzToolsFramework::SliceUtilities::SliceTransaction;
const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset = sliceAddress.GetReference()->GetSliceAsset();
if (!sliceAsset)
{
return AZ::Failure(AZStd::string::format("Asset \"%s\" with id %s is not loaded, or is not a slice.",
sliceAsset.GetHint().c_str(),
sliceAsset.GetId().ToString<AZStd::string>().c_str()));
}
// Not all entities in the list need to be part of the slice instance being pushed (sliceAddress) since we could
// be pushing a new instance into the slice. However, it is an error if there is a second instance of the same slice
// asset that we are pushing to in the entity set
for (AZ::EntityId entityId : entityIdList)
{
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
if (entitySliceAddress.IsValid() && entitySliceAddress.GetReference()->GetSliceAsset() == sliceAsset)
{
if (entitySliceAddress != sliceAddress)
{
// error there is a second instance of the same slice asset in the set
return AZ::Failure(AZStd::string::format("Entity with id %s is part of a different slice instance of the same slice asset. A slice cannot contain an instance of itself.",
entityId.ToString().c_str()));
}
}
}
// Check for any invalid slices
bool cancelPush = false;
AZ::SliceComponent* assetComponent = sliceAsset.Get()->GetComponent();
if (assetComponent)
{
// If there are any invalid slices, warn the user and allow them to choose the next step.
const AZ::SliceComponent::SliceList& invalidSlices = assetComponent->GetInvalidSlices();
if (invalidSlices.size() > 0)
{
// Assume an invalid slice count of 1 because this is a quick push, which only has one target.
AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult invalidSliceCheckResult = AzToolsFramework::SliceUtilities::DisplayInvalidSliceReferencesWarning(QApplication::activeWindow(),
/*invalidSliceCount*/ 1,
invalidSlices.size(),
/*showDetailsButton*/ true);
switch (invalidSliceCheckResult)
{
case AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult::Details:
{
cancelPush = true;
PushEntitiesModal(entityIdList, nullptr);
}
break;
case AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult::Save:
{
cancelPush = false;
}
break;
case AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult::Cancel:
default:
{
cancelPush = true;
}
break;
}
}
}
if (cancelPush)
{
return AZ::Success();
}
// Make a transaction targeting the specified slice and add all the entities in this set.
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginSlicePush(sliceAsset);
if (transaction)
{
AzToolsFramework::EntityIdList entitiesBeingAdded;
for (AZ::EntityId entityId : entityIdList)
{
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
// Check if this slice is in the slice instance being pushed
if (entitySliceAddress == sliceAddress)
{
const SliceTransaction::Result result = transaction->UpdateEntity(entityId);
if (!result)
{
return AZ::Failure(AZStd::string::format("Failed to add entity with Id %s to slice transaction for \"%s\". Slice push aborted.\n\nError:\n%s",
entityId.ToString().c_str(),
sliceAsset.GetHint().c_str(),
result.GetError().c_str()));
}
}
else
{
// This entity is not in a slice, treat it as an add
SliceTransaction::Result result = transaction->AddEntity(entityId, SliceTransaction::SliceAddEntityFlags::DiscardSliceAncestry);
if (!result)
{
return AZ::Failure(AZStd::string::format("Failed to add entity with Id %s to slice transaction for \"%s\". Slice push aborted.\n\nError:\n%s",
entityId.ToString().c_str(),
sliceAsset.GetHint().c_str(),
result.GetError().c_str()));
}
entitiesBeingAdded.push_back(entityId);
}
}
// Check for any entity removals
// We know the slice instance details, compare the entities it contains to the entities
// contained in the underlying asset. If it's missing any entities that exist in the asset,
// we can removal the entity from the base slice.
AZStd::unordered_set<AZ::EntityId> uniqueRemovedEntities;
AZ::SliceComponent::EntityAncestorList ancestorList;
AZ::SliceComponent::EntityList assetEntities;
const AZ::SliceComponent::SliceInstanceAddress& instanceAddr = sliceAddress;
if (instanceAddr.IsValid() && instanceAddr.GetReference()->GetSliceAsset() &&
instanceAddr.GetInstance()->GetInstantiated())
{
const AZ::SliceComponent::EntityList& instanceEntities = instanceAddr.GetInstance()->GetInstantiated()->m_entities;
assetEntities.clear();
instanceAddr.GetReference()->GetSliceAsset().Get()->GetComponent()->GetEntities(assetEntities);
if (assetEntities.size() > instanceEntities.size())
{
// The removed entity is already gone from the instance's map, so we have to do a reverse-lookup
// to pin down which specific entities have been removed in the instance vs the asset.
for (auto assetEntityIter = assetEntities.begin(); assetEntityIter != assetEntities.end(); ++assetEntityIter)
{
AZ::Entity* assetEntity = (*assetEntityIter);
const AZ::EntityId assetEntityId = assetEntity->GetId();
if (uniqueRemovedEntities.end() != uniqueRemovedEntities.find(assetEntityId))
{
continue;
}
// Iterate over the entities left in the instance and if none of them have this
// asset entity as its ancestor, then we want to remove it.
// \todo - Investigate ways to make this non-linear time. Tricky since removed entities
// obviously aren't maintained in any maps.
bool foundAsAncestor = false;
for (const AZ::Entity* instanceEntity : instanceEntities)
{
ancestorList.clear();
instanceAddr.GetReference()->GetInstanceEntityAncestry(instanceEntity->GetId(), ancestorList, 1);
if (!ancestorList.empty() && ancestorList.begin()->m_entity == assetEntity)
{
foundAsAncestor = true;
break;
}
}
if (!foundAsAncestor)
{
// Grab ancestors, which determines which slices the removal can be pushed to.
uniqueRemovedEntities.insert(assetEntityId);
}
}
for (AZ::EntityId entityToRemove : uniqueRemovedEntities)
{
SliceTransaction::Result result = transaction->RemoveEntity(entityToRemove);
if (!result)
{
return AZ::Failure(AZStd::string::format("Failed to add entity with Id %s to slice transaction for \"%s\" for removal. Slice push aborted.\n\nError:\n%s",
entityToRemove.ToString().c_str(),
sliceAsset.GetHint().c_str(),
result.GetError().c_str()));
break;
}
}
}
}
const SliceTransaction::Result result = transaction->Commit(
sliceAsset.GetId(),
SlicePreSaveCallbackForUiEntities,
nullptr);
if (result)
{
// Successful commit
// Remove any entities that were succesfully pushed into a slice (since they'll be brought to life through slice reloading)
EBUS_EVENT_ID(this->GetEntityContextId(), UiEditorEntityContextRequestBus, DeleteElements, entitiesBeingAdded);
}
else
{
AZStd::string sliceAssetPath;
AZ::Data::AssetCatalogRequestBus::BroadcastResult(sliceAssetPath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, sliceAsset.GetId());
return AZ::Failure(AZStd::string::format("Failed to to save slice \"%s\". Slice push aborted.\n\nError:\n%s",
sliceAssetPath.c_str(),
result.GetError().c_str()));
}
}
return AZ::Success();
}
//////////////////////////////////////////////////////////////////////////
AZStd::string UiSliceManager::MakeTemporaryFilePathForSave(const char* targetFilename)
{
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
AZ_Assert(fileIO, "File IO is not initialized.");
AZStd::string devAssetPath = fileIO->GetAlias("@projectroot@");
AZStd::string userPath = fileIO->GetAlias("@user@");
AZStd::string tempPath = targetFilename;
EBUS_EVENT(AzFramework::ApplicationRequests::Bus, NormalizePath, devAssetPath);
EBUS_EVENT(AzFramework::ApplicationRequests::Bus, NormalizePath, userPath);
EBUS_EVENT(AzFramework::ApplicationRequests::Bus, NormalizePath, tempPath);
AzFramework::StringFunc::Replace(tempPath, "@projectroot@", devAssetPath.c_str());
AzFramework::StringFunc::Replace(tempPath, devAssetPath.c_str(), userPath.c_str());
tempPath.append(".slicetemp");
return tempPath;
}
//////////////////////////////////////////////////////////////////////////
bool UiSliceManager::ConfirmDialog_Detach(const QString& title, const QString& text)
{
QMessageBox questionBox(QApplication::activeWindow());
questionBox.setIcon(QMessageBox::Question);
questionBox.setWindowTitle(title);
questionBox.setText(text);
QAbstractButton* detachButton = questionBox.addButton(QObject::tr("Detach"), QMessageBox::YesRole);
questionBox.addButton(QObject::tr("Cancel"), QMessageBox::NoRole);
questionBox.exec();
return questionBox.clickedButton() == detachButton;
}