/* * 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 "stdafx.h" #include "EditorCommon.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ////////////////////////////////////////////////////////////////////////// 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 sliceAsset; sliceAsset.Create(assetId, true); EBUS_EVENT_ID(m_entityContextId, UiEditorEntityContextRequestBus, InstantiateEditorSliceAtChildIndex, sliceAsset, viewportPosition, childIndex); } ////////////////////////////////////////////////////////////////////////// void UiSliceManager::InstantiateSliceUsingBrowser(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(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(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 = "@devassets@/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(AZ::Debug::ProfileCategory::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(AZ::Debug::ProfileCategory::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(AZ::Debug::ProfileCategory::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(AZ::Debug::ProfileCategory::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 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, const char* fullPath, AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset) { AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::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 currentAsset = assetDb.GetAsset(transaction->GetTargetAsset().GetId(), false); 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; AZStd::unordered_set referencedEntities; AZStd::unordered_set referencedChildEntities; AZStd::unordered_set clonedEntityIds; AZStd::unordered_set 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 = ""; } } }, 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(); 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 sliceInstances; for (const AZ::EntityId& entityId : entities) { AZ::SliceComponent::SliceInstanceAddress sliceAddress; EBUS_EVENT_ID_RESULT(sliceAddress, entityId, AzFramework::EntityIdContextQueryBus, 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 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 UiSliceManager::PushEntitiesBackToSlice(const AzToolsFramework::EntityIdList& entityIdList, const AZ::Data::Asset& sliceAsset) { return AzToolsFramework::SliceUtilities::PushEntitiesBackToSlice(entityIdList, sliceAsset, SlicePreSaveCallbackForUiEntities); } ////////////////////////////////////////////////////////////////////////// AZ::Outcome 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& 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().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; EBUS_EVENT_ID_RESULT(entitySliceAddress, entityId, AzFramework::EntityIdContextQueryBus, 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; EBUS_EVENT_ID_RESULT(entitySliceAddress, entityId, AzFramework::EntityIdContextQueryBus, 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 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("@devassets@"); 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, "@devassets@", 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; }