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.
4303 lines
222 KiB
C++
4303 lines
222 KiB
C++
/*
|
|
* 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 "AzToolsFramework_precompiled.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/Debug/Profiler.h>
|
|
#include <AzCore/Math/Transform.h>
|
|
#include <AzCore/Math/Quaternion.h>
|
|
#include <AzCore/std/containers/stack.h>
|
|
#include <AzCore/std/sort.h>
|
|
#include <AzCore/std/smart_ptr/intrusive_ptr.h>
|
|
#include <AzCore/std/smart_ptr/make_shared.h>
|
|
#include <AzCore/std/containers/unordered_map.h>
|
|
#include <AzCore/std/containers/stack.h>
|
|
|
|
#include <AzFramework/Asset/AssetSystemBus.h>
|
|
#include <AzFramework/Entity/EntityContextBus.h>
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
#include <AzFramework/API/ApplicationAPI.h>
|
|
#include <AzFramework/Entity/EntityContext.h>
|
|
#include <AzFramework/IO/FileOperations.h>
|
|
#include <AzFramework/StringFunc/StringFunc.h>
|
|
|
|
#include <AzQtComponents/AzQtComponentsAPI.h>
|
|
#include <AzQtComponents/Components/Style.h>
|
|
|
|
#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
|
|
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
|
#include <AzToolsFramework/ToolsComponents/GenericComponentWrapper.h>
|
|
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
|
|
#include <AzToolsFramework/ToolsComponents/EditorOnlyEntityComponentBus.h>
|
|
#include <AzToolsFramework/Commands/EntityStateCommand.h>
|
|
#include <AzToolsFramework/Commands/SelectionCommand.h>
|
|
#include <AzToolsFramework/Commands/CreateSliceCommand.h>
|
|
#include <AzToolsFramework/Commands/PushToSliceCommand.h>
|
|
#include <AzToolsFramework/Entity/EditorEntityContextBus.h>
|
|
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
|
|
#include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
|
|
#include <AzToolsFramework/Entity/EditorEntitySortComponent.h>
|
|
#include <AzToolsFramework/Entity/SliceEditorEntityOwnershipServiceBus.h>
|
|
#include <AzToolsFramework/SourceControl/SourceControlAPI.h>
|
|
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
|
|
#include <AzToolsFramework/UI/UICore/ProgressShield.hxx>
|
|
#include <AzToolsFramework/UI/Slice/SlicePushWidget.hxx>
|
|
#include <AzToolsFramework/UI/Slice/SliceRelationshipBus.h>
|
|
#include <AzToolsFramework/UI/PropertyEditor/InstanceDataHierarchy.h>
|
|
#include <AzToolsFramework/Slice/SliceUtilities.h>
|
|
#include <AzToolsFramework/Slice/SliceTransaction.h>
|
|
#include <AzToolsFramework/Undo/UndoSystem.h>
|
|
|
|
#include <AzToolsFramework/Slice/SliceMetadataEntityContextBus.h>
|
|
AZ_PUSH_DISABLE_WARNING(4251 4244, "-Wunknown-warning-option") // 4251: class '...' needs to have dll-interface to be used by clients of class '...'
|
|
// 4244: 'argument': conversion from 'int' to 'float', possible loss of data
|
|
#include <QtWidgets/QWidget>
|
|
#include <QtWidgets/QWidgetAction>
|
|
#include <QtWidgets/QMenu>
|
|
#include <QtWidgets/QDialog>
|
|
#include <QtWidgets/QFileDialog>
|
|
#include <QtWidgets/QMessageBox>
|
|
#include <QtWidgets/QPushButton>
|
|
#include <QtWidgets/QErrorMessage>
|
|
#include <QtWidgets/QVBoxLayout>
|
|
#include <QtCore/QThread>
|
|
#include <QDialogButtonBox>
|
|
AZ_POP_DISABLE_WARNING
|
|
|
|
namespace AzToolsFramework
|
|
{
|
|
namespace SliceUtilities
|
|
{
|
|
|
|
namespace Internal {
|
|
|
|
using SliceInstanceList = AZStd::vector<const AZ::SliceComponent::SliceInstance*>;
|
|
|
|
/**
|
|
* Gets the path to the icon used for representing the inability to save.
|
|
* \return The icon path.
|
|
*/
|
|
QString GetNoSaveableChangesIconPath() { return ":/PropertyEditor/Resources/no_save_available_icon.png"; }
|
|
|
|
/**
|
|
* Gets the default width of rows representing slices in menus.
|
|
*/
|
|
AZ::u32 GetSliceItemDefaultWidth() { return 200; }
|
|
|
|
|
|
enum class SliceSaveResult
|
|
{
|
|
Continue,
|
|
Retry,
|
|
Cancel
|
|
};
|
|
/**
|
|
* Checks if the passed in slice path is a valid place to save assets for the current project.
|
|
* \param activeWindow the Qt widget to parent any warning popups to.
|
|
* \param slicePath the path to verify is a safe place to save a slice.
|
|
* param retrySavePath an output parameter of a path to retry the save at, so the user doesn't have to find it.
|
|
* \return Continue if the save is valid, Retry if the user wants to try again, Cancel if the user wants to stop.
|
|
*/
|
|
SliceSaveResult IsSlicePathValidForAssets(QWidget* activeWindow, QString slicePath, AZStd::string &retrySavePath);
|
|
|
|
/**
|
|
* Convenience function for retrieving the specified entity's chain of ancestors and their associated assets along the slice data hierarchy
|
|
* \param entityId - ID of an entity in a live slice instance
|
|
* \param ancestors - [out] Output list of ancestors
|
|
*/
|
|
void GetSliceEntityAncestors(const AZ::EntityId& entityId, AZ::SliceComponent::EntityAncestorList& ancestors);
|
|
|
|
/**
|
|
* Convenience function for retrieving the specified entity's chain of ancestors, specifically subslices
|
|
* \param entityId - ID of an entity in a live slice instance
|
|
* \param ancestors - [out] Output list of ancestors
|
|
*/
|
|
void GetSubsliceEntityAncestors(const AZ::EntityId& entityId, AZ::SliceComponent::EntityAncestorList& ancestors);
|
|
|
|
/**
|
|
* Convenience function for identifying the entities and subslices requiring detachment in order to perform a NonTrivialReparent
|
|
* \param rootEntity The root entity of the hierarchy being reparented
|
|
* \param subslicesToDetach List to be filled with the subslices requiring detachment in order to perform a NonTrivialReparent
|
|
* \param entitiesToDetach List to be filled with the entities requiring detachment in order to perform a NonTrivialReparent
|
|
*/
|
|
void PartitionEntityHierarchyForNonTrivialReparent(const AZ::EntityId& rootEntity,
|
|
AZ::SliceComponent::SliceInstanceEntityIdRemapList& subslicesToDetach,
|
|
AzToolsFramework::EntityIdList& entitiesToDetach);
|
|
|
|
/**
|
|
* Convenience function for retrieving the specified entity's slice instance history
|
|
* \param entityId - ID of an entity in a live slice instance
|
|
* \param sliceInstanceAncestors - [out] Output list of ancestor slice instances
|
|
*/
|
|
void GetSliceInstanceAncestry(const AZ::EntityId& entityId, SliceInstanceList& sliceInstanceAncestors);
|
|
|
|
/**
|
|
* Generates filename string based on entity names of passed-in entities (limits size, converts spaces to underscores)
|
|
* \param entities - entities to generate name from
|
|
* \param outName - [out] generated file name string
|
|
*/
|
|
void GenerateSuggestedSliceFilenameFromEntities(const AzToolsFramework::EntityIdList& entities, AZStd::string& outName);
|
|
|
|
/**
|
|
* Generates path string based on name and directory chosen, handling duplicates by appending increasing numbers
|
|
* If /mypath/ and SliceName are passed in, and SliceName_001.slice already exists in that directory, it outputs
|
|
* /mypath/SliceName_002.slice
|
|
* \param sliceName - desired filename
|
|
* \param targetDirectory - desired directory for file to go in
|
|
* \param suggestedFullPath - [out] generated full path string
|
|
*/
|
|
void GenerateSuggestedSlicePath(const AZStd::string& sliceName, const AZStd::string& targetDirectory, AZStd::string& suggestedFullPath);
|
|
|
|
/**
|
|
* Sets slice save location to path for given CRC id of SliceUserSettings
|
|
*/
|
|
void SetSliceSaveLocation(const AZStd::string& path, AZ::u32 settingsId);
|
|
|
|
/**
|
|
* Gets slice save location path for given CRC id of SliceUserSettings, returns true whether it existed or not
|
|
*/
|
|
bool GetSliceSaveLocation(AZStd::string& path, AZ::u32 settingsId);
|
|
|
|
/**
|
|
* Calculate the centroid bottom level of a group of entities to be made into a slice
|
|
*/
|
|
AZ::Vector3 GetSliceRootPosition(const AZ::EntityId commonRoot, const AzToolsFramework::EntityList& selectionRootEntities);
|
|
|
|
/**
|
|
* Calculates the differences between a live entity and a comparison entity (typically a slice ancestor).
|
|
* Optionally, function can determine if a specific field differs, vs. all differences across the entity.
|
|
*/
|
|
AZ::u32 CountDifferencesVersusSlice(AZ::EntityId entityId, AZ::Entity* compareTo, AZ::SerializeContext& serializeContext, const InstanceDataHierarchy::Address* fieldAddress = nullptr);
|
|
|
|
/**
|
|
* \brief Checks if the entities in the provided asset have a common root, if not, prompts the user to add one or
|
|
* cancel the slice creation operation.
|
|
* Slices inherently do not care if a slice has a single root or multiple roots. We have decided that single
|
|
* rooted slices are easier for users to understand so we do not allows users to create a slice with multiples
|
|
* entities at the root level.
|
|
* This method helps users by interactively adding a new slice root during slice creation IF it is required,
|
|
* it takes a slice asset and during the pre save step for a slice, inspects this asset for a common root.
|
|
* If the slice asset has only one common root, then the slice can be created as is.
|
|
* If the slice has multiple entities at root level [X,Y,Z] and the parents of all these entities is the same [A] (even if that same parent is null).
|
|
* Then the user is presented with a dialog box that allows them to inject a parent entity [P] that is parent of [X,Y,Z] Child of [A] and is the root
|
|
* of this new slice.
|
|
* \param asset Asset of the slice the entities belong to
|
|
* \param assetToLiveMap Mapping of the asset's EntityIDs to "Live" EntityIDs present in the EditorEntityContext as outputted by SliceTransaction
|
|
* \param sliceRootName IF a slice root is added, the slice root entity is set to this name (Currently, the name of the slice)
|
|
* \param liveAndAssetAutoGeneratedRoots [OUT] If a shared root is generated this pair will contain the asset's root EntityID and the matching "Live" root EntityID allocated for the first instance of the slice
|
|
* \param sliceRootTranslation Position of the slice root entity wrt its parent after the slice has replaced live entities in the editor
|
|
* \param activeWindow QT Window used to render user prompt for adding a shared root
|
|
* \param defaultGenerateSharedRoot If true will auto add a shared root if necessary/possible without prompting the user. Defaults to false.
|
|
*/
|
|
SliceTransaction::Result CheckAndAddSliceRoot(const AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset,
|
|
AZ::SliceComponent::EntityIdToEntityIdMap assetToLiveMap,
|
|
AZStd::string sliceRootName,
|
|
AZStd::pair<AZ::EntityId, AZ::EntityId>& liveAndAssetAutoGeneratedRoots,
|
|
const AZ::Vector3& sliceRootEntityTranslation,
|
|
const AZ::Quaternion& sliceRootEntityRotation,
|
|
QWidget* activeWindow,
|
|
bool defaultGenerateSharedRoot = false);
|
|
|
|
/**
|
|
* \brief Populates a QMenu with sub menu options for pushing slice overrides/differences back toward the source slice.
|
|
* \param outerMenu outer Qt menu to which sub menu items will be added.
|
|
* \param inputEntities list of entities to use for population of menu options. Typically callers will pass the selected entity set.
|
|
* \param fieldAddress optional field address to filter push to.
|
|
* \param options contains optional settings to affect the appearance of the push menu, i.e. title and whether to display a change count if it's singular.
|
|
*/
|
|
void PopulateQuickPushMenu(QMenu& outerMenu, const AzToolsFramework::EntityIdList& inputEntities, const InstanceDataNode::Address* fieldAddress, const QuickPushMenuOptions& options);
|
|
|
|
/**
|
|
* \brief Generates the quick push sub menu.
|
|
* \param parent parent widget of the menu.
|
|
* \param numRelevantEntitiesInSlices [out] the number of relevant entities in the slices.
|
|
* \param entitiesToAdd [out] new entities to add.
|
|
* \param entitiesToRemove [out] entities to remove.
|
|
* \param numEntitiesToUpdateMapping [out] mapping used to record the number of entities to update for each slice
|
|
* \param inputEntities list of entities to use for population of menu options. Typically callers will pass the selected entity set.
|
|
* \param fieldAddress optional field address to filter push to.
|
|
* \param options contains optional settings to alter the appearance of the menu.
|
|
*/
|
|
QMenu* GenerateQuickPushMenu(
|
|
QWidget* parent,
|
|
size_t& numRelevantEntitiesInSlices,
|
|
AZStd::unordered_set<AZ::EntityId>& entitiesToAdd,
|
|
AZStd::unordered_set<AZ::EntityId>& entitiesToRemove,
|
|
AZStd::unordered_map<int, AZ::u32>& numEntitiesToUpdateMapping,
|
|
const AzToolsFramework::EntityIdList& inputEntities,
|
|
const InstanceDataNode::Address* fieldAddress,
|
|
const QuickPushMenuOptions& options);
|
|
|
|
/**
|
|
* \brief Finalizes a subslice instance clone for use in the editor: Adds it to the editor,
|
|
* captures its creation in the current undo batch, selects the instance clone, and adds
|
|
* the selection action to the undo batch.
|
|
* \param clonedSubslice The subslice instance that was cloned.
|
|
*/
|
|
void FinalizeSubsliceClone(AZ::SliceComponent::SliceInstanceAddress& clonedSubslice);
|
|
|
|
/**
|
|
* \brief Checks if any of the passed in entities have invalid slice references.
|
|
* If any are found, display a dialog warning the user that this push will remove those references.
|
|
* \param parent The widget to parent a warning message to, if a warning message is generated.
|
|
* \param sliceAsset The quick push target to check for invalid slice references.
|
|
* \returns The action to take. Save if no invalid references were found or if the user chose to overwrite them,
|
|
* Cancel if the user decides to stop this operation, and Details if the user wants to open the advanced
|
|
* slice push widget to view additional details.
|
|
*/
|
|
InvalidSliceReferencesWarningResult CheckForInvalidSliceReferences(
|
|
QWidget* parent,
|
|
AZ::Data::Asset<AZ::SliceAsset> sliceAsset);
|
|
|
|
/**
|
|
* \brief Performs a quick push to a slice.
|
|
* \param sliceAsset The slice to push to.
|
|
* \param entityAncestors The ancestors of the slice.
|
|
* \param entitiesToAdd New entities to add to the slice.
|
|
* \param entitiesToRemove Existing entities to remove from the slice.
|
|
* \param pushFieldAddress InstanceDataHierarchy address of the field to be pushed back to the slice.
|
|
* \param inputEntities The entities used to generate the quick push.
|
|
*/
|
|
void QuickPushToSlice(
|
|
const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
|
|
const AZStd::vector<EntityAncestorPair>& entityAncestors,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToAdd,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToRemove,
|
|
const InstanceDataHierarchy::Address& pushFieldAddress,
|
|
const AzToolsFramework::EntityIdList& inputEntities,
|
|
const EntityIdSet unpushableIds);
|
|
|
|
/**
|
|
* \brief Flattens the ancestry of a slice to a selected point
|
|
* \param toFlatten The ancestral level being flattened to
|
|
* \param toFlattenImmediateAncestor The next ancestor from toFlatten in the direction of the root base ancestor
|
|
*/
|
|
void FlattenAncestry(const AZ::SliceComponent::Ancestor& toFlatten, const AZ::SliceComponent::Ancestor& toFlattenImmediateAncestor);
|
|
|
|
/**
|
|
* \brief Populates out_PushableChangesPerAsset with how many pushable changes exist per slice to be displayed.
|
|
* \param pushableChangesPerAsset Output parameter populated with a map of IDs to pushable changes.
|
|
* \param sliceDisplayOrder The list of slices to use to generate the pushable change counts.
|
|
* \param serializeContext An input/output parameter for serialization information for the slice.
|
|
* \param assetEntityAncestorMap The entity ancestor list per slice in the sliceDisplayOrder list.
|
|
* \param numRelevantEntitiesInSlices How many entities are in the slice.
|
|
* \param fieldAddress The address used to count how many differences exist in the in-scene entity against the slice.
|
|
*/
|
|
bool CalculatePushableChangesPerAsset(
|
|
AZStd::unordered_map<AZ::Data::AssetId, int>& pushableChangesPerAsset,
|
|
AZ::SerializeContext& serializeContext,
|
|
const AZStd::vector<AZ::Data::AssetId>& sliceDisplayOrder,
|
|
const AZStd::unordered_map<AZ::Data::AssetId, AZStd::vector<EntityAncestorPair>>& assetEntityAncestorMap,
|
|
const size_t& numRelevantEntitiesInSlices,
|
|
const InstanceDataNode::Address* fieldAddress);
|
|
|
|
/**
|
|
* \brief Add the "Detach slice entity" action to the detach menu.
|
|
* \param detachMenu The detach menu to add to.
|
|
* \param selectedTransformHierarchyEntities The selected entities and all of their children.
|
|
*/
|
|
void addDetachSliceEntityAction(QMenu* detachMenu, const AzToolsFramework::EntityIdSet& selectedTransformHierarchyEntities);
|
|
|
|
/**
|
|
* \brief Add the "Detach slice instance" action to the detach menu.
|
|
* \param detachMenu The detach menu to add to.
|
|
* \param selectedEntities The selected entities.
|
|
* \param sliceInstances The slice instances affected by the selected entity set.
|
|
*/
|
|
void addDetachSliceInstanceAction(QMenu* detachMenu, const AzToolsFramework::EntityIdList& selectedEntities, const AZ::SliceComponent::SliceInstanceAddressSet& sliceInstances);
|
|
|
|
/**
|
|
* \brief Get the selected entities belong to slice instances.
|
|
* \param selectedEntities The selected entities.
|
|
* \param entitiesInSlices [OUT] The selected entities belong to slice instances.
|
|
* \param sliceInstances [OUT] The slice instances affected by the selected entity set.
|
|
*/
|
|
void GetEntitiesInSlices(const AzToolsFramework::EntityIdList& selectedEntities, AZ::u32& entitiesInSlices, AZ::SliceComponent::SliceInstanceAddressSet& sliceInstances);
|
|
|
|
/**
|
|
* Resaves the slice entity to the slice file on disk.
|
|
* \param sliceEntity the slice entity to save.
|
|
* \param fullFilePath the path to save the slice entity to.
|
|
*/
|
|
void ResaveSlice(AZStd::shared_ptr<AZ::Entity> sliceEntity, const AZStd::string& fullFilePath);
|
|
|
|
/**
|
|
* Analyse slice ancestry to check whether given EntityId can be safely pushed.
|
|
* \param entityId the entity to check push safety for.
|
|
* \param entitySliceAddress slice that owns this entity.
|
|
* \param transformAncestorSliceAddress slice that owns ancestor to check push safety against.
|
|
* \param sliceAncestryToPushTo Entity Instance Ancestry for slice to attempt to push to.
|
|
* \param unpushableEntityIdsPerAsset list of each entity that can't be pushed to each asset in the ancestry.
|
|
* \param newChildEntityIdAncestorPairs all new children paired with the ancestor they can be pushed to.
|
|
* \param rootAncestorPushList all discovered root ancestors, used to block pushes to multiple slices at once which is hard to check for cyclic dependencies.
|
|
*/
|
|
void AnalyseAncestoryForPushableEntities(const AZ::EntityId& entityId,
|
|
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress,
|
|
AZ::SliceComponent::SliceInstanceAddress& transformAncestorSliceAddress,
|
|
AZ::SliceComponent::EntityAncestorList& sliceAncestryToPushTo,
|
|
AZStd::unordered_map<AZ::Data::AssetId, EntityIdSet>& unpushableEntityIdsPerAsset,
|
|
AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>>& newChildEntityIdAncestorPairs,
|
|
AZStd::vector< AZ::Data::AssetId>& rootAncestorPushList);
|
|
|
|
//! Helper method to load a slice Entity from a given assetId
|
|
AZStd::shared_ptr<AZ::Entity> GetSliceEntityForAssetId(const AZ::Data::AssetId& assetId);
|
|
|
|
} // namespace Internal
|
|
|
|
//=========================================================================
|
|
void PushEntitiesModal(QWidget* parent, const EntityIdList& entities,
|
|
AZ::SerializeContext* serializeContext)
|
|
{
|
|
AZStd::shared_ptr<SlicePushWidgetConfig> config = AZStd::make_shared<SlicePushWidgetConfig>();
|
|
config->m_defaultAddedEntitiesCheckState = true;
|
|
config->m_defaultRemovedEntitiesCheckState = true;
|
|
config->m_preSaveCB = SliceUtilities::SlicePreSaveCallbackForWorldEntities;
|
|
config->m_postSaveCB = SliceUtilities::SlicePostPushCallback;
|
|
config->m_deleteEntitiesCB = nullptr; // SlicePostPushCallback handles added entities
|
|
config->m_isRootEntityCB = [](const AZ::Entity* entity) -> bool
|
|
{
|
|
return SliceUtilities::IsRootEntity(*entity);
|
|
};
|
|
SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(config->m_rootSlice,
|
|
&SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
|
|
AZ_Warning("SlicePush", config->m_rootSlice != nullptr, "Could not find editor root slice for Slice Push!");
|
|
|
|
QDialog* dialog = new QDialog(parent);
|
|
QVBoxLayout* mainLayout = new QVBoxLayout();
|
|
mainLayout->setContentsMargins(0, 0, 0, 0);
|
|
SlicePushWidget* widget = new 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, &SlicePushWidget::OnFinished, dialog,
|
|
[dialog] ()
|
|
{
|
|
dialog->accept();
|
|
}
|
|
);
|
|
|
|
QWidget::connect(widget, &SlicePushWidget::OnCanceled, dialog,
|
|
[dialog] ()
|
|
{
|
|
dialog->reject();
|
|
}
|
|
);
|
|
|
|
dialog->exec();
|
|
delete dialog;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool QueryAndPruneMissingExternalReferences(AzToolsFramework::EntityIdSet& entities, AzToolsFramework::EntityIdSet& selectedAndReferencedEntities,
|
|
bool& useReferencedEntities, bool defaultMoveExternalRefs = false)
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:HandleNotIncludedReferences");
|
|
useReferencedEntities = false;
|
|
|
|
AZStd::string includedEntities;
|
|
AZStd::string referencedEntities;
|
|
AzToolsFramework::EntityIdList missingEntityIds;
|
|
for (const AZ::EntityId& id : selectedAndReferencedEntities)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, id);
|
|
if (entity)
|
|
{
|
|
if (entities.find(id) != entities.end())
|
|
{
|
|
includedEntities.append(" ");
|
|
includedEntities.append(entity->GetName());
|
|
includedEntities.append("\r\n");
|
|
}
|
|
else
|
|
{
|
|
referencedEntities.append(" ");
|
|
referencedEntities.append(entity->GetName());
|
|
referencedEntities.append("\r\n");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
missingEntityIds.push_back(id);
|
|
}
|
|
}
|
|
|
|
if(!referencedEntities.empty())
|
|
{
|
|
if (!defaultMoveExternalRefs)
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:HandleNotIncludedReferences:UserDialog");
|
|
|
|
const AZStd::string message = AZStd::string::format(
|
|
"Entity references may not be valid if the entity IDs change or if the entities do not exist when the slice is instantiated.\r\n\r\nSelected Entities\n%s\nReferenced Entities\n%s\n",
|
|
includedEntities.c_str(),
|
|
referencedEntities.c_str());
|
|
|
|
QWidget* mainWindow = nullptr;
|
|
AzToolsFramework::EditorRequests::Bus::BroadcastResult(
|
|
mainWindow,
|
|
&AzToolsFramework::EditorRequests::Bus::Events::GetMainWindow);
|
|
|
|
QMessageBox msgBox(mainWindow);
|
|
msgBox.setWindowTitle("External Entity References");
|
|
msgBox.setText("The slice contains references to external entities that are not selected.");
|
|
msgBox.setInformativeText("You can move the referenced entities into this slice or retain the external references.");
|
|
QAbstractButton* moveButton = (QAbstractButton*)msgBox.addButton("Move", QMessageBox::YesRole);
|
|
QAbstractButton* retainButton = (QAbstractButton*)msgBox.addButton("Retain", QMessageBox::NoRole);
|
|
msgBox.setStandardButtons(QMessageBox::Cancel);
|
|
msgBox.setDefaultButton(QMessageBox::Yes);
|
|
msgBox.setDetailedText(message.c_str());
|
|
const int response = msgBox.exec();
|
|
|
|
if (msgBox.clickedButton() == moveButton)
|
|
{
|
|
useReferencedEntities = true;
|
|
}
|
|
else if (msgBox.clickedButton() != retainButton)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
useReferencedEntities = true;
|
|
}
|
|
}
|
|
|
|
for (const AZ::EntityId& missingEntityId : missingEntityIds)
|
|
{
|
|
entities.erase(missingEntityId);
|
|
selectedAndReferencedEntities.erase(missingEntityId);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool QueryUserForSliceFilename(const AZStd::string& suggestedName,
|
|
const char* initialTargetDirectory,
|
|
AZ::u32 sliceUserSettingsId,
|
|
QWidget* activeWindow,
|
|
AZStd::string& outSliceName,
|
|
AZStd::string& outSliceFilePath)
|
|
{
|
|
AZStd::string saveAsInitialSuggestedDirectory;
|
|
if (!Internal::GetSliceSaveLocation(saveAsInitialSuggestedDirectory, sliceUserSettingsId))
|
|
{
|
|
saveAsInitialSuggestedDirectory = initialTargetDirectory;
|
|
}
|
|
|
|
AZStd::string saveAsInitialSuggestedFullPath;
|
|
Internal::GenerateSuggestedSlicePath(suggestedName, saveAsInitialSuggestedDirectory, saveAsInitialSuggestedFullPath);
|
|
|
|
QString saveAs;
|
|
AZStd::string targetPath;
|
|
QFileInfo sliceSaveFileInfo;
|
|
QString sliceName;
|
|
while (true)
|
|
{
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:SaveAsDialog");
|
|
saveAs = QFileDialog::getSaveFileName(nullptr, QString("Save As..."), saveAsInitialSuggestedFullPath.c_str(), QString("Slices (*.slice)"));
|
|
}
|
|
|
|
sliceSaveFileInfo = saveAs;
|
|
sliceName = sliceSaveFileInfo.baseName();
|
|
if (saveAs.isEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
targetPath = saveAs.toUtf8().constData();
|
|
if (AzFramework::StringFunc::Utf8::CheckNonAsciiChar(targetPath))
|
|
{
|
|
QMessageBox::warning(activeWindow,
|
|
QStringLiteral("Slice Creation Failed."),
|
|
QString("Unicode file name is not supported. \r\n"
|
|
"Please use ASCII characters to name your slice."),
|
|
QMessageBox::Ok);
|
|
return false;
|
|
}
|
|
|
|
Internal::SliceSaveResult saveResult = Internal::IsSlicePathValidForAssets(activeWindow, saveAs, saveAsInitialSuggestedFullPath);
|
|
if (saveResult == Internal::SliceSaveResult::Cancel)
|
|
{
|
|
// The error was already reported if this failed.
|
|
return false;
|
|
}
|
|
else if (saveResult == Internal::SliceSaveResult::Continue)
|
|
{
|
|
// The slice save name is valid, continue with the save attempt.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If the slice already exists, we instead want to *push* the entities to the existing
|
|
// asset, as opposed to creating a new one.
|
|
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
|
|
if (fileIO && fileIO->Exists(targetPath.c_str()))
|
|
{
|
|
const AZStd::string message = AZStd::string::format(
|
|
"You are attempting to overwrite an existing slice: \"%s\".\r\n\r\n"
|
|
"This will damage instances or cascades of this slice. \r\n\r\n"
|
|
"Instead, either push entities/fields to the slice, or save to a different location.",
|
|
targetPath.c_str());
|
|
|
|
QMessageBox::warning(activeWindow, QStringLiteral("Unable to Overwrite Slice"),
|
|
QString(message.c_str()), QMessageBox::Ok, QMessageBox::Ok);
|
|
|
|
return false;
|
|
}
|
|
|
|
// We prevent users from creating a new slice with the same relative path that's already
|
|
// been used by an existing slice in other places (e.g. Gems) because the AssetProcessor
|
|
// generates asset ids based on relative paths. This is unnecessary once AssetProcessor
|
|
// starts to generate UUID to every asset regardless of paths.
|
|
{
|
|
AZStd::string sliceRelativeName;
|
|
bool relativePathFound;
|
|
AssetSystemRequestBus::BroadcastResult(relativePathFound, &AssetSystemRequestBus::Events::GetRelativeProductPathFromFullSourceOrProductPath, targetPath, sliceRelativeName);
|
|
|
|
AZ::Data::AssetId sliceAssetId;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(sliceAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, sliceRelativeName.c_str(), AZ::Data::s_invalidAssetType, false);
|
|
if (sliceAssetId.IsValid())
|
|
{
|
|
const AZStd::string message = AZStd::string::format(
|
|
"A slice with the relative path \"%s\" already exists in the Asset Database. \r\n\r\n"
|
|
"Overriding it will damage instances or cascades of this slice. \r\n\r\n"
|
|
"Instead, either push entities/fields to the slice, or save to a different location.",
|
|
sliceRelativeName.c_str());
|
|
|
|
QMessageBox::warning(activeWindow, QStringLiteral("Unable to Overwrite Slice"),
|
|
QString(message.c_str()), QMessageBox::Ok, QMessageBox::Ok);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
AZStd::string saveDir(sliceSaveFileInfo.absoluteDir().absolutePath().toUtf8().constData());
|
|
Internal::SetSliceSaveLocation(saveDir, sliceUserSettingsId);
|
|
|
|
outSliceName = sliceName.toUtf8().constData();
|
|
outSliceFilePath = targetPath.c_str();
|
|
|
|
return true;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool MakeNewSlice(const AzToolsFramework::EntityIdSet& entities,
|
|
const char* targetDirectory,
|
|
bool inheritSlices,
|
|
bool setAsDynamic,
|
|
bool acceptDefaultPath,
|
|
bool defaultMoveExternalRefs,
|
|
bool defaultGenerateSharedRoot,
|
|
bool silenceWarningPopups,
|
|
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();
|
|
|
|
//
|
|
// Determine entities to include in slice - check for references to entities
|
|
// outside selected entity set, ask user if they want to include them or not or to cancel
|
|
//
|
|
AzToolsFramework::EntityIdSet entitiesToIncludeInAsset = entities;
|
|
{
|
|
AzToolsFramework::EntityIdSet allReferencedEntities;
|
|
bool hasExternalReferences = false;
|
|
SliceUtilities::GatherAllReferencedEntitiesAndCompare(entitiesToIncludeInAsset, allReferencedEntities, hasExternalReferences, *serializeContext);
|
|
|
|
if (hasExternalReferences)
|
|
{
|
|
bool useAllReferencedEntities = false;
|
|
bool continueCreation = QueryAndPruneMissingExternalReferences(entitiesToIncludeInAsset, allReferencedEntities, useAllReferencedEntities, defaultMoveExternalRefs);
|
|
if (!continueCreation)
|
|
{
|
|
// User cancelled transaction
|
|
return false;
|
|
}
|
|
|
|
if (useAllReferencedEntities)
|
|
{
|
|
entitiesToIncludeInAsset = allReferencedEntities;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Determine slice asset file name/path - come up with default suggested name, ask user
|
|
//
|
|
AZStd::string sliceName;
|
|
AZStd::string sliceFilePath;
|
|
{
|
|
AZStd::string suggestedName;
|
|
AzToolsFramework::EntityIdList sliceRootEntities;
|
|
{
|
|
AZ::EntityId commonRoot;
|
|
bool hasCommonRoot = false;
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(hasCommonRoot,
|
|
&AzToolsFramework::ToolsApplicationRequests::FindCommonRoot, entitiesToIncludeInAsset, commonRoot, &sliceRootEntities);
|
|
if (hasCommonRoot && commonRoot.IsValid() && entitiesToIncludeInAsset.find(commonRoot) != entitiesToIncludeInAsset.end())
|
|
{
|
|
sliceRootEntities.insert(sliceRootEntities.begin(), commonRoot);
|
|
}
|
|
}
|
|
|
|
Internal::GenerateSuggestedSliceFilenameFromEntities(sliceRootEntities, suggestedName);
|
|
|
|
if (!acceptDefaultPath)
|
|
{
|
|
if (!QueryUserForSliceFilename(suggestedName, targetDirectory, AZ_CRC("SliceUserSettings", 0x055b32eb), activeWindow, sliceName, sliceFilePath))
|
|
{
|
|
// User cancelled slice creation or error prevented continuation (related warning dialog boxes, if necessary, already done at this point)
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sliceName = suggestedName;
|
|
sliceFilePath = targetDirectory;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Determine if the selected entities are part of an existing slice that would result in a reverse slice link
|
|
// e.g. the new slice would reference the parent slice and apply a data patch to remove the parent entity
|
|
// and if so, break the connection only to the root slice
|
|
//
|
|
ScopedUndoBatch createSliceUndo("Create New Slice");
|
|
AZ::Vector3 sliceRootEntityPosition(AZ::Vector3::CreateZero());
|
|
AZ::Quaternion sliceRootEntityRotation(AZ::Quaternion::CreateZero());
|
|
UndoSystem::URSequencePoint* cloneUndoSequence = nullptr;
|
|
|
|
{
|
|
if (inheritSlices)
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:CloneExistingSliceEntities");
|
|
|
|
const AZ::EntityId dummyParentId;
|
|
|
|
AzToolsFramework::EntityIdList topLevelEntityIds;
|
|
AzToolsFramework::EntityIdList sliceEntities(entitiesToIncludeInAsset.begin(), entitiesToIncludeInAsset.end());
|
|
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(
|
|
&AzToolsFramework::ToolsApplicationRequests::FindTopLevelEntityIdsInactive,
|
|
sliceEntities,
|
|
topLevelEntityIds);
|
|
|
|
entitiesToIncludeInAsset.clear();
|
|
|
|
for (AZ::EntityId rootEntity : topLevelEntityIds)
|
|
{
|
|
if (IsReparentNonTrivial(rootEntity, dummyParentId))
|
|
{
|
|
AZ::EntityId oldParentId;
|
|
AZ::TransformBus::EventResult(oldParentId, rootEntity, &AZ::TransformBus::Events::GetParentId);
|
|
ReparentNonTrivialSliceInstanceHierarchy(rootEntity, oldParentId);
|
|
}
|
|
|
|
// update the list of entities to include in the new slice
|
|
AzToolsFramework::EntityIdSet entityHierarchy;
|
|
AzToolsFramework::EntityIdList currentEntity{ rootEntity };
|
|
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
|
|
entityHierarchy,
|
|
&AzToolsFramework::ToolsApplicationRequestBus::Events::GatherEntitiesAndAllDescendents,
|
|
currentEntity);
|
|
|
|
entitiesToIncludeInAsset.insert(entityHierarchy.begin(), entityHierarchy.end());
|
|
}
|
|
}
|
|
|
|
//calculate new slice root transform
|
|
AZ::EntityId commonRoot;
|
|
AzToolsFramework::EntityList sliceRootEntities;
|
|
|
|
bool entitiesHaveCommonRoot = false;
|
|
|
|
AzToolsFramework::EntityList entityObjectsToInclude;
|
|
for (AZ::EntityId entityId : entitiesToIncludeInAsset)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
|
|
entityObjectsToInclude.push_back(entity);
|
|
}
|
|
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(entitiesHaveCommonRoot, &AzToolsFramework::ToolsApplicationRequests::FindCommonRootInactive,
|
|
entityObjectsToInclude, commonRoot, &sliceRootEntities);
|
|
|
|
if (sliceRootEntities.size() > 1)
|
|
{
|
|
sliceRootEntityPosition = Internal::GetSliceRootPosition(commonRoot, sliceRootEntities);
|
|
}
|
|
else
|
|
{
|
|
AzToolsFramework::Components::TransformComponent* transformComponent =
|
|
sliceRootEntities.front()->FindComponent<AzToolsFramework::Components::TransformComponent>();
|
|
if (transformComponent)
|
|
{
|
|
sliceRootEntityPosition = transformComponent->GetWorldTranslation();
|
|
sliceRootEntityRotation = transformComponent->GetWorldRotationQuaternion();
|
|
}
|
|
}
|
|
|
|
// prepare entities for upcoming prent transform schange
|
|
for (const AZ::Entity* entity : entityObjectsToInclude)
|
|
{
|
|
if (entity->GetTransform()->GetParentId().IsValid())
|
|
{
|
|
//if the entity is not in the top level, it won't need adjusting
|
|
continue;
|
|
}
|
|
|
|
AZ::Transform oldParentWorldTM = AZ::Transform::Identity();
|
|
AZ::Transform newParentWorldTM = AZ::Transform::Identity();
|
|
|
|
newParentWorldTM.SetTranslation(sliceRootEntityPosition);
|
|
|
|
//signal entities that parent is about to move
|
|
EBUS_EVENT_ID(entity->GetId(), AZ::TransformNotificationBus, OnParentTransformWillChange, oldParentWorldTM, newParentWorldTM);
|
|
|
|
ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::Bus::Events::AddDirtyEntity, entity->GetId());
|
|
|
|
// delay the setting of the undo sequence point until we can guarantee it has a valid operation
|
|
// otherwise an empty undo will cause a crash when invoked (on slice save failure in this case)
|
|
if (!cloneUndoSequence)
|
|
{
|
|
cloneUndoSequence = createSliceUndo.GetUndoBatch();
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// Setup and execute transaction for the new slice.
|
|
//
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:SetupAndExecuteTransaction");
|
|
|
|
// PreSaveCallback for slice creation: Before saving slice, we ensure it has a single root by optionally auto-creating one for the user
|
|
SliceTransaction::PreSaveCallback preSaveCallback =
|
|
[&sliceName, &sliceRootEntityPosition, &sliceRootEntityRotation, &activeWindow, &defaultGenerateSharedRoot]
|
|
(SliceTransaction::TransactionPtr transaction, const char* fullPath, SliceTransaction::SliceAssetPtr& asset) -> SliceTransaction::Result
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:PreSaveCallback");
|
|
AZ::SliceComponent::EntityIdToEntityIdMap assetToLiveEntityIDMap;
|
|
const AZ::SliceComponent::EntityIdToEntityIdMap& liveToAssetEntityIDMap = transaction->GetLiveToAssetEntityIdMap();
|
|
|
|
for (const AZStd::pair<AZ::EntityId, AZ::EntityId>& liveToAssetPair : liveToAssetEntityIDMap)
|
|
{
|
|
assetToLiveEntityIDMap.emplace(AZStd::make_pair(liveToAssetPair.second, liveToAssetPair.first));
|
|
}
|
|
|
|
AZStd::pair<AZ::EntityId, AZ::EntityId> liveAndAssetAutoGeneratedRoots;
|
|
auto addRootResult = Internal::CheckAndAddSliceRoot(asset, assetToLiveEntityIDMap, sliceName.c_str(), liveAndAssetAutoGeneratedRoots, sliceRootEntityPosition, sliceRootEntityRotation, activeWindow, defaultGenerateSharedRoot);
|
|
if (!addRootResult)
|
|
{
|
|
return addRootResult;
|
|
}
|
|
|
|
// Enforce/check default rules
|
|
auto defaultRulesResult = SliceUtilities::SlicePreSaveCallbackForWorldEntities(transaction, fullPath, asset);
|
|
if (!defaultRulesResult)
|
|
{
|
|
return defaultRulesResult;
|
|
}
|
|
|
|
if (liveAndAssetAutoGeneratedRoots.first.IsValid() && liveAndAssetAutoGeneratedRoots.second.IsValid())
|
|
{
|
|
if (!transaction->AddLiveToAssetEntityIdMapping(liveAndAssetAutoGeneratedRoots))
|
|
{
|
|
return SliceTransaction::Result(AZStd::string("Failed to add generated root mapping to slice transaction during SliceUtilities::MakeNewSlice preSaveCallback"));
|
|
}
|
|
}
|
|
|
|
return AZ::Success();
|
|
};
|
|
|
|
SliceTransaction::PostSaveCallback postSaveCallback = SliceUtilities::SlicePostSaveCallbackForNewSlice;
|
|
|
|
AZ::u32 creationFlags = 0;
|
|
|
|
if (setAsDynamic)
|
|
{
|
|
creationFlags |= SliceTransaction::CreateAsDynamic;
|
|
}
|
|
|
|
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginNewSlice(nullptr, serializeContext, creationFlags);
|
|
|
|
// Add entities
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::MakeNewSlice:SetupAndExecuteTransaction:AddEntities");
|
|
for (const AZ::EntityId& entityId : entitiesToIncludeInAsset)
|
|
{
|
|
SliceTransaction::Result addResult = transaction->AddEntity(entityId, !inheritSlices ? SliceTransaction::SliceAddEntityFlags::DiscardSliceAncestry : 0);
|
|
if (!addResult)
|
|
{
|
|
if (cloneUndoSequence)
|
|
{
|
|
cloneUndoSequence->RunUndo();
|
|
}
|
|
|
|
if (!silenceWarningPopups)
|
|
{
|
|
QMessageBox::warning(activeWindow, QStringLiteral("Slice Save Failed"),
|
|
QString(addResult.GetError().c_str()), QMessageBox::Ok);
|
|
}
|
|
else
|
|
{
|
|
AZ_Warning("SliceUtilities::MakeNewSlice", false, "Slice Save Failed: %s", addResult.GetError().c_str());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
SliceTransaction::Result result = transaction->Commit(
|
|
sliceFilePath.c_str(),
|
|
preSaveCallback,
|
|
postSaveCallback);
|
|
|
|
if (!result)
|
|
{
|
|
if (cloneUndoSequence)
|
|
{
|
|
cloneUndoSequence->RunUndo();
|
|
}
|
|
|
|
if (!silenceWarningPopups)
|
|
{
|
|
QMessageBox::warning(activeWindow, QStringLiteral("Slice Save Failed"),
|
|
QString(result.GetError().c_str()), QMessageBox::Ok);
|
|
}
|
|
else
|
|
{
|
|
AZ_Warning("SliceUtilities::MakeNewSlice", false, "Slice Save Failed: %s", result.GetError().c_str());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
void GatherAllReferencedEntities(AzToolsFramework::EntityIdSet& entitiesWithReferences,
|
|
AZ::SerializeContext& serializeContext)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZStd::vector<AZ::EntityId> floodQueue;
|
|
floodQueue.reserve(entitiesWithReferences.size());
|
|
|
|
auto visitChild = [&floodQueue, &entitiesWithReferences](const AZ::EntityId& childId) -> void
|
|
{
|
|
if (entitiesWithReferences.insert(childId).second)
|
|
{
|
|
floodQueue.push_back(childId);
|
|
}
|
|
};
|
|
|
|
// Seed with all provided entity Ids
|
|
for (const AZ::EntityId& entityId : entitiesWithReferences)
|
|
{
|
|
floodQueue.push_back(entityId);
|
|
}
|
|
|
|
// Flood-fill via outgoing entity references and gather all unique visited entities.
|
|
while (!floodQueue.empty())
|
|
{
|
|
const AZ::EntityId id = floodQueue.back();
|
|
floodQueue.pop_back();
|
|
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, id);
|
|
|
|
if (entity)
|
|
{
|
|
AZStd::vector<const AZ::SerializeContext::ClassData*> parentStack;
|
|
parentStack.reserve(30);
|
|
auto beginCB = [&](void* ptr, const AZ::SerializeContext::ClassData* classData, const AZ::SerializeContext::ClassElement* elementData) -> bool
|
|
{
|
|
parentStack.push_back(classData);
|
|
|
|
AZ::u32 sliceFlags = GetSliceFlags(elementData ? elementData->m_editData : nullptr, classData ? classData->m_editData : nullptr);
|
|
|
|
// Skip any class or element marked as don't gather references
|
|
if (0 != (sliceFlags & AZ::Edit::SliceFlags::DontGatherReference))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (classData->m_typeId == AZ::SerializeTypeInfo<AZ::EntityId>::GetUuid())
|
|
{
|
|
if (!parentStack.empty() && parentStack.back()->m_typeId == AZ::SerializeTypeInfo<AZ::Entity>::GetUuid())
|
|
{
|
|
// Ignore the entity's actual Id field. We're only looking for references.
|
|
}
|
|
else
|
|
{
|
|
AZ::EntityId* entityIdPtr = (elementData->m_flags & AZ::SerializeContext::ClassElement::FLG_POINTER) ?
|
|
*reinterpret_cast<AZ::EntityId**>(ptr) : reinterpret_cast<AZ::EntityId*>(ptr);
|
|
if (entityIdPtr)
|
|
{
|
|
const AZ::EntityId id = *entityIdPtr;
|
|
if (id.IsValid())
|
|
{
|
|
visitChild(id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Keep recursing.
|
|
return true;
|
|
};
|
|
|
|
auto endCB = [&]() -> bool
|
|
{
|
|
parentStack.pop_back();
|
|
return true;
|
|
};
|
|
|
|
AZ::SerializeContext::EnumerateInstanceCallContext callContext(
|
|
beginCB,
|
|
endCB,
|
|
&serializeContext,
|
|
AZ::SerializeContext::ENUM_ACCESS_FOR_READ,
|
|
nullptr
|
|
);
|
|
|
|
serializeContext.EnumerateInstanceConst(
|
|
&callContext,
|
|
entity,
|
|
azrtti_typeid<AZ::Entity>(),
|
|
nullptr,
|
|
nullptr
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void GatherAllReferencedEntitiesAndCompare(const AzToolsFramework::EntityIdSet& entities,
|
|
AzToolsFramework::EntityIdSet& entitiesAndReferencedEntities,
|
|
bool& hasExternalReferences,
|
|
AZ::SerializeContext& serializeContext)
|
|
{
|
|
entitiesAndReferencedEntities.clear();
|
|
entitiesAndReferencedEntities = entities;
|
|
GatherAllReferencedEntities(entitiesAndReferencedEntities, serializeContext);
|
|
|
|
// NOTE: that AZStd::unordered_set equality operator only returns true if they are in the same order
|
|
// (which appears to deviate from the standard). So we have to do the comparison ourselves.
|
|
hasExternalReferences = (entitiesAndReferencedEntities.size() > entities.size());
|
|
if (!hasExternalReferences)
|
|
{
|
|
for (const AZ::EntityId& id : entitiesAndReferencedEntities)
|
|
{
|
|
if (entities.find(id) == entities.end())
|
|
{
|
|
hasExternalReferences = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
AZStd::unique_ptr<AZ::Entity> CloneSliceEntityForComparison(const AZ::Entity& sourceEntity,
|
|
const AZ::SliceComponent::SliceInstance& instance,
|
|
AZ::SerializeContext& serializeContext)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZ_Assert(instance.GetEntityIdMap().find(sourceEntity.GetId()) != instance.GetEntityIdMap().end(), "Provided source entity is not a member of the provided slice instance.");
|
|
|
|
AZ::Entity* clone = serializeContext.CloneObject<AZ::Entity>(&sourceEntity);
|
|
|
|
// Prior to comparison, remap asset entity's Id references to the instance values, so we don't see instance-remapped Ids as differences.
|
|
const AZ::SliceComponent::EntityIdToEntityIdMap& assetToInstanceIdMap = instance.GetEntityIdMap();
|
|
AZ::IdUtils::Remapper<AZ::EntityId>::RemapIds(clone,
|
|
[&assetToInstanceIdMap](const AZ::EntityId& originalId, bool isEntityId, const AZStd::function<AZ::EntityId()>&) -> AZ::EntityId
|
|
{
|
|
if (!isEntityId)
|
|
{
|
|
auto findIter = assetToInstanceIdMap.find(originalId);
|
|
if (findIter != assetToInstanceIdMap.end())
|
|
{
|
|
return findIter->second;
|
|
}
|
|
}
|
|
|
|
return originalId;
|
|
},
|
|
&serializeContext, false);
|
|
|
|
return AZStd::unique_ptr<AZ::Entity>(clone);
|
|
}
|
|
|
|
//=========================================================================
|
|
AZ::Outcome<void, AZStd::string> PushEntitiesBackToSlice(const AzToolsFramework::EntityIdList& entityIdList,
|
|
const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset, SliceTransaction::PreSaveCallback preSaveCallback)
|
|
{
|
|
if (!sliceAsset)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("Asset %s is not loaded, or is not a slice.",
|
|
sliceAsset.ToString<AZStd::string>().c_str()));
|
|
}
|
|
|
|
// Make a transaction targeting the specified slice and add all the entities in this set.
|
|
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginSlicePush(sliceAsset);
|
|
if (transaction)
|
|
{
|
|
for (AZ::EntityId entityId : entityIdList)
|
|
{
|
|
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.ToString<AZStd::string>().c_str(),
|
|
result.GetError().c_str()));
|
|
}
|
|
}
|
|
|
|
ScopedUndoBatch undoBatch("Slice Push (Quick)");
|
|
|
|
const SliceTransaction::Result result = transaction->Commit(
|
|
sliceAsset.GetId(),
|
|
preSaveCallback,
|
|
nullptr);
|
|
|
|
if (!result)
|
|
{
|
|
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();
|
|
}
|
|
|
|
//=========================================================================
|
|
AZ::Outcome<void, AZStd::string> PushEntitiesIncludingAdditionAndSubtractionBackToSlice(
|
|
const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToUpdate,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToAdd,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToRemove,
|
|
SliceTransaction::PreSaveCallback preSaveCallback,
|
|
SliceTransaction::PostSaveCallback postSaveCallback)
|
|
{
|
|
using AzToolsFramework::SliceUtilities::SliceTransaction;
|
|
// Make a transaction targeting the specified slice and add all the entities in this set.
|
|
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginSlicePush(sliceAsset);
|
|
if (transaction)
|
|
{
|
|
// Update existing entities
|
|
for (AZ::EntityId entityId : entitiesToUpdate)
|
|
{
|
|
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.ToString<AZStd::string>().c_str(),
|
|
result.GetError().c_str()));
|
|
}
|
|
}
|
|
|
|
// Add new entites
|
|
for (AZ::EntityId entityId : entitiesToAdd)
|
|
{
|
|
SliceTransaction::Result result = transaction->AddEntity(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()));
|
|
}
|
|
}
|
|
|
|
// Remove existing entities
|
|
AZStd::vector<AZ::SliceComponent::SliceInstanceAddress> sliceInstances;
|
|
for (AZ::EntityId entityId : entitiesToUpdate)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (entitySliceAddress.IsValid())
|
|
{
|
|
if (sliceInstances.end() == AZStd::find(sliceInstances.begin(), sliceInstances.end(), entitySliceAddress))
|
|
{
|
|
sliceInstances.push_back(entitySliceAddress);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 remove the entity from the base slice.
|
|
for (const AZ::SliceComponent::SliceInstanceAddress& instanceAddr : sliceInstances)
|
|
{
|
|
const AZ::SliceComponent::SliceReference* sliceReference = instanceAddr.GetReference();
|
|
const AZ::SliceComponent::SliceInstance* sliceInstance = instanceAddr.GetInstance();
|
|
if (sliceReference == nullptr || sliceInstance == nullptr)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (AZ::EntityId entityToRemove : entitiesToRemove)
|
|
{
|
|
AZ::SliceComponent::EntityAncestorList ancestors;
|
|
AZ::SliceComponent::SliceInstanceAddress assetInstanceAddress = sliceReference->GetSliceAsset().Get()->GetComponent()->FindSlice(entityToRemove);
|
|
if (assetInstanceAddress.IsValid())
|
|
{
|
|
assetInstanceAddress.GetReference()->GetInstanceEntityAncestry(entityToRemove, ancestors);
|
|
}
|
|
else
|
|
{
|
|
// This is a loose entity of the slice
|
|
sliceReference->GetInstanceEntityAncestry(entityToRemove, ancestors);
|
|
}
|
|
|
|
AZ::EntityId removalEntity = entityToRemove;
|
|
for (AZ::SliceComponent::Ancestor& ancestor : ancestors)
|
|
{
|
|
if (ancestor.m_sliceAddress.GetReference()->GetSliceAsset().GetId() == sliceAsset.GetId())
|
|
{
|
|
removalEntity = ancestor.m_entity->GetId();
|
|
}
|
|
}
|
|
|
|
SliceTransaction::Result result = transaction->RemoveEntity(removalEntity);
|
|
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",
|
|
removalEntity.ToString().c_str(),
|
|
sliceAsset.GetHint().c_str(),
|
|
result.GetError().c_str()));
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
ScopedUndoBatch undoBatch("Slice Push");
|
|
|
|
const SliceTransaction::Result result = transaction->Commit(
|
|
sliceAsset.GetId(),
|
|
preSaveCallback,
|
|
postSaveCallback);
|
|
|
|
if (!result)
|
|
{
|
|
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::unordered_set<AZ::EntityId> GetPushableNewChildEntityIds(
|
|
const AzToolsFramework::EntityIdList& entityIdList,
|
|
AZStd::unordered_map<AZ::Data::AssetId, EntityIdSet>& unpushableEntityIdsPerAsset,
|
|
AZStd::unordered_map<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>& sliceAncestryMapping,
|
|
AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>>& newChildEntityIdAncestorPairs,
|
|
EntityIdSet& newEntityIds)
|
|
{
|
|
AZStd::unordered_set<AZ::EntityId> pushableNewChildEntityIds;
|
|
AZStd::vector< AZ::Data::AssetId> rootAncestorPushList;
|
|
|
|
for (const AZ::EntityId& entityId : entityIdList)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationRequests::FindEntity, entityId);
|
|
|
|
if (!entity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
// Determine which slice ancestry this entity should be considered for addition to, currently determined by the nearest
|
|
// transform ancestor entity in the current selection of entities.
|
|
AZ::SliceComponent::EntityAncestorList& sliceAncestryToPushTo = sliceAncestryMapping[entityId];
|
|
AZ::SliceComponent::SliceInstanceAddress transformAncestorSliceAddress;
|
|
AZ::EntityId parentId;
|
|
AZ::SliceEntityHierarchyRequestBus::EventResult(parentId, entityId, &AZ::SliceEntityHierarchyRequestBus::Events::GetSliceEntityParentId);
|
|
while (parentId.IsValid())
|
|
{
|
|
// If we find a transform ancestor that's not part of the selected entities
|
|
// before we find a transform ancestor that has a relevant slice to consider
|
|
// pushing this entity to, we skip the consideration of this entity for addition
|
|
// because that would mean trying to add the entity to something we don't have selected
|
|
if (AZStd::find(entityIdList.begin(), entityIdList.end(), parentId) == entityIdList.end())
|
|
{
|
|
break;
|
|
}
|
|
|
|
AZ::Entity* parentEntity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(parentEntity, &AZ::ComponentApplicationRequests::FindEntity, parentId);
|
|
if (!parentEntity)
|
|
{
|
|
break;
|
|
}
|
|
|
|
AzFramework::SliceEntityRequestBus::EventResult(transformAncestorSliceAddress, parentId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (transformAncestorSliceAddress.IsValid() && transformAncestorSliceAddress != entitySliceAddress)
|
|
{
|
|
transformAncestorSliceAddress.GetReference()->GetInstanceEntityAncestry(parentId, sliceAncestryToPushTo);
|
|
if (newEntityIds.find(entityId) == newEntityIds.end())
|
|
{
|
|
newEntityIds.insert(entityId);
|
|
}
|
|
//check each parent in the ancestry, as entity might be pushable to some but not others.
|
|
Internal::AnalyseAncestoryForPushableEntities(entityId,
|
|
entitySliceAddress,
|
|
transformAncestorSliceAddress,
|
|
sliceAncestryToPushTo,
|
|
unpushableEntityIdsPerAsset,
|
|
newChildEntityIdAncestorPairs,
|
|
rootAncestorPushList);
|
|
}
|
|
AZ::SliceEntityHierarchyRequestBus::EventResult(parentId, parentId, &AZ::SliceEntityHierarchyRequestBus::Events::GetSliceEntityParentId);
|
|
}
|
|
}
|
|
|
|
return pushableNewChildEntityIds;
|
|
}
|
|
|
|
AZStd::unordered_set<AZ::EntityId> GetUniqueRemovedEntities(
|
|
const AZStd::vector<AZ::SliceComponent::SliceInstanceAddress>& sliceInstances,
|
|
IdToEntityMapping& assetEntityIdtoAssetEntityMapping,
|
|
IdToInstanceAddressMapping& assetEntityIdtoInstanceAddressMapping)
|
|
{
|
|
// For all slice instances we encountered, 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 add fields to allow removal of the entity from the base slice.
|
|
AZStd::unordered_set<AZ::EntityId> uniqueRemovedEntities;
|
|
AZ::SliceComponent::EntityAncestorList ancestorList;
|
|
AZ::SliceComponent::EntityList assetEntities;
|
|
for (const AZ::SliceComponent::SliceInstanceAddress& instanceAddr : sliceInstances)
|
|
{
|
|
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. (LY-88218)
|
|
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);
|
|
assetEntityIdtoAssetEntityMapping[assetEntityId] = assetEntity;
|
|
assetEntityIdtoInstanceAddressMapping[assetEntityId] = instanceAddr;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return uniqueRemovedEntities;
|
|
}
|
|
|
|
//=========================================================================
|
|
AZ::Outcome<void, AZStd::string> PushEntityFieldBackToSlice(AZ::EntityId entityId, const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
|
|
const InstanceDataNode::Address& fieldAddress, SliceTransaction::PreSaveCallback preSaveCallback)
|
|
{
|
|
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()));
|
|
}
|
|
|
|
// Make a transaction targeting the specified slice and add the target entity/field.
|
|
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginSlicePush(sliceAsset);
|
|
if (!transaction)
|
|
{
|
|
return AZ::Failure(AZStd::string("Failed to begin a transaction for pushing changes to an existing slice asset."));
|
|
}
|
|
|
|
SliceTransaction::Result result = transaction->UpdateEntityField(entityId, fieldAddress);
|
|
if (!result)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("Failed to update field for entity with Id %s in slice transaction for \"%s\". Slice push aborted.\n\nError:\n%s",
|
|
entityId.ToString().c_str(),
|
|
sliceAsset.GetHint().c_str(),
|
|
result.GetError().c_str()));
|
|
}
|
|
|
|
bool undoSliceOverrideValue;
|
|
AzToolsFramework::EditorRequests::Bus::BroadcastResult(undoSliceOverrideValue, &AzToolsFramework::EditorRequests::GetUndoSliceOverrideSaveValue);
|
|
AZ::u32 sliceCommitFlags = 0;
|
|
|
|
if (!undoSliceOverrideValue)
|
|
{
|
|
sliceCommitFlags = SliceTransaction::SliceCommitFlags::DisableUndoCapture;
|
|
}
|
|
|
|
result = transaction->Commit(
|
|
sliceAsset.GetId(),
|
|
preSaveCallback,
|
|
nullptr,
|
|
sliceCommitFlags);
|
|
|
|
if (!result)
|
|
{
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* \brief Applies standard root entity transform logic to slice, used for PreSave callbacks on world entity slices
|
|
* \param targetSliceAsset Slice asset to check/modify
|
|
*/
|
|
SliceTransaction::Result VerifyAndApplySliceWorldTransformRules(SliceTransaction::SliceAssetPtr& targetSliceAsset)
|
|
{
|
|
AZ::SliceComponent::EntityList sliceEntities;
|
|
targetSliceAsset.Get()->GetComponent()->GetEntities(sliceEntities);
|
|
|
|
AZ::EntityId commonRoot;
|
|
AzToolsFramework::EntityList sliceRootEntities;
|
|
|
|
bool entitiesHaveCommonRoot = false;
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(entitiesHaveCommonRoot, &AzToolsFramework::ToolsApplicationRequests::FindCommonRootInactive,
|
|
sliceEntities, commonRoot, &sliceRootEntities);
|
|
|
|
// Slices must have a single root entity
|
|
if (!entitiesHaveCommonRoot)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("No common root for entities"));
|
|
}
|
|
|
|
if (sliceRootEntities.size() != 1)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("Must have single root entity"));
|
|
}
|
|
|
|
// Root entities cannot have a parent and must be located at the origin
|
|
for (AZ::Entity* rootEntity : sliceRootEntities)
|
|
{
|
|
AzToolsFramework::Components::TransformComponent* transformComponent = rootEntity->FindComponent<AzToolsFramework::Components::TransformComponent>();
|
|
if (transformComponent)
|
|
{
|
|
// Position and rotation are only marked as NotPushableOnSliceRoot for the editor specific
|
|
// m_translate and m_rotate fields.
|
|
// The transformation matrix will have those properties applied, and it needs to be cleared at this point
|
|
// to avoid pushing them to the slice.
|
|
// Only scale is preserved on the root entity of a slice.
|
|
transformComponent->SetParent(AZ::EntityId());
|
|
transformComponent->SetWorldTranslation(AZ::Vector3::CreateZero());
|
|
transformComponent->SetLocalRotation(AZ::Vector3::CreateZero());
|
|
}
|
|
}
|
|
|
|
// Clear cached world transforms for all asset entities
|
|
// Not a hard requirement, just they don't make too much sense without context (slices can be instantiated anywhere)
|
|
// and the cached world transform isn't pushable (so once it's set, it won't be changed in assets)
|
|
for (AZ::Entity* entity : sliceEntities)
|
|
{
|
|
AzToolsFramework::Components::TransformComponent* transformComponent = entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
|
|
if (transformComponent)
|
|
{
|
|
transformComponent->ClearCachedWorldTransform();
|
|
}
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
//=========================================================================
|
|
SliceTransaction::Result SlicePreSaveCallbackForWorldEntities(SliceTransaction::TransactionPtr transaction, const char* fullPath, SliceTransaction::SliceAssetPtr& asset)
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::SlicePreSaveCallbackForWorldEntities");
|
|
|
|
// Apply standard root transform rules. Zero out root entity translation, ensure single root, ensure slice root has no parent in slice.
|
|
SliceTransaction::Result worldTransformRulesResult = VerifyAndApplySliceWorldTransformRules(asset);
|
|
if (!worldTransformRulesResult)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("Transform root entity rules for slice save to asset\n\"%s\"\ncould not be enforced:\n%s", fullPath, worldTransformRulesResult.GetError().c_str()));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
void SlicePostPushCallback(SliceTransaction::TransactionPtr transaction, const char* /*fullSourcePath*/, const SliceTransaction::SliceAssetPtr& /*asset*/)
|
|
{
|
|
if (!transaction.get())
|
|
{
|
|
AZ_Error("SliceUtilities::SlicePostPushCallback", false, "Invalid TransactionPtr passed in. Cannot proceed with callback");
|
|
return;
|
|
}
|
|
|
|
const AZ::SliceComponent::EntityIdToEntityIdMap& addedEntityIdRemaps = transaction->GetAddedEntityIdRemaps();
|
|
|
|
// Nothing to do if no remaps
|
|
if (addedEntityIdRemaps.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
{
|
|
const char* undoMessage = "Push To Slice";
|
|
ScopedUndoBatch undoBatch(undoMessage);
|
|
|
|
AzToolsFramework::PushToSliceCommand* pushCommand = aznew AzToolsFramework::PushToSliceCommand(undoMessage);
|
|
pushCommand->Capture(transaction->GetOriginalTargetAsset(), addedEntityIdRemaps);
|
|
pushCommand->SetParent(undoBatch.GetUndoBatch());
|
|
|
|
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, undoBatch.GetUndoBatch());
|
|
}
|
|
}
|
|
|
|
void SlicePostSaveCallbackForNewSlice(SliceTransaction::TransactionPtr transaction, const char* fullPath, const SliceTransaction::SliceAssetPtr& transactionAsset)
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::SlicePostSaveCallbackForNewSlice");
|
|
|
|
const char* undoMessage = "Create Slice Asset";
|
|
ScopedUndoBatch undoBatch(undoMessage);
|
|
|
|
const AZ::SliceComponent::EntityIdToEntityIdMap& transactionLiveToAssetEntityIdMap = transaction->GetLiveToAssetEntityIdMap();
|
|
AZ::SliceComponent::EntityIdToEntityIdMap finalLiveToAssetEntityIdMap;
|
|
|
|
// Filter out MetaData entities from the list
|
|
// We will provide new ones in the final slice instance
|
|
for (auto liveToAssetEntityIdPair : transactionLiveToAssetEntityIdMap)
|
|
{
|
|
bool isMetaDataEntity = false;
|
|
AzToolsFramework::SliceMetadataEntityContextRequestBus::BroadcastResult(isMetaDataEntity, &AzToolsFramework::SliceMetadataEntityContextRequestBus::Events::IsSliceMetadataEntity, liveToAssetEntityIdPair.first);
|
|
|
|
if (!isMetaDataEntity)
|
|
{
|
|
finalLiveToAssetEntityIdMap.emplace(liveToAssetEntityIdPair);
|
|
}
|
|
}
|
|
|
|
AzToolsFramework::CreateSliceCommand* createCommand = aznew AzToolsFramework::CreateSliceCommand(undoMessage);
|
|
createCommand->Capture(transactionAsset, fullPath, finalLiveToAssetEntityIdMap);
|
|
createCommand->SetParent(undoBatch.GetUndoBatch());
|
|
|
|
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, undoBatch.GetUndoBatch());
|
|
}
|
|
|
|
//=========================================================================
|
|
bool CheckSliceAdditionCyclicDependencySafe(const AZ::SliceComponent::SliceInstanceAddress& instanceToAdd,
|
|
const AZ::SliceComponent::SliceInstanceAddress& targetInstanceToAddTo)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZ_Assert(instanceToAdd.IsValid(), "Invalid instanceToAdd passed to CheckSliceADditionCyclicDependencySafe.");
|
|
|
|
// Ensure that parent target instance is valid.
|
|
if (!targetInstanceToAddTo.IsValid())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Cannot add a slice instance to the very same instance
|
|
if (instanceToAdd == targetInstanceToAddTo)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Cannot add an asset reference to itself - "directly cyclic" check
|
|
if (instanceToAdd.GetReference()->GetSliceAsset().GetId() == targetInstanceToAddTo.GetReference()->GetSliceAsset().GetId())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// If the instanceToAdd already has a dependency on the targetInstanceToAddTo's asset before adding, if we added it,
|
|
// the targetInstanceToAddTo would then depend on instanceToAdd would depend on targetInstanceToAddTo, and on, cyclic!
|
|
AZ::SliceComponent::AssetIdSet referencedSliceAssetIds;
|
|
instanceToAdd.GetReference()->GetSliceAsset().Get()->GetComponent()->GetReferencedSliceAssets(referencedSliceAssetIds);
|
|
if (referencedSliceAssetIds.find(targetInstanceToAddTo.GetReference()->GetSliceAsset().GetId()) != referencedSliceAssetIds.end())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool IsRootEntity(const AZ::Entity& entity)
|
|
{
|
|
auto* transformComponent = entity.FindComponent<AzToolsFramework::Components::TransformComponent>();
|
|
return (transformComponent && !transformComponent->GetParentId().IsValid());
|
|
}
|
|
|
|
//=========================================================================
|
|
bool IsSliceOrSubsliceRootEntity(const AZ::EntityId& id)
|
|
{
|
|
bool isSliceEntity = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceEntity, id, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceEntity);
|
|
if (!isSliceEntity)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool isSliceRoot = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceRoot, id, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceRoot);
|
|
bool isSubsliceRoot = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSubsliceRoot, id, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceRoot);
|
|
|
|
return isSliceRoot || isSubsliceRoot;
|
|
}
|
|
|
|
//=========================================================================
|
|
AZ::u32 GetSliceFlags(const AZ::Edit::ElementData* editData, const AZ::Edit::ClassData* classData)
|
|
{
|
|
AZ::u32 sliceFlags = 0;
|
|
|
|
if (editData)
|
|
{
|
|
AZ::Edit::Attribute* slicePushAttribute = editData->FindAttribute(AZ::Edit::Attributes::SliceFlags);
|
|
if (slicePushAttribute)
|
|
{
|
|
AZ::u32 elementSliceFlags = 0;
|
|
AzToolsFramework::PropertyAttributeReader reader(nullptr, slicePushAttribute);
|
|
reader.Read<AZ::u32>(elementSliceFlags);
|
|
sliceFlags |= elementSliceFlags;
|
|
}
|
|
}
|
|
|
|
const AZ::Edit::ElementData* classEditData = classData ? classData->FindElementData(AZ::Edit::ClassElements::EditorData) : nullptr;
|
|
if (classEditData)
|
|
{
|
|
AZ::Edit::Attribute* slicePushAttribute = classEditData->FindAttribute(AZ::Edit::Attributes::SliceFlags);
|
|
if (slicePushAttribute)
|
|
{
|
|
AZ::u32 classSliceFlags = 0;
|
|
AzToolsFramework::PropertyAttributeReader reader(nullptr, slicePushAttribute);
|
|
reader.Read<AZ::u32>(classSliceFlags);
|
|
sliceFlags |= classSliceFlags;
|
|
}
|
|
}
|
|
|
|
return sliceFlags;
|
|
}
|
|
|
|
AZ::u32 GetNodeSliceFlags(const InstanceDataNode& node)
|
|
{
|
|
const AZ::Edit::ElementData* editData = node.GetElementEditMetadata();
|
|
|
|
AZ::u32 sliceFlags = 0;
|
|
|
|
if (node.GetClassMetadata())
|
|
{
|
|
sliceFlags = GetSliceFlags(editData, node.GetClassMetadata()->m_editData);
|
|
}
|
|
|
|
return sliceFlags;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool IsNodePushable(const InstanceDataNode& node, bool isRootEntity /*= false*/)
|
|
{
|
|
const AZ::u32 sliceFlags = GetNodeSliceFlags(node);
|
|
|
|
if (0 != (sliceFlags & AZ::Edit::SliceFlags::NotPushable))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (isRootEntity && 0 != (sliceFlags & AZ::Edit::SliceFlags::NotPushableOnSliceRoot))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//=========================================================================
|
|
void PopulateQuickPushMenu(QMenu& outerMenu, AZ::EntityId entityId, const InstanceDataNode::Address& fieldAddress, const QuickPushMenuOptions& options)
|
|
{
|
|
Internal::PopulateQuickPushMenu(outerMenu, { entityId }, &fieldAddress, options);
|
|
}
|
|
|
|
//=========================================================================
|
|
void PopulateQuickPushMenu(QMenu& outerMenu, const AzToolsFramework::EntityIdList& inputEntities, const QuickPushMenuOptions& options)
|
|
{
|
|
Internal::PopulateQuickPushMenu(outerMenu, inputEntities, nullptr, options);
|
|
}
|
|
|
|
void PopulateSliceSubMenus(QMenu& outerMenu, const AzToolsFramework::EntityIdList& inputEntities, SliceSelectedCallback sliceSelectedCallback, SliceSelectedCallback sliceRelationshipViewCallback)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
// The find slice menu only works with a single entity selected.
|
|
if (inputEntities.size() != 1)
|
|
{
|
|
outerMenu.addSeparator();
|
|
return;
|
|
}
|
|
const AZ::EntityId selectedEntity = inputEntities[0];
|
|
|
|
AZ::SliceComponent::EntityAncestorList ancestors;
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, selectedEntity,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
// No need to make the find slice menu if this entity has no slice association.
|
|
if (!sliceAddress.IsValid())
|
|
{
|
|
outerMenu.addSeparator();
|
|
return;
|
|
}
|
|
|
|
sliceAddress.GetReference()->GetInstanceEntityAncestry(selectedEntity, ancestors);
|
|
|
|
// No need to make the find slice menu if this entity has no slice ancestry.
|
|
if (ancestors.size() <= 0)
|
|
{
|
|
outerMenu.addSeparator();
|
|
return;
|
|
}
|
|
|
|
const AZ::SliceComponent::SliceReference* sliceReference = ancestors.at(0).m_sliceAddress.GetReference();
|
|
|
|
PopulateFindSliceMenu(outerMenu, selectedEntity, ancestors, sliceSelectedCallback);
|
|
|
|
if (sliceReference)
|
|
{
|
|
PopulateSliceRelationshipViewMenu(outerMenu, selectedEntity, ancestors, sliceRelationshipViewCallback);
|
|
}
|
|
|
|
outerMenu.addSeparator();
|
|
}
|
|
|
|
QWidgetAction* MakeSliceMenuItem(const AZ::EntityId& /*selectedEntity*/, const AZ::SliceComponent::Ancestor& ancestor, QMenu* menu, int indentation, const QPixmap icon, QString tooltip, AZ::Data::AssetId& sliceAssetId)
|
|
{
|
|
const AZ::SliceComponent::SliceReference* sliceReference = ancestor.m_sliceAddress.GetReference();
|
|
if (!sliceReference)
|
|
{
|
|
return nullptr;;
|
|
}
|
|
sliceAssetId = sliceReference->GetSliceAsset().GetId();
|
|
|
|
// Grab the path to the asset so the name can be found.
|
|
AZStd::string sliceAssetPath;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(
|
|
sliceAssetPath,
|
|
&AZ::Data::AssetCatalogRequestBus::Handler::GetAssetPathById,
|
|
sliceAssetId);
|
|
|
|
// Grab the file name of the slice to use as the slice's title in the menu.
|
|
AZStd::string sliceAssetName;
|
|
AzFramework::StringFunc::Path::GetFullFileName(sliceAssetPath.c_str(), sliceAssetName);
|
|
if (sliceAssetName.empty())
|
|
{
|
|
AZ_Warning("PopulateFindSliceMenu", false, "Failed to determine path/name for slice with id %s", sliceAssetId.ToString<AZStd::string>().c_str());
|
|
return nullptr;
|
|
}
|
|
|
|
// Build the menu item. A QWidgetAction is used instead of a QAction to allow the ancestry
|
|
// hierarchy to be represented by indenting each ancestor under the previous.
|
|
// The layout for each row is: [QLabel Indent][QLabel Slice Icon][QLabel Slice Name]
|
|
|
|
// Create the container for the row: A WidgetAction to attach to the menu,
|
|
// the base Widget to contain the horizontal layout, and the horizontal layout.
|
|
QWidgetAction* findAction = new QWidgetAction(menu);
|
|
QWidget* sliceLayoutWidget = new QWidget(menu);
|
|
sliceLayoutWidget->setObjectName("SliceHierarchyMenuItem");
|
|
// Add class to fix hover state styling for WidgetAction
|
|
AzQtComponents::Style::addClass(sliceLayoutWidget, "WidgetAction");
|
|
QHBoxLayout* sliceLayout = new QHBoxLayout(sliceLayoutWidget);
|
|
findAction->setDefaultWidget(sliceLayoutWidget);
|
|
|
|
// A label with a fixed size is used to indent instead of margins on the
|
|
// QHBoxLayout because this matches the QuickPush menu.
|
|
QLabel* indentLabel = new QLabel(menu);
|
|
indentLabel->setFixedSize(indentation, GetSliceItemHeight());
|
|
sliceLayout->addWidget(indentLabel);
|
|
|
|
// Use the SliceIcon to visually reinforce that this is a slice file.
|
|
QLabel* iconLabel = new QLabel(menu);
|
|
iconLabel->setPixmap(icon);
|
|
iconLabel->setFixedSize(GetSliceItemIconSize());
|
|
sliceLayout->addWidget(iconLabel);
|
|
|
|
// Use the filename without the path as the label for this menu icon, to match the QuickPush menu's behavior.
|
|
QLabel* sliceLabel = new QLabel(sliceAssetName.c_str(), menu);
|
|
sliceLabel->setToolTip(tooltip);
|
|
sliceLayout->addWidget(sliceLabel);
|
|
|
|
return findAction;
|
|
}
|
|
|
|
void PopulateFindSliceMenu(QMenu& outerMenu, const AZ::EntityId& selectedEntity, const AZ::SliceComponent::EntityAncestorList& ancestors, SliceSelectedCallback sliceSelectedCallback)
|
|
{
|
|
(void)selectedEntity;
|
|
QMenu* findSliceMenu = new QMenu(&outerMenu);
|
|
QPixmap sliceItemIcon(GetSliceItemIconPath());
|
|
|
|
// Track how many ancestors deep the loop is, so the hierarchy can be visually represented.
|
|
AZ::u32 indentation = 0;
|
|
for (const AZ::SliceComponent::Ancestor& ancestor : ancestors)
|
|
{
|
|
AZ::Data::AssetId sliceAssetId;
|
|
QWidgetAction* action = MakeSliceMenuItem(selectedEntity, ancestor, findSliceMenu, indentation, sliceItemIcon, QObject::tr("Selects this slice in the Asset Browser."), sliceAssetId);
|
|
|
|
if (action)
|
|
{
|
|
// Connect the action to select this slice in the AssetBrowser when it is clicked.
|
|
QObject::connect(action, &QAction::triggered,
|
|
[sliceSelectedCallback, sliceAssetId]
|
|
{
|
|
sliceSelectedCallback();
|
|
AzToolsFramework::AssetBrowser::AssetBrowserViewRequestBus::Broadcast(
|
|
&AzToolsFramework::AssetBrowser::AssetBrowserViewRequestBus::Events::ClearFilter);
|
|
|
|
AzToolsFramework::AssetBrowser::AssetBrowserViewRequestBus::Broadcast(
|
|
&AzToolsFramework::AssetBrowser::AssetBrowserViewRequestBus::Events::SelectProduct,
|
|
sliceAssetId);
|
|
});
|
|
findSliceMenu->addAction(action);
|
|
|
|
// Grow the indentation size for the next step in the hierarchy.
|
|
indentation += GetSliceHierarchyMenuIdentationPerLevel();
|
|
}
|
|
}
|
|
|
|
findSliceMenu->setTitle(QObject::tr("Find slice in Asset Browser"));
|
|
outerMenu.addMenu(findSliceMenu);
|
|
}
|
|
|
|
void PopulateSliceRelationshipViewMenu(QMenu& outerMenu, const AZ::EntityId& selectedEntity, const AZ::SliceComponent::EntityAncestorList& ancestors, SliceSelectedCallback sliceSelectedCallback)
|
|
{
|
|
(void)selectedEntity;
|
|
QMenu* findSliceMenu = new QMenu(&outerMenu);
|
|
QPixmap sliceItemIcon(GetSliceItemIconPath());
|
|
|
|
// Track how many ancestors deep the loop is, so the hierarchy can be visually represented.
|
|
AZ::u32 indentation = 0;
|
|
for (const AZ::SliceComponent::Ancestor& ancestor : ancestors)
|
|
{
|
|
AZ::Data::AssetId sliceAssetId;
|
|
QWidgetAction* action = MakeSliceMenuItem(selectedEntity, ancestor, findSliceMenu, indentation, sliceItemIcon, QObject::tr("Opens this slice in the Slice Relationship View"), sliceAssetId);
|
|
|
|
if (action)
|
|
{
|
|
// Connect the action to select this slice in the AssetBrowser when it is clicked.
|
|
QObject::connect(action, &QAction::triggered,
|
|
[sliceSelectedCallback, sliceAssetId]
|
|
{
|
|
sliceSelectedCallback();
|
|
AzToolsFramework::SliceRelationshipRequestBus::Broadcast(&AzToolsFramework::SliceRelationshipRequests::OnSliceRelationshipViewRequested, sliceAssetId);
|
|
});
|
|
findSliceMenu->addAction(action);
|
|
|
|
// Grow the indentation size for the next step in the hierarchy.
|
|
indentation += GetSliceHierarchyMenuIdentationPerLevel();
|
|
}
|
|
}
|
|
|
|
findSliceMenu->setTitle(QObject::tr("Open in Slice Relationship View"));
|
|
outerMenu.addMenu(findSliceMenu);
|
|
}
|
|
|
|
//=========================================================================
|
|
void PopulateDetachMenu(QMenu& outerMenu, const AzToolsFramework::EntityIdList& selectedEntities, const AzToolsFramework::EntityIdSet& selectedTransformHierarchyEntities, const AZStd::string& headerText)
|
|
{
|
|
AZ::u32 entitiesInSlices;
|
|
AZ::SliceComponent::SliceInstanceAddressSet sliceInstances;
|
|
Internal::GetEntitiesInSlices(selectedEntities, entitiesInSlices, sliceInstances);
|
|
// Offer slice-related options if any selected entities belong to slice instances.
|
|
if (sliceInstances.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
QMenu* detachMenu = new QMenu(&outerMenu);
|
|
detachMenu->setTitle(QObject::tr(headerText.c_str()));
|
|
detachMenu->setToolTipsVisible(true);
|
|
|
|
Internal::addDetachSliceEntityAction(detachMenu, selectedTransformHierarchyEntities);
|
|
Internal::addDetachSliceInstanceAction(detachMenu, selectedEntities, sliceInstances);
|
|
outerMenu.addMenu(detachMenu);
|
|
|
|
if (selectedEntities.size() != 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ::EntityId selectedEntity = selectedEntities.front();
|
|
// Only allow operations on SliceRoots
|
|
bool isSliceRoot = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceRoot, selectedEntity, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceRoot);
|
|
|
|
bool isSubSlice = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSubSlice, selectedEntity, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceRoot);
|
|
|
|
if (!isSliceRoot && !isSubSlice)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Error("Slice", false, "Could not retrieve application serialize context.");
|
|
return;
|
|
}
|
|
|
|
// Grab the slice the selected entity belongs to
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, selectedEntity,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (!sliceAddress.IsValid())
|
|
{
|
|
AZ_Warning("Slice", false, "Could not find owning slice of selected entity with ID %s", selectedEntity.ToString().c_str());
|
|
return;
|
|
}
|
|
|
|
// Acquire the slice ancestry of the selected entity
|
|
AZ::SliceComponent::EntityAncestorList ancestors;
|
|
sliceAddress.GetReference()->GetInstanceEntityAncestry(selectedEntity, ancestors);
|
|
|
|
// Hide the reassign options completely if there are no reassign options to reassign to
|
|
if (ancestors.size() < 2)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (isSubSlice)
|
|
{
|
|
// Don't count subslices top parent ancestor
|
|
ancestors.erase(ancestors.begin());
|
|
}
|
|
|
|
// We want our first ancestor to be a root ancestor
|
|
// If we are a subslice this will be the subslice root, ignoring any nested parent ancestry past the first
|
|
// Example: A.slice contains a B.slice child and we selected B we want to operate up to B and not include A
|
|
for (auto it = ancestors.begin(); it < ancestors.end(); ++it)
|
|
{
|
|
if (it->m_entity && IsRootEntity(*it->m_entity))
|
|
{
|
|
ancestors.erase(ancestors.begin(), it);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Hold onto our last ancestor for the warning message detailing what the operation will do
|
|
AZStd::string lastAncestorPath;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(
|
|
lastAncestorPath,
|
|
&AZ::Data::AssetCatalogRequestBus::Events::GetAssetPathById,
|
|
ancestors.back().m_sliceAddress.GetReference()->GetSliceAsset().GetId());
|
|
|
|
AZStd::string lastAncestorName;
|
|
AzFramework::StringFunc::Path::GetFullFileName(lastAncestorPath.c_str(), lastAncestorName);
|
|
|
|
// Get first ancestor
|
|
AZStd::string owningSlicePath;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(
|
|
owningSlicePath,
|
|
&AZ::Data::AssetCatalogRequestBus::Events::GetAssetPathById,
|
|
sliceAddress.GetReference()->GetSliceAsset().GetId());
|
|
|
|
AZStd::string owningSliceName;
|
|
AzFramework::StringFunc::Path::GetFullFileName(owningSlicePath.c_str(), owningSliceName);
|
|
|
|
QPixmap sliceItemIcon(GetSliceItemIconPath());
|
|
|
|
detachMenu->addSeparator();
|
|
|
|
AZ::u32 indentation = 0;
|
|
for (unsigned int currentAncestorIndex = 0; currentAncestorIndex < ancestors.size(); ++currentAncestorIndex)
|
|
{
|
|
const AZ::SliceComponent::SliceReference* sliceReference = ancestors[currentAncestorIndex].m_sliceAddress.GetReference();
|
|
if (!sliceReference)
|
|
{
|
|
AZ_Warning("Slice", false, "Entity with ID %s has an invalid slice reference.", selectedEntity.ToString().c_str());
|
|
continue;
|
|
}
|
|
const AZ::Data::AssetId sliceAssetId = sliceReference->GetSliceAsset().GetId();
|
|
|
|
AZStd::string sliceAssetPath;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(
|
|
sliceAssetPath,
|
|
&AZ::Data::AssetCatalogRequests::GetAssetPathById,
|
|
sliceAssetId);
|
|
|
|
AZStd::string sliceAssetName;
|
|
AzFramework::StringFunc::Path::GetFullFileName(sliceAssetPath.c_str(), sliceAssetName);
|
|
if (sliceAssetName.empty())
|
|
{
|
|
AZ_Warning("Slice", false, "Failed to determine path/name for slice with id %s", sliceAssetId.ToString<AZStd::string>().c_str());
|
|
continue;
|
|
}
|
|
|
|
// Build the menu item. A QWidgetAction is used instead of a QAction to allow the ancestry
|
|
// hierarchy to be represented by indenting each ancestor under the previous.
|
|
// The layout for each row is: [QLabel indent][QLabel Slice icon][QLabel Slice Name][QLable From]
|
|
|
|
// Create the container for the row: A WidgetAction to attach to the menu,
|
|
// the base Widget to contain the horizontal layout
|
|
QWidgetAction* reassignToAction = new QWidgetAction(detachMenu);
|
|
|
|
bool isLastAncestor = currentAncestorIndex >= (ancestors.size() - 1);
|
|
QWidget* sliceLayoutWidget = new DetachMenuActionWidget(detachMenu, indentation, sliceAssetName, isLastAncestor);
|
|
reassignToAction->setDefaultWidget(sliceLayoutWidget);
|
|
|
|
if (isLastAncestor)
|
|
{
|
|
reassignToAction->setToolTip(QObject::tr("The selection currently inherits from this slice"));
|
|
}
|
|
else
|
|
{
|
|
reassignToAction->setToolTip(QObject::tr("Reassign the selection to this slice"));
|
|
|
|
QWidget* mainWindow = nullptr;
|
|
EditorRequests::Bus::BroadcastResult(mainWindow, &EditorRequests::Bus::Events::GetMainWindow);
|
|
|
|
MoveToSliceLevelConfirmation* confirmationMessageBox = new MoveToSliceLevelConfirmation(mainWindow, lastAncestorName, sliceAssetName);
|
|
confirmationMessageBox->setWindowTitle(QObject::tr("Move to slice level"));
|
|
|
|
// Action to represent when we confirm
|
|
QAction* confirmSelected = new QAction(detachMenu);
|
|
confirmationMessageBox->addAction(confirmSelected);
|
|
|
|
QObject::connect(reassignToAction, &QAction::triggered, [reassignToAction, confirmationMessageBox, selectedEntity, ancestors, currentAncestorIndex]() mutable
|
|
{
|
|
if (confirmationMessageBox->exec() == QDialog::Accepted)
|
|
{
|
|
if (currentAncestorIndex < ancestors.size() && currentAncestorIndex + 1 < ancestors.size())
|
|
{
|
|
Internal::FlattenAncestry(ancestors[currentAncestorIndex], ancestors[currentAncestorIndex + 1]);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
detachMenu->addAction(reassignToAction);
|
|
++indentation;
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
DetachMenuActionWidget::DetachMenuActionWidget(QWidget* parent, const int& indentation, const AZStd::string& sliceAssetName, const bool& isLastAncestor)
|
|
:QWidget(parent)
|
|
, m_toLabel(nullptr)
|
|
, m_sliceLabel(nullptr)
|
|
{
|
|
setObjectName("SliceHierarchyMenuItem");
|
|
// Add class to fix hover state styling for WidgetAction
|
|
AzQtComponents::Style::addClass(this, "WidgetAction");
|
|
QHBoxLayout* sliceLayout = new QHBoxLayout(parent);
|
|
setLayout(sliceLayout);
|
|
|
|
QPixmap lShapeIcon(GetLShapeIconPath());
|
|
int LShapeIconWidgetWidth = sliceLayout->contentsMargins().left() + GetLShapeIconSize().width() + sliceLayout->contentsMargins().right();
|
|
|
|
if (indentation > 0)
|
|
{
|
|
QLabel* indentLabel = new QLabel(parent);
|
|
indentLabel->setFixedSize(GetSliceHierarchyMenuIdentationPerLevel() + (indentation - 1) * LShapeIconWidgetWidth, GetSliceItemHeight());
|
|
sliceLayout->addWidget(indentLabel);
|
|
|
|
// Add the L shape icon to show the slice hierarchy
|
|
QLabel* lShapeIconLabel = new QLabel(parent);
|
|
lShapeIconLabel->setPixmap(lShapeIcon);
|
|
lShapeIconLabel->setFixedSize(GetLShapeIconSize());
|
|
sliceLayout->addWidget(lShapeIconLabel);
|
|
}
|
|
|
|
QPixmap sliceItemIcon(GetSliceItemIconPath());
|
|
|
|
// Use the SliceIcon to visually reinforce that this is a slice file.
|
|
QLabel* iconLabel = new QLabel(parent);
|
|
iconLabel->setPixmap(sliceItemIcon);
|
|
iconLabel->setFixedSize(GetSliceItemIconSize());
|
|
sliceLayout->addWidget(iconLabel);
|
|
|
|
// Use the filename without the path as the label for this menu icon, to match the QuickPush menu's behavior.
|
|
m_sliceLabel = new QLabel((sliceAssetName + " ").c_str(), parent);
|
|
sliceLayout->addWidget(m_sliceLabel, Qt::Alignment::enum_type::AlignLeft);
|
|
|
|
// If we are not the last ancestor we will be a selectable menu item
|
|
if (isLastAncestor)
|
|
{
|
|
// The last ancestor is not a selectable option as no ancestry would be flattened
|
|
setEnabled(false);
|
|
|
|
// Label denoting the base ancestor and where we're flattening from
|
|
QLabel* fromLabel = new QLabel(QObject::tr("<i>From</i>"), parent);
|
|
fromLabel->setObjectName("ContextLabelEnabled");
|
|
sliceLayout->addWidget(fromLabel);
|
|
sliceLayout->setAlignment(fromLabel, Qt::Alignment::enum_type::AlignRight);
|
|
}
|
|
else
|
|
{
|
|
// Label denoting the base ancestor and where we're flattening from
|
|
m_toLabel = new QLabel(QObject::tr("<i>To</i>"), parent);
|
|
m_toLabel->setObjectName("ContextLabelEnabled");
|
|
sliceLayout->addWidget(m_toLabel);
|
|
sliceLayout->setAlignment(m_toLabel, Qt::Alignment::enum_type::AlignRight);
|
|
m_toLabel->hide();
|
|
}
|
|
}
|
|
|
|
void DetachMenuActionWidget::enterEvent(QEvent* event)
|
|
{
|
|
if (m_toLabel)
|
|
{
|
|
m_toLabel->show();
|
|
}
|
|
|
|
setsliceLabelTextColor(detachMenuItemHoverColor);
|
|
|
|
QWidget::enterEvent(event);
|
|
}
|
|
|
|
void DetachMenuActionWidget::leaveEvent(QEvent* event)
|
|
{
|
|
if (m_toLabel)
|
|
{
|
|
m_toLabel->hide();
|
|
}
|
|
|
|
setsliceLabelTextColor(detachMenuItemDefaultColor);
|
|
|
|
QWidget::leaveEvent(event);
|
|
}
|
|
|
|
void DetachMenuActionWidget::setsliceLabelTextColor(QString color)
|
|
{
|
|
m_sliceLabel->setStyleSheet(QString("QLabel{color : %1;}").arg(color));
|
|
}
|
|
|
|
//=========================================================================
|
|
MoveToSliceLevelConfirmation::MoveToSliceLevelConfirmation(QWidget* parent, const AZStd::string& currentSlice, const AZStd::string& destinationSlice)
|
|
:QDialog(parent)
|
|
{
|
|
QVBoxLayout* confirmationDialogLayout = new QVBoxLayout(parent);
|
|
|
|
QWidget* warningWidget = new QWidget(parent);
|
|
QHBoxLayout* warningWidgetLayout = new QHBoxLayout(parent);
|
|
|
|
QLabel* iconLabel = new QLabel(parent);
|
|
QPixmap warningIcon(GetWarningIconPath());
|
|
// Use the same size for the no saveable changes icon as the slice icon.
|
|
iconLabel->setFixedSize(GetWarningIconSize());
|
|
iconLabel->setPixmap(warningIcon);
|
|
warningWidgetLayout->addWidget(iconLabel, Qt::AlignLeft);
|
|
|
|
QLabel* warningTextLabel = new QLabel(QObject::tr("You are about to reassign entities. This operation cannot be undone. Do you want to continue?"), parent);
|
|
warningTextLabel->setMinimumWidth(GetWarningLabelMinimumWidth());
|
|
warningTextLabel->setWordWrap(true);
|
|
warningWidgetLayout->addWidget(warningTextLabel);
|
|
|
|
warningWidget->setLayout(warningWidgetLayout);
|
|
|
|
confirmationDialogLayout->addWidget(warningWidget);
|
|
|
|
QWidget* detailWidget = new QWidget(parent);
|
|
detailWidget->setStyleSheet(QString("background-color:%1;").arg(detailWidgetBackgroundColor));
|
|
QGridLayout* detailWidgetLayout = new QGridLayout(parent);
|
|
detailWidgetLayout->setColumnStretch(0, 0);
|
|
detailWidgetLayout->setColumnStretch(1, 1);
|
|
|
|
detailWidgetLayout->addWidget(new QLabel("From: ", parent), 0, 0, Qt::AlignRight);
|
|
detailWidgetLayout->addWidget(new QLabel(currentSlice.c_str(), parent), 0, 1, Qt::AlignLeft);
|
|
detailWidgetLayout->addWidget(new QLabel("To: ", parent), 1, 0, Qt::AlignRight);
|
|
detailWidgetLayout->addWidget(new QLabel(destinationSlice.c_str(), parent), 1, 1, Qt::AlignLeft);
|
|
detailWidget->setLayout(detailWidgetLayout);
|
|
confirmationDialogLayout->addWidget(detailWidget);
|
|
|
|
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
|
|
| QDialogButtonBox::Cancel, parent);
|
|
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
|
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
|
confirmationDialogLayout->addWidget(buttonBox);
|
|
|
|
setLayout(confirmationDialogLayout);
|
|
}
|
|
|
|
//=========================================================================
|
|
bool SaveSlice(
|
|
const AzToolsFramework::EntityIdList& inputEntities,
|
|
int& numEntitiesToAdd,
|
|
int& numEntitiesToRemove,
|
|
int& numEntitiesToUpdate,
|
|
const bool& QuickPushToFirstLevel)
|
|
{
|
|
size_t numRelevantEntitiesInSlices;
|
|
AZStd::unordered_set<AZ::EntityId> entitiesToAdd;
|
|
AZStd::unordered_set<AZ::EntityId> entitiesToRemove;
|
|
AZStd::unordered_map<int, AZ::u32> numEntitiesToUpdateMapping;
|
|
QMenu emptyMenu;
|
|
|
|
QMenu* quickPushMenu = Internal::GenerateQuickPushMenu(
|
|
&emptyMenu,
|
|
numRelevantEntitiesInSlices,
|
|
entitiesToAdd,
|
|
entitiesToRemove,
|
|
numEntitiesToUpdateMapping,
|
|
inputEntities,
|
|
nullptr,
|
|
QuickPushMenuOptions());
|
|
|
|
numEntitiesToAdd = static_cast<int>(entitiesToAdd.size());
|
|
numEntitiesToRemove = static_cast<int>(entitiesToRemove.size());
|
|
|
|
if (quickPushMenu && quickPushMenu->actions().size() > 0)
|
|
{
|
|
numEntitiesToUpdate = QuickPushToFirstLevel ? numEntitiesToUpdateMapping[quickPushMenu->actions().size() - 1] : numEntitiesToUpdateMapping[0];
|
|
|
|
QAction* action = QuickPushToFirstLevel ? quickPushMenu->actions().last() : quickPushMenu->actions().first();
|
|
if (action->isEnabled())
|
|
{
|
|
action->triggered();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool DoEntitiesHaveOverrides(const AzToolsFramework::EntityIdList& inputEntities)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Error("Slice", false, "Could not retrieve application serialize context.");
|
|
return false;
|
|
}
|
|
|
|
AZ::SliceComponent::EntityAncestorList tempAncestors;
|
|
for (AZ::EntityId entityId : inputEntities)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.IsValid())
|
|
{
|
|
tempAncestors.clear();
|
|
|
|
// We only want the immediate ancestor so pass 1 for max levels
|
|
sliceAddress.GetReference()->GetInstanceEntityAncestry(entityId, tempAncestors, 1);
|
|
|
|
for (const AZ::SliceComponent::Ancestor& entityAncestor : tempAncestors)
|
|
{
|
|
AZStd::unique_ptr<AZ::Entity> compareClone = CloneSliceEntityForComparison(*entityAncestor.m_entity, *sliceAddress.GetInstance(), *serializeContext);
|
|
|
|
const AZ::u32 numDifferences = Internal::CountDifferencesVersusSlice(entityId, compareClone.get(), *serializeContext, nullptr);
|
|
if (numDifferences > 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool IsReparentNonTrivial(const AZ::EntityId& entityId, const AZ::EntityId& newParentId)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZ::EntityId oldParentId;
|
|
AZ::TransformBus::EventResult(oldParentId, entityId, &AZ::TransformBus::Events::GetParentId);
|
|
|
|
bool isSliceEntity = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceEntity, entityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceEntity);
|
|
|
|
bool isSliceRoot = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceRoot, entityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceRoot);
|
|
|
|
bool isNonTrivial = false;
|
|
|
|
if (isSliceEntity && !isSliceRoot)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress oldParentSliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(oldParentSliceAddress, oldParentId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
AZ::SliceComponent::SliceInstanceAddress newParentSliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(newParentSliceAddress, newParentId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
// additional checks are necessary to determine if the entity hierarchy should be cloned when re-ordering the same slice instance
|
|
if (oldParentSliceAddress.IsValid() && newParentSliceAddress.IsValid() && (oldParentSliceAddress.GetInstance() == newParentSliceAddress.GetInstance()))
|
|
{
|
|
bool isSubsliceEntity = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSubsliceEntity, entityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceEntity);
|
|
|
|
bool isOldParentSubsliceEntity = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isOldParentSubsliceEntity, oldParentId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceEntity);
|
|
|
|
bool isNewParentSubsliceEntity = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isNewParentSubsliceEntity, newParentId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceEntity);
|
|
|
|
// moving into or out of a subslice
|
|
if (isOldParentSubsliceEntity != isNewParentSubsliceEntity)
|
|
{
|
|
isNonTrivial = true;
|
|
}
|
|
// moving between subslices
|
|
else if (isSubsliceEntity && isOldParentSubsliceEntity && isNewParentSubsliceEntity)
|
|
{
|
|
Internal::SliceInstanceList sliceHistory;
|
|
Internal::GetSliceInstanceAncestry(entityId, sliceHistory);
|
|
|
|
Internal::SliceInstanceList newParentSliceHistory;
|
|
Internal::GetSliceInstanceAncestry(newParentId, newParentSliceHistory);
|
|
|
|
for (auto& sliceInstance : sliceHistory)
|
|
{
|
|
if (AZStd::find(newParentSliceHistory.begin(), newParentSliceHistory.end(), sliceInstance) == newParentSliceHistory.end())
|
|
{
|
|
isNonTrivial = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// otherwise, the entity hierarchy should always be cloned if the parents are part of different slice instances
|
|
else
|
|
{
|
|
isNonTrivial = true;
|
|
}
|
|
}
|
|
|
|
return isNonTrivial;
|
|
}
|
|
|
|
void ReparentNonTrivialSliceInstanceHierarchy(const AZ::EntityId& entityId, const AZ::EntityId& newParentId)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZ::SliceComponent::SliceInstanceEntityIdRemapList subslicesToDetach;
|
|
AzToolsFramework::EntityIdList entitiesToDetach;
|
|
|
|
Internal::PartitionEntityHierarchyForNonTrivialReparent(entityId, subslicesToDetach, entitiesToDetach);
|
|
|
|
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Broadcast(
|
|
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::DetachSubsliceInstances, subslicesToDetach);
|
|
|
|
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Broadcast(
|
|
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::DetachSliceEntities, entitiesToDetach);
|
|
|
|
AZ::TransformBus::Event(entityId, &AZ::TransformBus::Events::SetParent, newParentId);
|
|
}
|
|
|
|
InvalidSliceReferencesWarningResult DisplayInvalidSliceReferencesWarning(
|
|
QWidget* parent,
|
|
size_t invalidSliceCount,
|
|
size_t invalidReferenceCount,
|
|
bool showDetailsButton)
|
|
{
|
|
QMessageBox warningMessageBox(parent);
|
|
warningMessageBox.setObjectName("SliceUtilities.warningMessageBox");
|
|
warningMessageBox.setWindowTitle(QObject::tr("Remove invalid references"));
|
|
// Qt's .arg(firstArg, secondArg) changes behavior on the data type of firstArg and secondArg.
|
|
// If firstArg and secondArg are both QStrings, then secondArg will actually replace %2.
|
|
// If secondArg is an integer value, then Qt will use a different override for arg, where secondArg
|
|
// is treated as a width value. See http://doc.qt.io/archives/qt-4.8/qstring.html#arg and
|
|
// http://doc.qt.io/archives/qt-4.8/qstring.html#arg-2
|
|
warningMessageBox.setText(
|
|
QObject::tr("This change will remove %1 invalid reference(s) from %2 slice file(s). You can't undo this change.")
|
|
.arg(invalidReferenceCount)
|
|
.arg(invalidSliceCount));
|
|
|
|
// Add the two buttons that are always available: Cancel and Confirm.
|
|
warningMessageBox.setStandardButtons(QMessageBox::Cancel);
|
|
QAbstractButton* saveButton = warningMessageBox.addButton(QObject::tr("Confirm"), QMessageBox::AcceptRole);
|
|
|
|
// Add the details button if it needs to be there.
|
|
QAbstractButton* detailsButton = nullptr;
|
|
if (showDetailsButton)
|
|
{
|
|
detailsButton = warningMessageBox.addButton(QObject::tr("More details"), QMessageBox::ActionRole);
|
|
}
|
|
|
|
QIcon linkStateError = QIcon(":/PropertyEditor/Resources/error_link_state.png");
|
|
warningMessageBox.setIconPixmap(linkStateError.pixmap(linkStateError.availableSizes().first()));
|
|
|
|
// Open the message box and wait for the user to make a choice.
|
|
int warningMessageResult = warningMessageBox.exec();
|
|
|
|
// Check which button the user clicked on, and return the result.
|
|
QAbstractButton* clickedButton = warningMessageBox.clickedButton();
|
|
|
|
if (detailsButton != nullptr && clickedButton == detailsButton)
|
|
{
|
|
return InvalidSliceReferencesWarningResult::Details;
|
|
}
|
|
if (clickedButton == saveButton)
|
|
{
|
|
return InvalidSliceReferencesWarningResult::Save;
|
|
}
|
|
switch (warningMessageResult)
|
|
{
|
|
case QMessageBox::Cancel:
|
|
return InvalidSliceReferencesWarningResult::Cancel;
|
|
default:
|
|
AZ_Error("InvalidSliceReferencesPopup",
|
|
false,
|
|
"Invalid slice references warning popup dismissed with result %d. "
|
|
"This result will be treated as cancel, try again if you did not wish to cancel.",
|
|
warningMessageResult);
|
|
return InvalidSliceReferencesWarningResult::Cancel;
|
|
}
|
|
|
|
}
|
|
|
|
bool CountPushableChangesToSlice(const AzToolsFramework::EntityIdList& inputEntities,
|
|
const InstanceDataNode::Address* fieldAddress,
|
|
AZStd::unordered_set<AZ::EntityId>& entitiesToAdd,
|
|
AZStd::unordered_set<AZ::EntityId>& entitiesToRemove,
|
|
size_t& numRelevantEntitiesInSlices,
|
|
AZStd::unordered_map<AZ::Data::AssetId, int>& numPushableChangesPerAsset,
|
|
AZStd::vector<AZ::Data::AssetId>& sliceDisplayOrder,
|
|
AZStd::unordered_map<AZ::Data::AssetId, AZStd::vector<EntityAncestorPair>>& assetEntityAncestorMap,
|
|
AZStd::unordered_map<AZ::Data::AssetId, EntityIdSet>& unpushableEntityIdsPerAsset)
|
|
{
|
|
bool haveNewOrDeletedEntities = false;
|
|
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Error("Slice", false, "Could not retrieve application serialize context.");
|
|
return false;
|
|
}
|
|
|
|
numRelevantEntitiesInSlices = 0;
|
|
AZStd::unordered_map<AZ::EntityId, AZ::SliceComponent::EntityAncestorList> sliceAncestryMapping;
|
|
AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>> newChildEntityIdAncestorPairs;
|
|
|
|
AZStd::unordered_set<AZ::EntityId> pushableNewChildEntityIds = GetPushableNewChildEntityIds(inputEntities,
|
|
unpushableEntityIdsPerAsset,
|
|
sliceAncestryMapping,
|
|
newChildEntityIdAncestorPairs,
|
|
entitiesToAdd);
|
|
|
|
AZStd::vector<AZ::SliceComponent::SliceInstanceAddress> sliceInstances;
|
|
for (AZ::EntityId entityId : inputEntities)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (entitySliceAddress.IsValid())
|
|
{
|
|
if (sliceInstances.end() == AZStd::find(sliceInstances.begin(), sliceInstances.end(), entitySliceAddress))
|
|
{
|
|
sliceInstances.push_back(entitySliceAddress);
|
|
}
|
|
}
|
|
}
|
|
|
|
IdToEntityMapping assetEntityIdtoAssetEntityMapping;
|
|
IdToInstanceAddressMapping assetEntityIdtoInstanceAddressMapping;
|
|
entitiesToRemove = GetUniqueRemovedEntities(sliceInstances, assetEntityIdtoAssetEntityMapping, assetEntityIdtoInstanceAddressMapping);
|
|
|
|
haveNewOrDeletedEntities = !entitiesToRemove.empty() || !entitiesToAdd.empty();
|
|
|
|
AZ::SliceComponent::EntityAncestorList tempAncestors;
|
|
|
|
for (AZ::EntityId entityId : inputEntities)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.IsValid())
|
|
{
|
|
if (entitiesToAdd.find(entityId) != entitiesToAdd.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
tempAncestors.clear();
|
|
sliceAddress.GetReference()->GetInstanceEntityAncestry(entityId, tempAncestors);
|
|
|
|
for (const AZ::SliceComponent::Ancestor& ancestor : tempAncestors)
|
|
{
|
|
const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset = ancestor.m_sliceAddress.GetReference()->GetSliceAsset();
|
|
AZStd::vector<EntityAncestorPair>& entityAncestors = assetEntityAncestorMap[sliceAsset.GetId()];
|
|
AZStd::unique_ptr<AZ::Entity> compareClone = CloneSliceEntityForComparison(*ancestor.m_entity, *ancestor.m_sliceAddress.GetInstance(), *serializeContext);
|
|
AZ_Error("Slice", compareClone.get(), "Failed to clone entity for slice comparison.");
|
|
if (compareClone)
|
|
{
|
|
entityAncestors.emplace_back(entityId, compareClone.release());
|
|
}
|
|
|
|
// Maintain a display-order array of slice assets.
|
|
if (sliceDisplayOrder.end() == AZStd::find(sliceDisplayOrder.begin(), sliceDisplayOrder.end(), sliceAsset.GetId()))
|
|
{
|
|
sliceDisplayOrder.push_back(sliceAsset.GetId());
|
|
}
|
|
}
|
|
|
|
++numRelevantEntitiesInSlices;
|
|
}
|
|
}
|
|
|
|
bool pushableChangesAvailable = Internal::CalculatePushableChangesPerAsset(
|
|
numPushableChangesPerAsset,
|
|
*serializeContext,
|
|
sliceDisplayOrder,
|
|
assetEntityAncestorMap,
|
|
numRelevantEntitiesInSlices,
|
|
fieldAddress);
|
|
|
|
return pushableChangesAvailable || haveNewOrDeletedEntities;
|
|
}
|
|
|
|
bool IsDynamic(const AZ::Data::AssetId& assetId)
|
|
{
|
|
AZStd::shared_ptr<AZ::Entity> sliceEntity = Internal::GetSliceEntityForAssetId(assetId);
|
|
AZ::SliceComponent* sliceComponent = sliceEntity ? sliceEntity->FindComponent<AZ::SliceComponent>() : nullptr;
|
|
if (sliceComponent)
|
|
{
|
|
return sliceComponent->IsDynamic();
|
|
}
|
|
else
|
|
{
|
|
AZ_Warning("Slice", false, "Asset %s does not contain a slice component", assetId.ToString<AZStd::string>().c_str());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SetIsDynamic(const AZ::Data::AssetId& assetId, bool isDynamic)
|
|
{
|
|
AZStd::shared_ptr<AZ::Entity> sliceEntity = Internal::GetSliceEntityForAssetId(assetId);
|
|
AZ::SliceComponent* sliceComponent = sliceEntity ? sliceEntity->FindComponent<AZ::SliceComponent>() : nullptr;
|
|
if (sliceComponent)
|
|
{
|
|
AZStd::string relativePath, fullPath;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(relativePath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, assetId);
|
|
|
|
bool fullPathFound = false;
|
|
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(fullPathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, relativePath, fullPath);
|
|
if (fullPathFound)
|
|
{
|
|
sliceComponent->SetIsDynamic(isDynamic);
|
|
Internal::ResaveSlice(sliceEntity, fullPath);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AZ_Warning("Slice", false, "Asset %s does not contain a slice component", assetId.ToString<AZStd::string>().c_str());
|
|
}
|
|
}
|
|
|
|
void CreateSliceAssetContextMenu(QMenu* menu, const AZStd::string& fullFilePath)
|
|
{
|
|
if (!menu)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AZStd::string relativePath;
|
|
bool relativePathFound = false;
|
|
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(relativePathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetRelativeProductPathFromFullSourceOrProductPath, fullFilePath, relativePath);
|
|
|
|
AZ::Data::AssetId sliceAssetId;
|
|
if (relativePathFound)
|
|
{
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(sliceAssetId, &AZ::Data::AssetCatalogRequestBus::Events::GetAssetIdByPath, relativePath.c_str(), AZ::Data::s_invalidAssetType, false);
|
|
}
|
|
|
|
if (!sliceAssetId.IsValid())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// For slices, we provide the option to toggle the dynamic flag.
|
|
bool isDynamic = IsDynamic(sliceAssetId);
|
|
QString sliceOptions[] = { QObject::tr("Set Dynamic Slice"), QObject::tr("Unset Dynamic Slice") };
|
|
QString optionLabel = isDynamic ? sliceOptions[1] : sliceOptions[0];
|
|
menu->addAction(optionLabel, [sliceAssetId, isDynamic]()
|
|
{
|
|
SetIsDynamic(sliceAssetId, !isDynamic);
|
|
});
|
|
}
|
|
|
|
void RemoveInvalidChildOrderArrayEntries(const AZStd::vector<AZ::EntityId>& originalOrderArray,
|
|
AZStd::vector<AZ::EntityId>& prunedOrderArray,
|
|
const AZ::Data::Asset <AZ::SliceAsset>& targetSlice,
|
|
WillPushEntityCallback willPushEntityCallback)
|
|
{
|
|
// Build prunedOrderArray as a copy of originalOrderArray but with only valid entity ids.
|
|
for (const AZ::EntityId& childId : originalOrderArray)
|
|
{
|
|
if (willPushEntityCallback(childId, targetSlice))
|
|
{
|
|
prunedOrderArray.push_back(childId);
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::DataStream::StreamType GetSliceStreamFormat()
|
|
{
|
|
return AZ::DataStream::ST_XML;
|
|
}
|
|
|
|
namespace Internal
|
|
{
|
|
SliceSaveResult IsSlicePathValidForAssets(QWidget* activeWindow, QString slicePath, AZStd::string &retrySavePath)
|
|
{
|
|
bool assetSetFoldersRetrieved = false;
|
|
AZStd::vector<AZStd::string> assetSafeFolders;
|
|
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(
|
|
assetSetFoldersRetrieved,
|
|
&AzToolsFramework::AssetSystemRequestBus::Events::GetAssetSafeFolders,
|
|
assetSafeFolders);
|
|
|
|
if (!assetSetFoldersRetrieved)
|
|
{
|
|
// If the asset safe list couldn't be retrieved, don't block the user but warn them.
|
|
AZ_Warning("Slice", false, "Unable to verify that the slice file to create is in a valid path.");
|
|
}
|
|
else
|
|
{
|
|
QString cleanSaveAs(QDir::cleanPath(slicePath));
|
|
|
|
bool isPathSafeForAssets = false;
|
|
for (AZStd::string assetSafeFolder : assetSafeFolders)
|
|
{
|
|
QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str()));
|
|
// Compare using clean paths so slash direction does not matter.
|
|
// Note that this comparison is case sensitive because some file systems
|
|
// Open 3D Engine supports are case sensitive.
|
|
if (cleanSaveAs.startsWith(cleanAssetSafeFolder))
|
|
{
|
|
isPathSafeForAssets = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isPathSafeForAssets)
|
|
{
|
|
// Put an error in the console, so the log files have info about this error, or the user can look up the error after dismissing it.
|
|
AZStd::string errorMessage = "You can save slices only to your game project folder or the Gems folder. Update the location and try again.\n\n"
|
|
"You can also review and update your save locations in the AssetProcessorPlatformConfig.ini file.";
|
|
AZ_Error("Slice", false, errorMessage.c_str());
|
|
|
|
QString learnMoreLink(QObject::tr("https://docs.aws.amazon.com/console/lumberyard/slices/save"));
|
|
QString learnMoreDescription(QObject::tr(" <a href='%1'>Learn more</a>").arg(learnMoreLink));
|
|
|
|
// Display a pop-up, the logs are easy to miss. This will make sure a user who encounters this error immediately knows their slice save has failed.
|
|
QMessageBox msgBox(activeWindow);
|
|
msgBox.setIcon(QMessageBox::Icon::Warning);
|
|
msgBox.setTextFormat(Qt::RichText);
|
|
msgBox.setWindowTitle(QObject::tr("Invalid save location"));
|
|
msgBox.setText(QString("%1 %2").arg(QObject::tr(errorMessage.c_str())).arg(learnMoreDescription));
|
|
msgBox.setStandardButtons(QMessageBox::Cancel | QMessageBox::Retry);
|
|
msgBox.setDefaultButton(QMessageBox::Retry);
|
|
const int response = msgBox.exec();
|
|
switch (response)
|
|
{
|
|
case QMessageBox::Retry:
|
|
// If the user wants to retry, they probably want to save to a valid location,
|
|
// so set the suggested save path to a known valid location.
|
|
if (assetSafeFolders.size() > 0)
|
|
{
|
|
retrySavePath = assetSafeFolders[0];
|
|
}
|
|
return SliceSaveResult::Retry;
|
|
case QMessageBox::Cancel:
|
|
default:
|
|
return SliceSaveResult::Cancel;
|
|
}
|
|
}
|
|
}
|
|
// Valid slice save location, continue with the save attempt.
|
|
return SliceSaveResult::Continue;
|
|
}
|
|
|
|
//=========================================================================
|
|
void GetSliceEntityAncestors(const AZ::EntityId& entityId, AZ::SliceComponent::EntityAncestorList& ancestors)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.IsValid())
|
|
{
|
|
sliceAddress.GetReference()->GetInstanceEntityAncestry(entityId, ancestors);
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
void GetSubsliceEntityAncestors(const AZ::EntityId& entityId, AZ::SliceComponent::EntityAncestorList& ancestors)
|
|
{
|
|
GetSliceEntityAncestors(entityId, ancestors);
|
|
if (!ancestors.empty())
|
|
{
|
|
// Pop the first ancestor as we only care about sub-slices
|
|
ancestors.erase(ancestors.begin());
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
void PartitionEntityHierarchyForNonTrivialReparent(const AZ::EntityId& rootEntity,
|
|
AZ::SliceComponent::SliceInstanceEntityIdRemapList& subslicesToDetach,
|
|
AzToolsFramework::EntityIdList& entitiesToDetach)
|
|
{
|
|
// Acquire the owning slice of the rootEntity hierarchy
|
|
AZ::SliceComponent::SliceInstanceAddress sourceSliceInstance;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sourceSliceInstance, rootEntity,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (!sourceSliceInstance.IsValid())
|
|
{
|
|
AZ_Warning("SliceUtilities::Internal::PartitionEntityHierarchyForNonTrivialReparent",
|
|
false,
|
|
"Passed in Root Entity with Id: %s has no owning slice. Unable to detach its hierarchy",
|
|
rootEntity.ToString().c_str());
|
|
|
|
return;
|
|
}
|
|
|
|
// Acquire the hierarchy found below the rootEntity
|
|
AzToolsFramework::EntityIdSet entityHierarchy;
|
|
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(entityHierarchy, &AzToolsFramework::ToolsApplicationRequestBus::Events::GatherEntitiesAndAllDescendents, AzToolsFramework::EntityIdList{ rootEntity });
|
|
|
|
// Sort the hierarchy
|
|
AzToolsFramework::EntityIdList entityHierarchySortedByDepthAndOrder(entityHierarchy.begin(), entityHierarchy.end());
|
|
AzToolsFramework::SortEntitiesByLocationInHierarchy(entityHierarchySortedByDepthAndOrder);
|
|
|
|
entitiesToDetach.reserve(entityHierarchySortedByDepthAndOrder.size());
|
|
|
|
AZ::SliceComponent::SliceInstanceAddressSet subslices;
|
|
for (const AZ::EntityId& currentEntityId : entityHierarchySortedByDepthAndOrder)
|
|
{
|
|
// Get the owning slice of the current entity
|
|
AZ::SliceComponent::SliceInstanceAddress currentSliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(currentSliceAddress, currentEntityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
// Only need to detach entities owned by the same slice instance of the entity being reparented.
|
|
if (currentSliceAddress.GetInstance() != sourceSliceInstance.GetInstance())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool isSubsliceEntity = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSubsliceEntity, currentEntityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceEntity);
|
|
|
|
// If it's not a subslice entity we can directly remove the entity from the owning slice
|
|
if (!isSubsliceEntity)
|
|
{
|
|
entitiesToDetach.emplace_back(currentEntityId);
|
|
continue;
|
|
}
|
|
|
|
// If the current entity is a subslice entity we need to determine if we should detach it and its children from its current owning slice
|
|
AZ::SliceComponent::EntityAncestorList currentEntityAncestry;
|
|
currentSliceAddress.GetReference()->GetInstanceEntityAncestry(currentEntityId, currentEntityAncestry, std::numeric_limits<uint32_t>::max());
|
|
|
|
if (currentEntityAncestry.empty())
|
|
{
|
|
subslicesToDetach.clear();
|
|
entitiesToDetach.clear();
|
|
|
|
AZ_Error("SliceUtilities::Internal::PartitionEntityHierarchyForNonTrivialReparent",
|
|
false,
|
|
"Entity with Id: %s marked as a Subslice Root entity but has no slice ancestry, unable to proceed",
|
|
currentEntityId.ToString().c_str());
|
|
|
|
return;
|
|
}
|
|
|
|
bool subsliceEntityHandled = false;
|
|
AZ::SliceComponent::SliceInstanceAddress subsliceRootAncestorAddress;
|
|
AZStd::vector<AZ::SliceComponent::SliceInstanceAddress> subsliceAncestry;
|
|
|
|
// Walk the ancestry until we either find an ancestor whose entity is a root (subslice root)
|
|
// or we find the ancestor's slice address in our handled subslices set
|
|
for (const AZ::SliceComponent::Ancestor& subsliceAncestor : currentEntityAncestry)
|
|
{
|
|
if (subslices.find(subsliceAncestor.m_sliceAddress) != subslices.end())
|
|
{
|
|
subsliceEntityHandled = true;
|
|
break;
|
|
}
|
|
|
|
if (subsliceAncestor.m_entity && IsRootEntity(*subsliceAncestor.m_entity))
|
|
{
|
|
subsliceRootAncestorAddress = subsliceAncestor.m_sliceAddress;
|
|
break;
|
|
}
|
|
|
|
subsliceAncestry.emplace_back(subsliceAncestor.m_sliceAddress);
|
|
}
|
|
|
|
// Verify we have not handled this subslice entity or a subslice owning this entity already
|
|
if (subsliceEntityHandled)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool isSubsliceRoot = false;
|
|
AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSubsliceRoot, currentEntityId, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSubsliceRoot);
|
|
|
|
if (!isSubsliceRoot)
|
|
{
|
|
// If it's not a subslice root we can directly remove the entity from the owning slice without worrying about children beneath it
|
|
entitiesToDetach.emplace_back(currentEntityId);
|
|
continue;
|
|
}
|
|
|
|
// Remove the owning slice from the ancestry list
|
|
subsliceAncestry.erase(subsliceAncestry.begin());
|
|
|
|
// Acquire a mapping from the asset id of the subslice to the live id of the source slice
|
|
// This will allow us to determine the specific list of entities associated with the subslice to extract from the source slice instance
|
|
AZ::SliceComponent::EntityIdToEntityIdMap liveToSubsliceIdMapping;
|
|
AZ::SliceComponent::GetMappingBetweenSubsliceAndSourceInstanceEntityIds(currentSliceAddress.GetInstance(),
|
|
subsliceAncestry,
|
|
subsliceRootAncestorAddress,
|
|
liveToSubsliceIdMapping,
|
|
true);
|
|
|
|
// Filter out MetaData entities from the list
|
|
// We will provide new ones for the subslice when we detach it into a standalone slice instance
|
|
AZStd::vector<AZ::EntityId> metaDataEntities;
|
|
for (const AZStd::pair<AZ::EntityId, AZ::EntityId>& liveToSubslicePair : liveToSubsliceIdMapping)
|
|
{
|
|
bool isMetaDataEntity = false;
|
|
AzToolsFramework::SliceMetadataEntityContextRequestBus::BroadcastResult(isMetaDataEntity, &AzToolsFramework::SliceMetadataEntityContextRequestBus::Events::IsSliceMetadataEntity, liveToSubslicePair.first);
|
|
|
|
if (isMetaDataEntity)
|
|
{
|
|
metaDataEntities.emplace_back(liveToSubslicePair.first);
|
|
}
|
|
}
|
|
|
|
for (AZ::EntityId metaDataEntity : metaDataEntities)
|
|
{
|
|
liveToSubsliceIdMapping.erase(metaDataEntity);
|
|
}
|
|
|
|
// Mark this instanceAddress as handled and add it to our detach list
|
|
subslices.emplace(subsliceRootAncestorAddress);
|
|
subslicesToDetach.emplace_back(AZStd::make_pair(subsliceRootAncestorAddress, liveToSubsliceIdMapping));
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
void GetSliceInstanceAncestry(const AZ::EntityId& entityId, SliceInstanceList& sliceInstanceAncestors)
|
|
{
|
|
AZ::SliceComponent::EntityAncestorList ancestors;
|
|
GetSliceEntityAncestors(entityId, ancestors);
|
|
|
|
for (const AZ::SliceComponent::Ancestor& ancestor : ancestors)
|
|
{
|
|
if (ancestor.m_sliceAddress.IsValid())
|
|
{
|
|
sliceInstanceAncestors.push_back(ancestor.m_sliceAddress.GetInstance());
|
|
}
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
void GenerateSuggestedSliceFilenameFromEntities(const AzToolsFramework::EntityIdList& entities, AZStd::string& outName)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
// Determine suggested save name for slice based on entity names
|
|
// For example, with entities Entity0, Entity1, and Entity2, we would end up with
|
|
// Entity0Entity1Entity2
|
|
|
|
AZStd::string sliceName;
|
|
size_t sliceNameCutoffLength = 32; ///< When naming a slice after its entities, we stop appending additional names once we've reached this cutoff length
|
|
|
|
AzToolsFramework::EntityIdSet usedNameEntities;
|
|
auto appendToSliceName = [&sliceName, sliceNameCutoffLength, &usedNameEntities](const AZ::EntityId& id) -> bool
|
|
{
|
|
if (usedNameEntities.find(id) == usedNameEntities.end())
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, id);
|
|
if (entity)
|
|
{
|
|
AZStd::string entityNameFiltered = entity->GetName();
|
|
|
|
// Convert spaces in entity names to underscores
|
|
for (size_t i = 0; i < entityNameFiltered.size(); ++i)
|
|
{
|
|
char& character = entityNameFiltered.at(i);
|
|
if (character == ' ')
|
|
{
|
|
character = '_';
|
|
}
|
|
}
|
|
|
|
sliceName.append(entityNameFiltered);
|
|
usedNameEntities.insert(id);
|
|
if (sliceName.size() > sliceNameCutoffLength)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
bool sliceNameFull = false;
|
|
|
|
if (!sliceNameFull)
|
|
{
|
|
for (const AZ::EntityId& id : entities)
|
|
{
|
|
if (!appendToSliceName(id))
|
|
{
|
|
sliceNameFull = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (sliceName.size() == 0)
|
|
{
|
|
sliceName = "NewSlice";
|
|
}
|
|
else if (AzFramework::StringFunc::Utf8::CheckNonAsciiChar(sliceName))
|
|
{
|
|
sliceName = "NewSlice";
|
|
}
|
|
|
|
outName = sliceName;
|
|
}
|
|
|
|
//=========================================================================
|
|
void GenerateSuggestedSlicePath(const AZStd::string& sliceName, const AZStd::string& targetDirectory, AZStd::string& suggestedFullPath)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
// Generate full suggested path from sliceName - if given NewSlice as sliceName,
|
|
// NewSlice_001.slice would be tried, and if that already existed we would suggest
|
|
// the first unused number value (NewSlice_002.slice etc.)
|
|
suggestedFullPath = targetDirectory;
|
|
if (suggestedFullPath.size() > 0 &&
|
|
suggestedFullPath.at(suggestedFullPath.size() - 1) != '/')
|
|
{
|
|
suggestedFullPath += "/";
|
|
}
|
|
|
|
// Convert spaces in entity names to underscores
|
|
AZStd::string sliceNameFiltered = sliceName;
|
|
for (size_t i = 0; i < sliceNameFiltered.size(); ++i)
|
|
{
|
|
char& character = sliceNameFiltered.at(i);
|
|
if (character == ' ')
|
|
{
|
|
character = '_';
|
|
}
|
|
}
|
|
|
|
auto settings = AZ::UserSettings::CreateFind<SliceUserSettings>(AZ_CRC("SliceUserSettings", 0x055b32eb), AZ::UserSettings::CT_LOCAL);
|
|
if (settings->m_autoNumber)
|
|
{
|
|
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
|
|
AZStd::string possiblePath;
|
|
|
|
const AZ::u32 maxSliceNumber = 1000;
|
|
for (AZ::u32 sliceNumber = 1; sliceNumber < maxSliceNumber; ++sliceNumber)
|
|
{
|
|
possiblePath = AZStd::string::format("%s%s_%3.3u%s", suggestedFullPath.c_str(), sliceNameFiltered.c_str(), sliceNumber, GetSliceFileExtension().c_str());
|
|
if (!fileIO || !fileIO->Exists(possiblePath.c_str()))
|
|
{
|
|
suggestedFullPath = possiblePath;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// use the entity name as the file name regardless of it already existing, the OS will ask the user to overwrite the file in that case.
|
|
suggestedFullPath = AZStd::string::format("%s%s%s", suggestedFullPath.c_str(), sliceNameFiltered.c_str(), GetSliceFileExtension().c_str());
|
|
}
|
|
}
|
|
|
|
//=========================================================================
|
|
void SetSliceSaveLocation(const AZStd::string& path, AZ::u32 settingsId)
|
|
{
|
|
auto settings = AZ::UserSettings::CreateFind<SliceUserSettings>(settingsId, AZ::UserSettings::CT_LOCAL);
|
|
settings->m_saveLocation = path;
|
|
}
|
|
|
|
//=========================================================================
|
|
bool GetSliceSaveLocation(AZStd::string& path, AZ::u32 settingsId)
|
|
{
|
|
auto settings = AZ::UserSettings::Find<SliceUserSettings>(settingsId, AZ::UserSettings::CT_LOCAL);
|
|
if (settings)
|
|
{
|
|
path = settings->m_saveLocation;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
//=========================================================================
|
|
AZ::Vector3 GetSliceRootPosition(const AZ::EntityId commonRoot, const AzToolsFramework::EntityList& selectionRootEntities)
|
|
{
|
|
AZ::Vector3 sliceRootTranslation = AZ::Vector3::CreateZero();
|
|
float sliceZmin = std::numeric_limits<float>::max();
|
|
|
|
int count = 0;
|
|
for (AZ::Entity* selectionRootEntity : selectionRootEntities)
|
|
{
|
|
if (selectionRootEntity)
|
|
{
|
|
AzToolsFramework::Components::TransformComponent* transformComponent =
|
|
selectionRootEntity->FindComponent<AzToolsFramework::Components::TransformComponent>();
|
|
|
|
if (transformComponent)
|
|
{
|
|
count++;
|
|
AZ::Vector3 currentPosition;
|
|
if (commonRoot.IsValid())
|
|
{
|
|
currentPosition = transformComponent->GetLocalTranslation();
|
|
}
|
|
else
|
|
{
|
|
currentPosition = transformComponent->GetWorldTranslation();
|
|
}
|
|
|
|
sliceRootTranslation += currentPosition;
|
|
sliceZmin = AZ::GetMin<float>(sliceZmin, currentPosition.GetZ());
|
|
}
|
|
}
|
|
}
|
|
|
|
sliceRootTranslation = (sliceRootTranslation / static_cast<float>(count));
|
|
|
|
sliceRootTranslation.SetZ(sliceZmin);
|
|
|
|
return sliceRootTranslation;
|
|
}
|
|
|
|
//=========================================================================
|
|
SliceTransaction::Result CheckAndAddSliceRoot(const AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset,
|
|
AZ::SliceComponent::EntityIdToEntityIdMap assetToLiveMap,
|
|
AZStd::string sliceRootName,
|
|
AZStd::pair<AZ::EntityId, AZ::EntityId>& liveAndAssetAutoGeneratedRoots,
|
|
const AZ::Vector3& sliceRootEntityTranslation,
|
|
const AZ::Quaternion& sliceRootEntityRotation,
|
|
QWidget* activeWindow,
|
|
bool defaultGenerateSharedRoot)
|
|
{
|
|
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
|
|
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
return AZ::Failure(AZStd::string("Could not retrieve application serialize context"));
|
|
}
|
|
|
|
AzFramework::EntityList sliceEntities;
|
|
asset.Get()->GetComponent()->GetEntities(sliceEntities);
|
|
|
|
AZ::EntityId commonRoot;
|
|
AzToolsFramework::EntityList selectionRootEntities;
|
|
|
|
bool result = false;
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(result, &AzToolsFramework::ToolsApplicationRequests::FindCommonRootInactive, sliceEntities, commonRoot, &selectionRootEntities);
|
|
|
|
if (result)
|
|
{
|
|
if (selectionRootEntities.size() > 1)
|
|
{
|
|
if (!defaultGenerateSharedRoot)
|
|
{
|
|
int response;
|
|
{
|
|
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "SliceUtilities::CheckAndAddSliceRoot:SingleRootUserQuery");
|
|
response = QMessageBox::warning(activeWindow,
|
|
QStringLiteral("Cannot Create Slice"),
|
|
QString("The slice cannot be created because no single transform root is defined. "
|
|
"Please make sure your slice contains only one root entity.\r\n\r\n"
|
|
"Do you want to create a Transform root entity ?"),
|
|
QMessageBox::Yes | QMessageBox::Cancel);
|
|
}
|
|
|
|
if (response == QMessageBox::Cancel)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("No single root entity."));
|
|
}
|
|
}
|
|
|
|
|
|
// Create a new slice root entity
|
|
AZ::Entity* assetSliceRootEntity = aznew AZ::Entity();
|
|
assetSliceRootEntity->SetName(sliceRootName);
|
|
liveAndAssetAutoGeneratedRoots.second = assetSliceRootEntity->GetId();
|
|
|
|
// Add all required editor components
|
|
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequests::AddRequiredComponents, *assetSliceRootEntity);
|
|
|
|
{
|
|
ScopedUndoBatch undoBatch("Create Slice Root Entity");
|
|
|
|
// Create a "Live" root entity to be owned by the first instance of this slice
|
|
AZ::Entity* liveSliceRootEntity = serializeContext->CloneObject(assetSliceRootEntity);
|
|
liveSliceRootEntity->SetId(AZ::Entity::MakeId());
|
|
liveAndAssetAutoGeneratedRoots.first = liveSliceRootEntity->GetId();
|
|
|
|
// Add new slice root entity to the final asset
|
|
asset.Get()->GetComponent()->AddEntity(assetSliceRootEntity);
|
|
|
|
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&EditorEntityContextRequestBus::Events::AddEditorEntity, liveSliceRootEntity);
|
|
|
|
// Set the rotation and translation of the new parent so that the relative transforms of the selected entities
|
|
// retain their world position when their parent is set.
|
|
// Also move the generated parent to be a child of any pre-existing parent (commonRoot)
|
|
// Use a lambda to batch multiple transform events.
|
|
AZ::TransformBus::Event(liveSliceRootEntity->GetId(), [&sliceRootEntityTranslation, &sliceRootEntityRotation, &commonRoot]
|
|
(AZ::TransformInterface* transformInterface)
|
|
{
|
|
transformInterface->SetParentRelative(commonRoot);
|
|
transformInterface->SetLocalTranslation(sliceRootEntityTranslation);
|
|
transformInterface->SetLocalRotationQuaternion(sliceRootEntityRotation);
|
|
});
|
|
|
|
AzToolsFramework::EntityCreateCommand* createParentCommand = aznew AzToolsFramework::EntityCreateCommand(
|
|
static_cast<AzToolsFramework::UndoSystem::URCommandID>(liveSliceRootEntity->GetId()));
|
|
|
|
createParentCommand->Capture(liveSliceRootEntity);
|
|
createParentCommand->SetParent(undoBatch.GetUndoBatch());
|
|
|
|
// Re root entities so that the new slice root is the parent of all selection root entities
|
|
// and reposition top level entities so that the slice root is at 0,0,0 in the slice
|
|
for (AZ::Entity* selectionRootEntity : selectionRootEntities)
|
|
{
|
|
if (!selectionRootEntity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AzToolsFramework::Components::TransformComponent* transformComponent =
|
|
selectionRootEntity->FindComponent<AzToolsFramework::Components::TransformComponent>();
|
|
|
|
if (!transformComponent)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AZ::Vector3 selectionEntityTranslation = transformComponent->GetLocalTranslation() - sliceRootEntityTranslation;
|
|
|
|
transformComponent->SetLocalTranslation(selectionEntityTranslation);
|
|
transformComponent->SetParent(assetSliceRootEntity->GetId());
|
|
|
|
auto liveEntityFindIt = assetToLiveMap.find(selectionRootEntity->GetId());
|
|
|
|
if (liveEntityFindIt == assetToLiveMap.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AZ::SliceComponent* editorRootSlice;
|
|
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(editorRootSlice,
|
|
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
|
|
|
|
if (!editorRootSlice)
|
|
{
|
|
AZ_Assert(false,
|
|
"SliceUtilities::Internal::CheckAndAddSliceRoot GetEditorRootSlice returned nullptr. Unable to proceed with reparenting entities to generated slice root");
|
|
|
|
// If Assert is passed. Attempt to complete construction of the slice asset which does not require the EditorRootSlice
|
|
continue;
|
|
}
|
|
|
|
AZ::Entity* childEntity = editorRootSlice->FindEntity(liveEntityFindIt->second);
|
|
|
|
AzToolsFramework::EntityStateCommand* reparentCommand = aznew AzToolsFramework::EntityStateCommand(
|
|
static_cast<AzToolsFramework::UndoSystem::URCommandID>(childEntity->GetId()));
|
|
|
|
// Capture Reparent and transform undo state
|
|
reparentCommand->Capture(childEntity, true);
|
|
|
|
// Update the "Live" root's children to match these transform updates
|
|
// Use a lambda to call multiple Requests via one Event
|
|
AZ::TransformBus::Event(childEntity->GetId(), [&selectionEntityTranslation, &liveSliceRootEntity]
|
|
(AZ::TransformInterface* transformInterface)
|
|
{
|
|
transformInterface->SetParentRelative(liveSliceRootEntity->GetId());
|
|
transformInterface->SetLocalTranslation(selectionEntityTranslation);
|
|
});
|
|
|
|
// Capture Reparent and transform redo state
|
|
reparentCommand->Capture(childEntity, false);
|
|
reparentCommand->SetParent(undoBatch.GetUndoBatch());
|
|
}
|
|
} // End Create Slice Root Entity ScopedUndoBatch
|
|
}
|
|
else if (selectionRootEntities.size() == 0)
|
|
{
|
|
return AZ::Failure(AZStd::string::format("Transforms could not be rooted."));
|
|
}
|
|
else if (selectionRootEntities.size() == 1)
|
|
{
|
|
//we have one common root, don't need to do anything
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return AZ::Failure(AZStd::string::format("Could not find common transform root between selected entities."));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
//=========================================================================
|
|
AZ::u32 CountDifferencesVersusSlice(AZ::EntityId entityId, AZ::Entity* compareTo, AZ::SerializeContext& serializeContext, const InstanceDataHierarchy::Address* fieldAddress /*= nullptr*/)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
|
|
|
|
if (!entity || !compareTo)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
const bool isRootEntity = IsRootEntity(*compareTo);
|
|
|
|
InstanceDataHierarchy source;
|
|
source.AddRootInstance<AZ::Entity>(entity);
|
|
source.Build(&serializeContext, AZ::SerializeContext::ENUM_ACCESS_FOR_READ);
|
|
|
|
InstanceDataHierarchy target;
|
|
target.AddRootInstance<AZ::Entity>(const_cast<AZ::Entity*>(compareTo));
|
|
target.Build(&serializeContext, AZ::SerializeContext::ENUM_ACCESS_FOR_READ);
|
|
|
|
AZStd::unordered_set<const InstanceDataNode*> differentNodes;
|
|
|
|
AZStd::function<void(const InstanceDataNode*)> nodeChanged =
|
|
[&differentNodes, isRootEntity, fieldAddress](const InstanceDataNode* node)
|
|
{
|
|
if (node)
|
|
{
|
|
if (fieldAddress)
|
|
{
|
|
const InstanceDataHierarchy::Address nodeAddress = node->ComputeAddress();
|
|
|
|
if (fieldAddress->size() > nodeAddress.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// If nodeAddress ends with the field's address, the filter matches.
|
|
auto nodeIter = nodeAddress.begin();
|
|
for (auto filterIter = fieldAddress->begin(); filterIter != fieldAddress->end(); ++filterIter, ++nodeIter)
|
|
{
|
|
if (*filterIter != *nodeIter)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the node has any un-hidden parent, count it as a difference.
|
|
while (node)
|
|
{
|
|
if (!SliceUtilities::IsNodePushable(*node, isRootEntity))
|
|
{
|
|
break;
|
|
}
|
|
|
|
const AzToolsFramework::NodeDisplayVisibility visibility = CalculateNodeDisplayVisibility(*node, true);
|
|
if (visibility == AzToolsFramework::NodeDisplayVisibility::Visible)
|
|
{
|
|
differentNodes.insert(node);
|
|
break;
|
|
}
|
|
|
|
node = node->GetParent();
|
|
}
|
|
}
|
|
};
|
|
|
|
InstanceDataHierarchy::NewNodeCB newCallback =
|
|
[&nodeChanged](InstanceDataNode* targetNode, AZStd::vector<AZ::u8>& /*data*/)
|
|
{
|
|
nodeChanged(targetNode);
|
|
};
|
|
|
|
InstanceDataHierarchy::RemovedNodeCB removedCallback =
|
|
[&nodeChanged](const InstanceDataNode* sourceNode, InstanceDataNode* /*targetNodeParent*/)
|
|
{
|
|
nodeChanged(sourceNode);
|
|
};
|
|
|
|
InstanceDataHierarchy::ChangedNodeCB changedCallback =
|
|
[&nodeChanged](const InstanceDataNode* sourceNode, InstanceDataNode* /*targetNode*/, AZStd::vector<AZ::u8>& /*sourceData*/, AZStd::vector<AZ::u8>& /*targetData*/)
|
|
{
|
|
nodeChanged(sourceNode);
|
|
};
|
|
|
|
InstanceDataHierarchy::CompareHierarchies(&source, &target,
|
|
InstanceDataHierarchy::DefaultValueComparisonFunction,
|
|
&serializeContext,
|
|
newCallback, removedCallback, changedCallback);
|
|
|
|
return static_cast<AZ::u32>(differentNodes.size());
|
|
}
|
|
|
|
//=========================================================================
|
|
void PopulateQuickPushMenu(QMenu& outerMenu, const AzToolsFramework::EntityIdList& inputEntities, const InstanceDataNode::Address* fieldAddress, const QuickPushMenuOptions& options)
|
|
{
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Error("Slice", false, "Could not retrieve application serialize context.");
|
|
return;
|
|
}
|
|
|
|
size_t numRelevantEntitiesInSlices = 0;
|
|
AZStd::unordered_set<AZ::EntityId> entitiesToAdd;
|
|
AZStd::unordered_set<AZ::EntityId> entitiesToRemove;
|
|
AZStd::unordered_map<int, AZ::u32> numEntitiesToUpdateMapping;
|
|
|
|
for (const AZ::EntityId& entityId : inputEntities)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress(nullptr, nullptr);
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.GetReference())
|
|
{
|
|
++numRelevantEntitiesInSlices;
|
|
}
|
|
}
|
|
|
|
QMenu* quickPushMenu = Internal::GenerateQuickPushMenu(
|
|
&outerMenu,
|
|
numRelevantEntitiesInSlices,
|
|
entitiesToAdd,
|
|
entitiesToRemove,
|
|
numEntitiesToUpdateMapping,
|
|
inputEntities,
|
|
fieldAddress,
|
|
options);
|
|
|
|
AZStd::string headerText = options.m_headerText;
|
|
const QString headerTextTranslated = QObject::tr(headerText.c_str());
|
|
const QString headerTextTranslatedAdvanced = QObject::tr(headerText.c_str()) + QObject::tr(" (Advanced)...");
|
|
|
|
// Setup slice push options - quickpush submenu if there are available quick pushes, otherwise single Advanced option
|
|
QAction* pushAdvancedAction = nullptr;
|
|
if (quickPushMenu)
|
|
{
|
|
QString saveSliceOptionText;
|
|
if (numRelevantEntitiesInSlices == 1)
|
|
{
|
|
saveSliceOptionText = QObject::tr(headerText.c_str());
|
|
}
|
|
else
|
|
{
|
|
saveSliceOptionText = QObject::tr("%1 for %2 entities").arg(headerTextTranslated).arg(numRelevantEntitiesInSlices);
|
|
}
|
|
quickPushMenu->setTitle(saveSliceOptionText);
|
|
outerMenu.addMenu(quickPushMenu);
|
|
|
|
// "Advanced" push option, which displays the modal push UI.
|
|
quickPushMenu->addSeparator();
|
|
pushAdvancedAction = quickPushMenu->addAction(headerTextTranslatedAdvanced);
|
|
}
|
|
else
|
|
{
|
|
pushAdvancedAction = outerMenu.addAction(headerTextTranslatedAdvanced);
|
|
}
|
|
pushAdvancedAction->setToolTip(QObject::tr("Allows selection of individual overrides, as well as the target slice asset to which each override is saved."));
|
|
|
|
QObject::connect(pushAdvancedAction, &QAction::triggered, [inputEntities]
|
|
{
|
|
QWidget* mainWindow = nullptr;
|
|
EditorRequests::Bus::BroadcastResult(mainWindow, &EditorRequests::Bus::Events::GetMainWindow);
|
|
AzToolsFramework::SliceUtilities::PushEntitiesModal(mainWindow, inputEntities, nullptr);
|
|
});
|
|
}
|
|
|
|
QMenu* GenerateQuickPushMenu(
|
|
QWidget* parent,
|
|
size_t& numRelevantEntitiesInSlices,
|
|
AZStd::unordered_set<AZ::EntityId>& entitiesToAdd,
|
|
AZStd::unordered_set<AZ::EntityId>& entitiesToRemove,
|
|
AZStd::unordered_map<int, AZ::u32>& numEntitiesToUpdateMapping,
|
|
const AzToolsFramework::EntityIdList& inputEntities,
|
|
const InstanceDataNode::Address* fieldAddress,
|
|
const QuickPushMenuOptions& options)
|
|
{
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Error("Slice", false, "Could not retrieve application serialize context.");
|
|
return nullptr;
|
|
}
|
|
|
|
QPixmap sliceItemIcon(GetSliceItemIconPath());
|
|
QIcon sliceChangedItemIcon(GetSliceItemChangedIconPath());
|
|
QPixmap lShapeIcon(GetLShapeIconPath());
|
|
|
|
AZStd::unordered_map<AZ::Data::AssetId, AZStd::vector<EntityAncestorPair>> assetEntityAncestorMap;
|
|
AZStd::vector<AZ::Data::AssetId> sliceDisplayOrder;
|
|
AZStd::unordered_map<AZ::Data::AssetId, int> numPushableChangesPerAsset;
|
|
AZStd::unordered_map<AZ::Data::AssetId, EntityIdSet> pushableEntityIdsPerAsset;
|
|
AZStd::unordered_map<AZ::Data::AssetId, EntityIdSet> unpushableEntityIdsPerAsset;
|
|
bool pushableChangesAvailable = CountPushableChangesToSlice(inputEntities,
|
|
fieldAddress,
|
|
entitiesToAdd,
|
|
entitiesToRemove,
|
|
numRelevantEntitiesInSlices,
|
|
numPushableChangesPerAsset,
|
|
sliceDisplayOrder,
|
|
assetEntityAncestorMap,
|
|
unpushableEntityIdsPerAsset);
|
|
|
|
AZ::Data::AssetManager& assetManager = AZ::Data::AssetManager::Instance();
|
|
AZStd::string sliceAssetPath, sliceAssetName;
|
|
|
|
// # of pixels of indentation for each slice level when multiple quick push options are available.
|
|
static const AZ::u32 kPixelIndentationPerLevel = GetSliceHierarchyMenuIdentationPerLevel();
|
|
|
|
// Any asset that acts as a valid target for all selected slice-instance-owned entities can be shown.
|
|
QMenu* quickPushMenu = quickPushMenu = new QMenu(parent);
|
|
|
|
bool setupMenu = false;
|
|
if (pushableChangesAvailable)
|
|
{
|
|
AZ::u32 indentation = 0;
|
|
// Loop through all potential target slice assets for the selected entity set.
|
|
for (const AZ::Data::AssetId& sliceAssetId : sliceDisplayOrder)
|
|
{
|
|
// Skip if the asset is not a valid target for all selected entities.
|
|
AZStd::vector<EntityAncestorPair>& entityAncestors = assetEntityAncestorMap[sliceAssetId];
|
|
if (entityAncestors.size() != numRelevantEntitiesInSlices)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AZ::Data::Asset<AZ::SliceAsset> sliceAsset = assetManager.FindOrCreateAsset<AZ::SliceAsset>(sliceAssetId, AZ::Data::AssetLoadBehavior::Default);
|
|
if (!sliceAsset)
|
|
{
|
|
AZ_Warning("Slice", false, "Failed to retrieve slice asset with id %s", sliceAssetId.ToString<AZStd::string>().c_str());
|
|
continue;
|
|
}
|
|
|
|
sliceAssetName.clear();
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(sliceAssetPath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, sliceAssetId);
|
|
AzFramework::StringFunc::Path::GetFullFileName(sliceAssetPath.c_str(), sliceAssetName);
|
|
if (sliceAssetName.empty())
|
|
{
|
|
AZ_Warning("Slice", false, "Failed to determine path/name for slice with id %s", sliceAssetId.ToString<AZStd::string>().c_str());
|
|
continue;
|
|
}
|
|
|
|
// Indent each slice level.
|
|
QString sliceText = sliceAssetName.c_str();
|
|
|
|
// Skip if multiple selected entities would be targeting the same ancestor within this asset.
|
|
bool targetConflict = false;
|
|
AZStd::unordered_set<AZ::EntityId> targetEntities;
|
|
for (const EntityAncestorPair& entityAncestor : entityAncestors)
|
|
{
|
|
auto iterPairBool = targetEntities.insert(entityAncestor.second->GetId());
|
|
if (!iterPairBool.second)
|
|
{
|
|
targetConflict = true;
|
|
sliceText.append(" (conflict)");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Limit the number of entities for which we're willing to compute differences against target
|
|
// slices, as doing so with large selections could induce significant context menu lag.
|
|
// If we exceed the limit, we simply don't show a preview of # of differences.
|
|
AZ::u32 totalDifferences = 0;
|
|
|
|
if (numPushableChangesPerAsset.find(sliceAssetId) != numPushableChangesPerAsset.end())
|
|
{
|
|
totalDifferences += numPushableChangesPerAsset[sliceAssetId];
|
|
}
|
|
|
|
|
|
AZ::u32 numUnpushableSliceEntityAdditions = 0;
|
|
|
|
if (unpushableEntityIdsPerAsset.find(sliceAssetId) != unpushableEntityIdsPerAsset.end())
|
|
{
|
|
numUnpushableSliceEntityAdditions += static_cast<AZ::u32>(unpushableEntityIdsPerAsset[sliceAssetId].size());
|
|
}
|
|
|
|
// Each quick push option UI is a collection of up to four separate widgets:
|
|
// [indentation by depth] [icon] [slice name] [# overrides]
|
|
QWidgetAction* widgetAction = new QWidgetAction(quickPushMenu);
|
|
QWidget* widget = new QWidget();
|
|
widget->setObjectName("SliceHierarchyMenuItem");
|
|
// Add class to fix hover state styling for WidgetAction
|
|
AzQtComponents::Style::addClass(widget, "WidgetAction");
|
|
QHBoxLayout* quickPushRowLayout = new QHBoxLayout();
|
|
widget->setLayout(quickPushRowLayout);
|
|
|
|
int LShapeIconWidgetWidth = quickPushRowLayout->contentsMargins().left() + GetLShapeIconSize().width() + quickPushRowLayout->contentsMargins().right();
|
|
|
|
if (indentation > 0)
|
|
{
|
|
QLabel* indentLabel = new QLabel(quickPushMenu);
|
|
indentLabel->setFixedSize(kPixelIndentationPerLevel + (indentation - 1) * LShapeIconWidgetWidth, GetSliceItemHeight());
|
|
quickPushRowLayout->addWidget(indentLabel);
|
|
|
|
// Add the L shape icon to show the slice hierarchy
|
|
QLabel* lShapeIconLabel = new QLabel(quickPushMenu);
|
|
lShapeIconLabel->setPixmap(lShapeIcon);
|
|
lShapeIconLabel->setFixedSize(GetLShapeIconSize());
|
|
quickPushRowLayout->addWidget(lShapeIconLabel);
|
|
}
|
|
|
|
QLabel* iconLabel = new QLabel(quickPushMenu);
|
|
iconLabel->setPixmap(sliceChangedItemIcon.pixmap(GetSliceItemIconSize()));
|
|
|
|
iconLabel->setFixedSize(GetSliceItemIconSize());
|
|
quickPushRowLayout->addWidget(iconLabel);
|
|
|
|
QLabel* sliceLabel = new QLabel(sliceText, quickPushMenu);
|
|
|
|
int minimumSliceLabelWidth = GetSliceItemDefaultWidth();
|
|
if (indentation == 0)
|
|
{
|
|
minimumSliceLabelWidth -= quickPushRowLayout->contentsMargins().left();
|
|
}
|
|
else
|
|
{
|
|
minimumSliceLabelWidth = minimumSliceLabelWidth - indentation * LShapeIconWidgetWidth - kPixelIndentationPerLevel;
|
|
}
|
|
|
|
sliceLabel->setMinimumWidth(minimumSliceLabelWidth);
|
|
|
|
quickPushRowLayout->addWidget(sliceLabel);
|
|
|
|
QString overridesText = "";
|
|
// Show preview of # of overrides if relevant/available.
|
|
if (entitiesToAdd.size() > 0)
|
|
{
|
|
overridesText += QString("<i>%1 added</i>").arg(static_cast<int>(entitiesToAdd.size()));
|
|
}
|
|
if (entitiesToRemove.size() > 0)
|
|
{
|
|
overridesText = overridesText != "" ? (overridesText + QString("<font color=\"%1\"> | </font>").arg(splitterColor)) : overridesText;
|
|
overridesText += QString("<i>%1 removed</i>").arg(static_cast<int>(entitiesToRemove.size()));
|
|
}
|
|
if (totalDifferences == 1 && options.m_singleOverrideDisplayOption == QuickPushMenuOverrideDisplayCount::ShowOverrideCountWhenSingle)
|
|
{
|
|
overridesText = overridesText != "" ? (overridesText + QString("<font color=\"%1\"> | </font>").arg(splitterColor)) : overridesText;
|
|
overridesText += QString("<i>%1 updated</i>").arg(totalDifferences);
|
|
}
|
|
if (totalDifferences > 1)
|
|
{
|
|
overridesText = overridesText != "" ? (overridesText + QString("<font color=\"%1\"> | </font>").arg(splitterColor)) : overridesText;
|
|
overridesText += QString("<i>%1 updated</i>").arg(totalDifferences);
|
|
}
|
|
if (numUnpushableSliceEntityAdditions > 0)
|
|
{
|
|
overridesText = overridesText != "" ? (overridesText + QString("<font color=\"%1\"> | </font>").arg(splitterColor)) : overridesText;
|
|
overridesText += QString("<font color=\"%1\"><i>%2 unsavable</i></font>").arg(unsavableChangesTextColor).arg(numUnpushableSliceEntityAdditions);
|
|
}
|
|
|
|
if (overridesText != "")
|
|
{
|
|
QLabel* overridesLabel = new QLabel(overridesText, quickPushMenu);
|
|
// Rich-text QLabel will block mouse events if this attribute is set to be false
|
|
overridesLabel->setAttribute(Qt::WA_TransparentForMouseEvents, true);
|
|
overridesLabel->setObjectName("NumOverrides");
|
|
quickPushRowLayout->addWidget(overridesLabel);
|
|
}
|
|
|
|
widgetAction->setDefaultWidget(widget);
|
|
|
|
if (targetConflict)
|
|
{
|
|
// The push option is disabled in the case of a conflict, but with a tooltip explaining why.
|
|
widget->setToolTip(QString("The selection contains more than one entity with overrides affecting the same entity in the target slice (%1). Adjust your selection and try again.")
|
|
.arg(sliceAssetPath.c_str()));
|
|
widget->setEnabled(false);
|
|
}
|
|
else if (totalDifferences == 0 && entitiesToAdd.size() == 0 && entitiesToRemove.size() == 0)
|
|
{
|
|
widget->setToolTip(QString("There are no pushable differences versus this slice."));
|
|
widget->setEnabled(false);
|
|
}
|
|
else
|
|
{
|
|
widget->setToolTip(QString("Save overrides to: %1").arg(sliceAssetPath.c_str()));
|
|
}
|
|
|
|
quickPushMenu->addAction(widgetAction);
|
|
setupMenu = true;
|
|
|
|
++indentation;
|
|
|
|
if (!targetConflict && (totalDifferences > 0 || entitiesToAdd.size() > 0 || entitiesToRemove.size() > 0))
|
|
{
|
|
InstanceDataHierarchy::Address pushFieldAddress;
|
|
if (fieldAddress)
|
|
{
|
|
pushFieldAddress = *fieldAddress;
|
|
}
|
|
|
|
// prune the entity addition list, removing entities we can't push
|
|
EntityIdSet unpushableIds;
|
|
if (unpushableEntityIdsPerAsset.find(sliceAsset.GetId()) != unpushableEntityIdsPerAsset.end())
|
|
{
|
|
unpushableIds = unpushableEntityIdsPerAsset[sliceAsset.GetId()];
|
|
}
|
|
AZStd::unordered_set<AZ::EntityId> pushableEntitiesToAdd;
|
|
for (AZ::EntityId id : entitiesToAdd)
|
|
{
|
|
if (unpushableIds.find(id) == unpushableIds.end())
|
|
{
|
|
pushableEntitiesToAdd.insert(id);
|
|
}
|
|
}
|
|
|
|
|
|
QObject::connect(widgetAction, &QAction::triggered,
|
|
[sliceAsset, entityAncestors, pushableEntitiesToAdd, entitiesToRemove, pushFieldAddress, inputEntities, numUnpushableSliceEntityAdditions, unpushableIds]
|
|
{
|
|
Internal::QuickPushToSlice(sliceAsset, entityAncestors, pushableEntitiesToAdd, entitiesToRemove, pushFieldAddress, inputEntities, unpushableIds);
|
|
|
|
bool showCircularDependencyError = true;
|
|
AzToolsFramework::EditorRequests::Bus::BroadcastResult(showCircularDependencyError, &AzToolsFramework::EditorRequests::GetShowCircularDependencyError);
|
|
|
|
if (numUnpushableSliceEntityAdditions > 0 && showCircularDependencyError)
|
|
{
|
|
QWidget* mainWindow = nullptr;
|
|
EditorRequests::Bus::BroadcastResult(mainWindow, &EditorRequests::Bus::Events::GetMainWindow);
|
|
|
|
QMessageBox* messageBox = new QMessageBox(QMessageBox::NoIcon,
|
|
QObject::tr("Potential circular dependency detected"),
|
|
QObject::tr("Potential invalid additions detected. These are unsaveable because slices cannot contain instances of themselves. "
|
|
"Saving these additions could potentially create a cyclic asset dependency, causing infinite recursion. Please push "
|
|
"individual additions of slice-owned entities separately. All other valid overrides have been saved."),
|
|
QMessageBox::NoButton,
|
|
mainWindow);
|
|
|
|
QCheckBox *displayOption = new QCheckBox(QObject::tr("Do not show again"), messageBox);
|
|
QObject::connect(displayOption, &QCheckBox::stateChanged, [](int state) {
|
|
AzToolsFramework::EditorRequests::Bus::Broadcast(&AzToolsFramework::EditorRequests::SetShowCircularDependencyError, state == Qt::Unchecked);
|
|
});
|
|
|
|
messageBox->setCheckBox(displayOption);
|
|
messageBox->exec();
|
|
}
|
|
});
|
|
}
|
|
|
|
numEntitiesToUpdateMapping[quickPushMenu->actions().size() - 1] = totalDifferences;
|
|
|
|
} // for each unique target asset
|
|
}
|
|
|
|
|
|
if (!setupMenu)
|
|
{
|
|
// Add a menu item to let the user know that a quick save is not available.
|
|
// This is built in the same way as the quick push rows, to make it look similar.
|
|
// See comments on the quick push row on why quickPushMenu->addAction(icon, actionLabel) is not used here.
|
|
QWidgetAction* widgetAction = new QWidgetAction(quickPushMenu);
|
|
QWidget* widget = new QWidget(quickPushMenu);
|
|
widget->setObjectName("SliceHierarchyMenuItem");
|
|
// Add class to fix hover state styling for WidgetAction
|
|
AzQtComponents::Style::addClass(widget, "WidgetAction");
|
|
QHBoxLayout* quickPushRowLayout = new QHBoxLayout(widget);
|
|
widget->setLayout(quickPushRowLayout);
|
|
|
|
QLabel* iconLabel = new QLabel(quickPushMenu);
|
|
QPixmap noSaveableChangesIcon(Internal::GetNoSaveableChangesIconPath());
|
|
iconLabel->setPixmap(noSaveableChangesIcon);
|
|
// Use the same size for the no saveable changes icon as the slice icon.
|
|
iconLabel->setFixedSize(GetSliceItemIconSize());
|
|
quickPushRowLayout->addWidget(iconLabel);
|
|
|
|
QLabel* sliceLabel = new QLabel("Quick save is not available", quickPushMenu);
|
|
sliceLabel->setMinimumWidth(GetSliceItemDefaultWidth());
|
|
quickPushRowLayout->addWidget(sliceLabel);
|
|
widget->setEnabled(false);
|
|
widgetAction->setDefaultWidget(widget);
|
|
quickPushMenu->addAction(widgetAction);
|
|
|
|
}
|
|
return quickPushMenu;
|
|
}
|
|
|
|
InvalidSliceReferencesWarningResult CheckForInvalidSliceReferences(QWidget* parent, AZ::Data::Asset<AZ::SliceAsset> sliceAsset)
|
|
{
|
|
AZ::SliceComponent* assetComponent = sliceAsset.Get()->GetComponent();
|
|
if (assetComponent == nullptr)
|
|
{
|
|
// The sliceComponent couldn't be found, let the later quick slice push logic handle this.
|
|
return InvalidSliceReferencesWarningResult::Save;
|
|
}
|
|
|
|
// 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.
|
|
return DisplayInvalidSliceReferencesWarning(parent,
|
|
/*invalidSliceCount*/ 1,
|
|
invalidSlices.size(),
|
|
/*showDetailsButton*/ true);
|
|
}
|
|
|
|
// If there were no invalid slices, then continue with the save.
|
|
return InvalidSliceReferencesWarningResult::Save;
|
|
}
|
|
|
|
void QuickPushToSlice(
|
|
const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset,
|
|
const AZStd::vector<EntityAncestorPair>& entityAncestors,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToAdd,
|
|
const AZStd::unordered_set<AZ::EntityId>& entitiesToRemove,
|
|
const InstanceDataHierarchy::Address& pushFieldAddress,
|
|
const AzToolsFramework::EntityIdList& inputEntities,
|
|
const EntityIdSet unpushableIds)
|
|
{
|
|
// Calculate entity Id set.
|
|
AZStd::unordered_set<AZ::EntityId> pushEntities;
|
|
pushEntities.reserve(entityAncestors.size());
|
|
for (const EntityAncestorPair& entityAncestor : entityAncestors)
|
|
{
|
|
pushEntities.insert(entityAncestor.first);
|
|
}
|
|
|
|
QWidget* mainWindow = nullptr;
|
|
EditorRequests::Bus::BroadcastResult(mainWindow, &EditorRequests::Bus::Events::GetMainWindow);
|
|
|
|
InvalidSliceReferencesWarningResult invalidSliceCheckResult =
|
|
Internal::CheckForInvalidSliceReferences(mainWindow, sliceAsset);
|
|
|
|
for (const AZ::EntityId& id : inputEntities)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Handler::FindEntity, id);
|
|
|
|
if (!entity)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
AzToolsFramework::Components::EditorEntitySortComponent* liveSortOrderComponent = entity->FindComponent<AzToolsFramework::Components::EditorEntitySortComponent>();
|
|
|
|
if (liveSortOrderComponent)
|
|
{
|
|
AzToolsFramework::EntityOrderArray orderArray = liveSortOrderComponent->GetChildEntityOrderArray();
|
|
AzToolsFramework::EntityOrderArray prunedOrderArray;
|
|
prunedOrderArray.reserve(orderArray.size());
|
|
|
|
WillPushEntityCallback willPushEntityCallback =
|
|
[unpushableIds]
|
|
(const AZ::EntityId entityId, const SliceAssetPtr& /*assetToPushTo*/) -> bool
|
|
{
|
|
return unpushableIds.find(entityId) == unpushableIds.end();
|
|
};
|
|
|
|
RemoveInvalidChildOrderArrayEntries(orderArray, prunedOrderArray, sliceAsset, willPushEntityCallback);
|
|
|
|
liveSortOrderComponent->SetChildEntityOrderArray(prunedOrderArray);
|
|
}
|
|
}
|
|
|
|
switch (invalidSliceCheckResult)
|
|
{
|
|
case InvalidSliceReferencesWarningResult::Details:
|
|
{
|
|
AzToolsFramework::SliceUtilities::PushEntitiesModal(mainWindow, inputEntities, nullptr);
|
|
}
|
|
break;
|
|
case InvalidSliceReferencesWarningResult::Save:
|
|
{
|
|
// Push all entities to the target slice.
|
|
SliceTransaction::Result outcome = AZ::Success();
|
|
if (pushFieldAddress.empty())
|
|
{
|
|
outcome = SliceUtilities::PushEntitiesIncludingAdditionAndSubtractionBackToSlice(
|
|
sliceAsset,
|
|
pushEntities,
|
|
entitiesToAdd,
|
|
entitiesToRemove,
|
|
SliceUtilities::SlicePreSaveCallbackForWorldEntities,
|
|
SliceUtilities::SlicePostPushCallback);
|
|
}
|
|
else
|
|
{
|
|
outcome = SliceUtilities::PushEntityFieldBackToSlice(*pushEntities.begin(), sliceAsset, pushFieldAddress, SliceUtilities::SlicePreSaveCallbackForWorldEntities);
|
|
}
|
|
|
|
if (!outcome)
|
|
{
|
|
mainWindow = nullptr;
|
|
EditorRequests::Bus::BroadcastResult(mainWindow, &EditorRequests::Bus::Events::GetMainWindow);
|
|
|
|
QMessageBox::critical(
|
|
mainWindow,
|
|
QObject::tr("Slice Push Failed"),
|
|
outcome.GetError().c_str());
|
|
}
|
|
}
|
|
break;
|
|
case InvalidSliceReferencesWarningResult::Cancel:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void FlattenAncestry(const AZ::SliceComponent::Ancestor& toFlatten, const AZ::SliceComponent::Ancestor& toFlattenImmediateAncestor)
|
|
{
|
|
if (!toFlatten.m_sliceAddress.IsValid() || !toFlattenImmediateAncestor.m_sliceAddress.IsValid())
|
|
{
|
|
AZ_Warning("Slice", false, "Failed to flatten slice ancestry, slice ancestry invalid");
|
|
return;
|
|
}
|
|
|
|
AZ::SerializeContext* serializeContext = nullptr;
|
|
AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
|
|
|
|
if (!serializeContext)
|
|
{
|
|
AZ_Error("Slice", false, "Could not retrieve application serialize context.");
|
|
return;
|
|
}
|
|
|
|
// We get the reference and asset of what's being flattened to generate a clone of its slice component
|
|
const AZ::SliceComponent::SliceReference* toFlattenReference = toFlatten.m_sliceAddress.GetReference();
|
|
const AZ::Data::Asset<AZ::SliceAsset> toFlattenAsset = toFlattenReference->GetSliceAsset();
|
|
|
|
// We get the asset of our immediate ancestor being flattened into us to help locate the cloned ancestors root
|
|
const AZ::SliceComponent::SliceReference* ancestorReference = toFlattenImmediateAncestor.m_sliceAddress.GetReference();
|
|
const AZ::Data::Asset<AZ::SliceAsset> ancestorAsset = ancestorReference->GetSliceAsset();
|
|
|
|
// Clone the component so we can safely manipulate it
|
|
AZ::SliceComponent::SliceInstanceToSliceInstanceMap cloneMap;
|
|
AZ::SliceComponent* toFlattenComponentClone = toFlattenAsset.Get()->GetComponent()->Clone(*serializeContext, &cloneMap);
|
|
|
|
if (!toFlattenComponentClone)
|
|
{
|
|
AZ_Error("Slice", false, "Failed to clone Slice Component during FlattenAncestry");
|
|
return;
|
|
}
|
|
|
|
// Confirm our component is instantiated so we operate on the slice post data patching
|
|
toFlattenComponentClone->Instantiate();
|
|
|
|
// Get the cloned address of the ancestor we are flattening into ourselves
|
|
auto findIt = cloneMap.find(toFlattenImmediateAncestor.m_sliceAddress);
|
|
if (findIt == cloneMap.end() || !findIt->second.IsValid())
|
|
{
|
|
AZ_Error("Slice", false, "Failed to recover instance address from Slice Component clone in FlattenSlice");
|
|
return;
|
|
}
|
|
AZ::SliceComponent::SliceInstanceAddress ancestorAddressClone = findIt->second;
|
|
|
|
// Get the root entity of what we're flattening into ourselves
|
|
AZ::EntityId ancestorRootEntityClone;
|
|
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(ancestorRootEntityClone, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetRootEntityIdOfSliceInstance, ancestorAddressClone);
|
|
|
|
// Flatten all ancestry of toFlatten into toFlatten breaking its ancestral dependencies
|
|
bool flattenSuccess = toFlattenComponentClone->FlattenSlice(ancestorAddressClone.GetReference(), ancestorRootEntityClone);
|
|
|
|
// Flattening failed, cannot complete operation, no need for error message as FlattenSlice covers that
|
|
if (!flattenSuccess)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Commit the flattened clone into the actual slice asset
|
|
SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginSliceOverwrite(toFlattenAsset, *toFlattenComponentClone, serializeContext);
|
|
|
|
if (!transaction)
|
|
{
|
|
AZ_Error("Slice", false, "Failed to create a transaction for FlattenAncestry");
|
|
return;
|
|
}
|
|
|
|
transaction->Commit(toFlattenAsset.GetId(), nullptr, nullptr, SliceTransaction::SliceCommitFlags::DisableUndoCapture);
|
|
|
|
|
|
// Flush undo for this operation because we cannot undo it and going back further is undefined
|
|
AzToolsFramework::ToolsApplicationRequestBus::Broadcast(&AzToolsFramework::ToolsApplicationRequestBus::Events::FlushUndo);
|
|
}
|
|
|
|
bool CalculatePushableChangesPerAsset(
|
|
AZStd::unordered_map<AZ::Data::AssetId, int>& pushableChangesPerAsset,
|
|
AZ::SerializeContext& serializeContext,
|
|
const AZStd::vector<AZ::Data::AssetId>& sliceDisplayOrder,
|
|
const AZStd::unordered_map<AZ::Data::AssetId, AZStd::vector<EntityAncestorPair>>& assetEntityAncestorMap,
|
|
const size_t& numRelevantEntitiesInSlices,
|
|
const InstanceDataNode::Address* fieldAddress)
|
|
{
|
|
|
|
const AZ::u32 kMaxEntitiesForOverrideCalculation = 5; // Max # of entities for which we'll do a full hierarchy comparison (to preview # of overrides).
|
|
|
|
// Track and return if any pushable changes are available. The quick push menu
|
|
// will use this result to check if it should display a message telling the user no quick saves area available.
|
|
bool pushableChangesAvailable = false;
|
|
for (const AZ::Data::AssetId& sliceAssetId : sliceDisplayOrder)
|
|
{
|
|
AZStd::unordered_map<AZ::Data::AssetId, AZStd::vector<EntityAncestorPair>>::const_iterator ancestorIter =
|
|
assetEntityAncestorMap.find(sliceAssetId);
|
|
if (ancestorIter == assetEntityAncestorMap.end())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const AZStd::vector<EntityAncestorPair>& entityAncestors = ancestorIter->second;
|
|
if (entityAncestors.size() != numRelevantEntitiesInSlices)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
pushableChangesPerAsset[sliceAssetId] = 0;
|
|
|
|
if (numRelevantEntitiesInSlices <= kMaxEntitiesForOverrideCalculation)
|
|
{
|
|
for (const EntityAncestorPair& entityAncestor : entityAncestors)
|
|
{
|
|
pushableChangesPerAsset[sliceAssetId] += Internal::CountDifferencesVersusSlice(
|
|
entityAncestor.first,
|
|
entityAncestor.second.get(),
|
|
serializeContext,
|
|
fieldAddress);
|
|
}
|
|
}
|
|
if (!pushableChangesAvailable && pushableChangesPerAsset[sliceAssetId] > 0)
|
|
{
|
|
pushableChangesAvailable = true;
|
|
}
|
|
}
|
|
return pushableChangesAvailable;
|
|
}
|
|
|
|
void FinalizeSubsliceClone(AZ::SliceComponent::SliceInstanceAddress& clonedSubslice)
|
|
{
|
|
AZ::SliceComponent::SliceInstance* subsliceInstanceClone = clonedSubslice.GetInstance();
|
|
|
|
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
|
|
&AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded,
|
|
subsliceInstanceClone->GetInstantiated()->m_entities);
|
|
|
|
ScopedUndoBatch cloneSubSliceInstanceUndoBatch("Clone Sub-Slice Instance");
|
|
|
|
for (AZ::Entity* clonedEntity : subsliceInstanceClone->GetInstantiated()->m_entities)
|
|
{
|
|
// Don't mark entities as dirty for PropertyChange undo action because they are just created and will be captured below for undo as new-creations.
|
|
AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(&ToolsApplicationRequests::RemoveDirtyEntity, clonedEntity->GetId());
|
|
|
|
AzToolsFramework::EntityCreateCommand* command = aznew AzToolsFramework::EntityCreateCommand(
|
|
static_cast<AzToolsFramework::UndoSystem::URCommandID>(clonedEntity->GetId()));
|
|
command->Capture(clonedEntity);
|
|
command->SetParent(cloneSubSliceInstanceUndoBatch.GetUndoBatch());
|
|
}
|
|
|
|
AZ::EntityId rootEntityIdOfClone;
|
|
ToolsApplicationRequestBus::BroadcastResult(
|
|
rootEntityIdOfClone,
|
|
&ToolsApplicationRequestBus::Events::GetRootEntityIdOfSliceInstance,
|
|
clonedSubslice);
|
|
|
|
EntityIdList selection = { rootEntityIdOfClone };
|
|
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, selection);
|
|
|
|
// Create selection undo command
|
|
AzToolsFramework::SelectionCommand* selCommand = aznew AzToolsFramework::SelectionCommand(selection, "Select slice root entity");
|
|
selCommand->SetParent(cloneSubSliceInstanceUndoBatch.GetUndoBatch());
|
|
}
|
|
|
|
void addDetachSliceEntityAction(QMenu* detachMenu, const AzToolsFramework::EntityIdSet& selectedTransformHierarchyEntities)
|
|
{
|
|
// Detach entities action currently acts on entities and all descendants, so include those as part of the selection
|
|
AzToolsFramework::EntityIdList selectedDetachEntities(selectedTransformHierarchyEntities.begin(), selectedTransformHierarchyEntities.end());
|
|
|
|
// A selection in Open 3D Engine is usually singular, but a selection can have more than one entity.
|
|
// No Open 3D Engine systems support multiple selections, or multiple different groups of selected entities.
|
|
QString detachEntitiesActionText(QObject::tr("Selection"));
|
|
QString detachEntitiesTooltipText;
|
|
if (selectedDetachEntities.size() == 1)
|
|
{
|
|
detachEntitiesTooltipText = QObject::tr("Detach the selected entity and its children from all slice references");
|
|
}
|
|
else
|
|
{
|
|
detachEntitiesTooltipText = QObject::tr("Detach the selected entities and their children from all slice references");
|
|
}
|
|
|
|
QAction* detachAction = detachMenu->addAction(detachEntitiesActionText);
|
|
detachAction->setToolTip(detachEntitiesTooltipText);
|
|
QObject::connect(detachAction, &QAction::triggered, [selectedDetachEntities] {
|
|
if (!selectedDetachEntities.empty())
|
|
{
|
|
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Broadcast(
|
|
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequests::DetachSliceEntities, selectedDetachEntities);
|
|
}});
|
|
}
|
|
|
|
void addDetachSliceInstanceAction(QMenu* detachMenu, const AzToolsFramework::EntityIdList& selectedEntities, const AZ::SliceComponent::SliceInstanceAddressSet& sliceInstances)
|
|
{
|
|
QString detachSlicesActionText;
|
|
QString detachSlicesTooltipText;
|
|
if (sliceInstances.size() == 1)
|
|
{
|
|
detachSlicesActionText = QObject::tr("Instance");
|
|
detachSlicesTooltipText = QObject::tr("Detach the selected entity and its hierarchy from all slice references");
|
|
}
|
|
else
|
|
{
|
|
detachSlicesActionText = QObject::tr("Instances");
|
|
detachSlicesTooltipText = QObject::tr("Detach the selected entities and their hierarchy from all slice references");
|
|
}
|
|
QAction* detachAllAction = detachMenu->addAction(detachSlicesActionText);
|
|
detachAllAction->setToolTip(detachSlicesTooltipText);
|
|
QObject::connect(detachAllAction, &QAction::triggered, [selectedEntities, sliceInstances] {
|
|
if (!selectedEntities.empty())
|
|
{
|
|
AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Broadcast(
|
|
&AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::DetachSliceInstances, sliceInstances);
|
|
}
|
|
});
|
|
}
|
|
|
|
void GetEntitiesInSlices(const AzToolsFramework::EntityIdList& selectedEntities, AZ::u32& entitiesInSlices, AZ::SliceComponent::SliceInstanceAddressSet& sliceInstances)
|
|
{
|
|
// Identify all slice instances affected by the selected entity set.
|
|
entitiesInSlices = 0;
|
|
for (const AZ::EntityId& entityId : selectedEntities)
|
|
{
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.IsValid())
|
|
{
|
|
sliceInstances.insert(sliceAddress);
|
|
++entitiesInSlices;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ResaveSlice(AZStd::shared_ptr<AZ::Entity> sliceEntity, const AZStd::string& fullFilePath)
|
|
{
|
|
AZStd::string tmpFileName;
|
|
bool tmpFilesaved = false;
|
|
// here we are saving the slice to a temp file instead of the original file and then copying the temp file to the original file.
|
|
// This ensures that AP will not a get a file change notification on an incomplete slice file causing it to fail processing. Temp files are ignored by AP.
|
|
if (AZ::IO::CreateTempFileName(fullFilePath.c_str(), tmpFileName))
|
|
{
|
|
AZ::IO::FileIOStream fileStream(tmpFileName.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeBinary);
|
|
|
|
if (fileStream.IsOpen())
|
|
{
|
|
tmpFilesaved = AZ::Utils::SaveObjectToStream<AZ::Entity>(fileStream, GetSliceStreamFormat(), sliceEntity.get());
|
|
}
|
|
|
|
using SCCommandBus = AzToolsFramework::SourceControlCommandBus;
|
|
SCCommandBus::Broadcast(&SCCommandBus::Events::RequestEdit, fullFilePath.c_str(), true,
|
|
[sliceEntity, fullFilePath, tmpFileName, tmpFilesaved](bool /*success*/, const AzToolsFramework::SourceControlFileInfo& info)
|
|
{
|
|
if (!info.IsReadOnly())
|
|
{
|
|
if (tmpFilesaved && AZ::IO::SmartMove(tmpFileName.c_str(), fullFilePath.c_str()))
|
|
{
|
|
// Bump the slice asset up in the asset processor's queue.
|
|
AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::EscalateAssetBySearchTerm, fullFilePath.c_str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QWidget* mainWindow = nullptr;
|
|
AzToolsFramework::EditorRequests::Bus::BroadcastResult(mainWindow, &AzToolsFramework::EditorRequests::GetMainWindow);
|
|
QMessageBox::warning(mainWindow, QObject::tr("Unable to Modify Slice"),
|
|
QObject::tr("File is not writable."), QMessageBox::Ok, QMessageBox::Ok);
|
|
}
|
|
});
|
|
}
|
|
else
|
|
{
|
|
QWidget* mainWindow = nullptr;
|
|
AzToolsFramework::EditorRequests::Bus::BroadcastResult(mainWindow, &AzToolsFramework::EditorRequests::GetMainWindow);
|
|
QMessageBox::warning(mainWindow, QObject::tr("Unable to Modify Slice"),
|
|
QObject::tr("Unable to Modify Slice (%1). Cannot create a temporary file for writing data in the same folder.").arg(fullFilePath.c_str()),
|
|
QMessageBox::Ok, QMessageBox::Ok);
|
|
}
|
|
}
|
|
|
|
void AnalyseAncestoryForPushableEntities(const AZ::EntityId& entityId,
|
|
AZ::SliceComponent::SliceInstanceAddress entitySliceAddress,
|
|
AZ::SliceComponent::SliceInstanceAddress& transformAncestorSliceAddress,
|
|
AZ::SliceComponent::EntityAncestorList& sliceAncestryToPushTo,
|
|
AZStd::unordered_map<AZ::Data::AssetId, EntityIdSet>& unpushableEntityIdsPerAsset,
|
|
AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>>& newChildEntityIdAncestorPairs,
|
|
AZStd::vector< AZ::Data::AssetId>& rootAncestorPushList)
|
|
{
|
|
if (entitySliceAddress.IsValid())
|
|
{
|
|
if (!sliceAncestryToPushTo.empty())
|
|
{
|
|
// trivial reject, don't allow pushes to multiple slices at once due to complexity in cyclic dependency check.
|
|
if (!sliceAncestryToPushTo.empty())
|
|
{
|
|
AZ::Data::AssetId rootAssetId = sliceAncestryToPushTo.back().m_sliceAddress.GetReference()->GetSliceAsset().GetId();
|
|
|
|
for (AZ::Data::AssetId pushAncestor : rootAncestorPushList)
|
|
{
|
|
if (pushAncestor != rootAssetId)
|
|
{
|
|
// Pushing addition of multiple slice-owned entities is currently disabled due to
|
|
// the complexity of detecting cycles in slice hierarchy.
|
|
EntityIdSet* unpushableEntityIds = &unpushableEntityIdsPerAsset[rootAssetId];
|
|
if (AZStd::find(unpushableEntityIds->begin(), unpushableEntityIds->end(), entityId) == unpushableEntityIds->end())
|
|
{
|
|
unpushableEntityIds->insert(entityId);
|
|
}
|
|
|
|
// this entity can't be pushed to this ancestry, no further checking to do
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is an entity that already belongs to a slice, need to verify it's a valid add
|
|
if (entitySliceAddress == transformAncestorSliceAddress)
|
|
{
|
|
// Entity shares slice instance address with transform ancestor, so it doesn't need to be added - it's already there!
|
|
return;
|
|
}
|
|
|
|
// Otherwise, this is a slice-owned entity that we want to push.
|
|
// At this point we have verified that it'd be safe to push to the immediate slice instance,
|
|
// but in the PushWidget the user will have the option of pushing to any slice asset
|
|
// in the sliceAncestryToPushTo. We need to check each ancestry entry and cull out any
|
|
// pushes that would result in cyclic asset dependencies.
|
|
// Example: Slice1 contains Slice2. I have a separate instance of Slice2, call it Slice2b.
|
|
// It is valid to push Slice2b to Slice1, since Slice1 would then have two instances of Slice2.
|
|
// But it would be invalid to push the addition of Slice2b to Slice2, since then Slice2 would
|
|
// reference itself.
|
|
|
|
bool canPush = false;
|
|
for (auto ancestorIt = sliceAncestryToPushTo.begin(); ancestorIt != sliceAncestryToPushTo.end(); ++ancestorIt)
|
|
{
|
|
const AZ::SliceComponent::SliceInstanceAddress& targetInstanceAddress = ancestorIt->m_sliceAddress;
|
|
if (SliceUtilities::CheckSliceAdditionCyclicDependencySafe(entitySliceAddress, targetInstanceAddress))
|
|
{
|
|
canPush = true;
|
|
|
|
newChildEntityIdAncestorPairs.emplace_back(entityId, sliceAncestryToPushTo);
|
|
}
|
|
else
|
|
{
|
|
//remember that this entity is unpushble to this slice asset
|
|
EntityIdSet* unpushableEntityIds = &unpushableEntityIdsPerAsset[targetInstanceAddress.GetReference()->GetSliceAsset().GetId()];
|
|
if (AZStd::find(unpushableEntityIds->begin(), unpushableEntityIds->end(), entityId) == unpushableEntityIds->end())
|
|
{
|
|
unpushableEntityIds->insert(entityId);
|
|
}
|
|
break;// Once you find one invalid ancestor, all the rest will be as well
|
|
}
|
|
}
|
|
|
|
if (canPush)
|
|
{
|
|
AZ::Data::AssetId targetSliceAssetId = sliceAncestryToPushTo.at(0).m_sliceAddress.GetReference()->GetSliceAsset().GetId();
|
|
|
|
//remember we're trying to push to this root, so we don't try to push to any others
|
|
size_t ancestrySize = sliceAncestryToPushTo.size();
|
|
rootAncestorPushList.push_back(sliceAncestryToPushTo[ancestrySize-1].m_sliceAddress.GetReference()->GetSliceAsset().GetId());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// This is an entity that doesn't belong to a slice yet, consider it for addition
|
|
newChildEntityIdAncestorPairs.emplace_back(entityId, AZStd::move(sliceAncestryToPushTo));
|
|
}
|
|
}
|
|
|
|
AZStd::shared_ptr<AZ::Entity> GetSliceEntityForAssetId(const AZ::Data::AssetId& assetId)
|
|
{
|
|
if (!assetId.IsValid())
|
|
{
|
|
AZ_Warning("Slice", false, "AssetId is invalid for asset %s", assetId.ToString<AZStd::string>().c_str());
|
|
return nullptr;
|
|
}
|
|
|
|
AZStd::string relativePath, fullPath;
|
|
AZ::Data::AssetCatalogRequestBus::BroadcastResult(relativePath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, assetId);
|
|
|
|
if (relativePath.empty())
|
|
{
|
|
AZ_Warning("Slice", false, "No relative path found for asset %s", assetId.ToString<AZStd::string>().c_str());
|
|
return nullptr;
|
|
}
|
|
|
|
bool fullPathFound = false;
|
|
AzToolsFramework::AssetSystemRequestBus::BroadcastResult(fullPathFound, &AzToolsFramework::AssetSystemRequestBus::Events::GetFullSourcePathFromRelativeProductPath, relativePath, fullPath);
|
|
|
|
if (fullPathFound)
|
|
{
|
|
AZStd::shared_ptr<AZ::Entity> sliceEntity(AZ::Utils::LoadObjectFromFile<AZ::Entity>(fullPath, nullptr,
|
|
AZ::ObjectStream::FilterDescriptor(&AZ::Data::AssetFilterNoAssetLoading)));
|
|
|
|
return sliceEntity;
|
|
}
|
|
else
|
|
{
|
|
AZ_Warning("Slice", false, "Could not find full path for asset %s", assetId.ToString<AZStd::string>().c_str());
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
} // namespace Internal
|
|
|
|
void SliceUserSettings::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
|
|
if (serializeContext)
|
|
{
|
|
serializeContext->Class<SliceUserSettings>()
|
|
->Version(1)
|
|
->Field("m_saveLocation", &SliceUserSettings::m_saveLocation)
|
|
->Field("m_autoNumber", &SliceUserSettings::m_autoNumber);
|
|
}
|
|
}
|
|
|
|
void Reflect(AZ::ReflectContext* context)
|
|
{
|
|
SliceUserSettings::Reflect(context);
|
|
}
|
|
|
|
QString GetSliceItemIconPath() { return ":/PropertyEditor/Resources/slice_item.png"; }
|
|
|
|
QString GetSliceItemChangedIconPath() { return ":/PropertyEditor/Resources/Slice_Handle_Modified.svg"; }
|
|
|
|
QString GetLShapeIconPath() { return ":/PropertyEditor/Resources/l_shape.png"; }
|
|
|
|
QString GetSliceEntityIconPath() { return ":/PropertyEditor/Resources/Slice_Entity.svg"; }
|
|
|
|
QString GetWarningIconPath() { return ":/PropertyEditor/Resources/warning.png"; }
|
|
|
|
AZ::u32 GetSliceItemHeight() { return 16; }
|
|
|
|
QSize GetSliceItemIconSize() { return QSize(20, GetSliceItemHeight()); }
|
|
|
|
QSize GetLShapeIconSize() { return QSize(12, GetSliceItemHeight()); }
|
|
|
|
QSize GetWarningIconSize() { return QSize(26, 25); }
|
|
|
|
int GetWarningLabelMinimumWidth() { return 300; }
|
|
|
|
AZ::u32 GetSliceHierarchyMenuIdentationPerLevel() { return 5; }
|
|
|
|
int GetSliceSelectFontSize() { return 10; }
|
|
|
|
AZStd::string GetSliceFileExtension() { return ".slice"; }
|
|
} // namespace SliceUtilities
|
|
} // AzToolsFramework
|