UiSliceManager.cpp 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include "EditorCommon.h"
  9. #include <AzToolsFramework/Slice/SliceUtilities.h>
  10. #include <AzCore/Component/ComponentApplication.h>
  11. #include <AzCore/Component/EntityUtils.h>
  12. #include <AzCore/IO/FileIO.h>
  13. #include <AzCore/Serialization/SerializeContext.h>
  14. #include <AzCore/Serialization/Utils.h>
  15. #include <AzCore/Asset/AssetManager.h>
  16. #include <AzCore/Asset/AssetManagerBus.h>
  17. #include <AzCore/Math/Transform.h>
  18. #include <AzCore/Math/Quaternion.h>
  19. #include <AzCore/std/sort.h>
  20. #include <AzCore/std/smart_ptr/make_shared.h>
  21. #include <AzFramework/Asset/AssetSystemBus.h>
  22. #include <AzFramework/Entity/EntityContextBus.h>
  23. #include <AzFramework/StringFunc/StringFunc.h>
  24. #include <AzToolsFramework/ToolsComponents/GenericComponentWrapper.h>
  25. #include <AzToolsFramework/Entity/EditorEntityContextBus.h>
  26. #include <AzToolsFramework/SourceControl/SourceControlAPI.h>
  27. #include <AzToolsFramework/API/EditorAssetSystemAPI.h>
  28. #include <AzToolsFramework/Slice/SliceTransaction.h>
  29. #include <AzToolsFramework/UI/UICore/ProgressShield.hxx>
  30. #include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
  31. #include <AzToolsFramework/UI/Slice/SlicePushWidget.hxx>
  32. #include <QtWidgets/QWidget>
  33. #include <QtWidgets/QDialog>
  34. #include <QtWidgets/QFileDialog>
  35. #include <QtWidgets/QMessageBox>
  36. #include <QtWidgets/QErrorMessage>
  37. #include <QtWidgets/QVBoxLayout>
  38. #include <QtCore/QThread>
  39. #include <LyShine/Bus/UiElementBus.h>
  40. #include <LyShine/Bus/Tools/UiSystemToolsBus.h>
  41. #include <AzFramework/API/ApplicationAPI.h>
  42. #include <AzToolsFramework/AssetBrowser/Search/Filter.h>
  43. #include <AzToolsFramework/AssetBrowser/AssetSelectionModel.h>
  44. //////////////////////////////////////////////////////////////////////////
  45. UiSliceManager::UiSliceManager(AzFramework::EntityContextId entityContextId)
  46. : m_entityContextId(entityContextId)
  47. {
  48. UiEditorEntityContextNotificationBus::Handler::BusConnect();
  49. }
  50. //////////////////////////////////////////////////////////////////////////
  51. UiSliceManager::~UiSliceManager()
  52. {
  53. UiEditorEntityContextNotificationBus::Handler::BusDisconnect();
  54. }
  55. //////////////////////////////////////////////////////////////////////////
  56. void UiSliceManager::OnSliceInstantiationFailed(const AZ::Data::AssetId&, const AzFramework::SliceInstantiationTicket&)
  57. {
  58. QMessageBox::warning(QApplication::activeWindow(),
  59. QStringLiteral("Cannot Instantiate UI Slice"),
  60. QString("Slice cannot be instantiated. Check that it is a slice containing UI elements."),
  61. QMessageBox::Ok);
  62. }
  63. //////////////////////////////////////////////////////////////////////////
  64. void UiSliceManager::InstantiateSlice(const AZ::Data::AssetId& assetId, AZ::Vector2 viewportPosition, int childIndex)
  65. {
  66. AZ::Data::Asset<AZ::SliceAsset> sliceAsset;
  67. sliceAsset.Create(assetId, true);
  68. UiEditorEntityContextRequestBus::Event(
  69. m_entityContextId,
  70. &UiEditorEntityContextRequestBus::Events::InstantiateEditorSliceAtChildIndex,
  71. sliceAsset,
  72. viewportPosition,
  73. childIndex);
  74. }
  75. //////////////////////////////////////////////////////////////////////////
  76. void UiSliceManager::InstantiateSliceUsingBrowser([[maybe_unused]] HierarchyWidget* hierarchy, AZ::Vector2 viewportPosition)
  77. {
  78. AssetSelectionModel selection = AssetSelectionModel::AssetTypeSelection("Slice");
  79. AzToolsFramework::EditorRequests::Bus::Broadcast(&AzToolsFramework::EditorRequests::BrowseForAssets, selection);
  80. if (!selection.IsValid())
  81. {
  82. return;
  83. }
  84. auto product = azrtti_cast<const ProductAssetBrowserEntry*>(selection.GetResult());
  85. AZ_Assert(product, "Selection is invalid.");
  86. InstantiateSlice(product->GetAssetId(), viewportPosition);
  87. }
  88. //////////////////////////////////////////////////////////////////////////
  89. void UiSliceManager::MakeSliceFromSelectedItems(HierarchyWidget* hierarchy, bool inheritSlices)
  90. {
  91. QTreeWidgetItemRawPtrQList selectedItems(hierarchy->selectedItems());
  92. HierarchyItemRawPtrList items = SelectionHelpers::GetSelectedHierarchyItems(hierarchy,
  93. selectedItems);
  94. AzToolsFramework::EntityIdList selectedEntities;
  95. for (auto item : items)
  96. {
  97. selectedEntities.push_back(item->GetEntityId());
  98. }
  99. MakeSliceFromEntities(selectedEntities, inheritSlices);
  100. }
  101. bool UiSliceManager::IsRootEntity([[maybe_unused]] const AZ::Entity& entity) const
  102. {
  103. // This is only used by IsNodePushable. For the UI system, we allow the root slice
  104. // to be pushed updates, so we always return false here to allow that. If the UI
  105. // system ever wants to leverage NotPushableOnSliceRoot, we'll need to revisit this.
  106. return false;
  107. }
  108. AZ::SliceComponent* UiSliceManager::GetRootSlice() const
  109. {
  110. AZ::SliceComponent* rootSlice = nullptr;
  111. UiEditorEntityContextRequestBus::EventResult(rootSlice, m_entityContextId, &UiEditorEntityContextRequestBus::Events::GetUiRootSlice);
  112. return rootSlice;
  113. }
  114. //////////////////////////////////////////////////////////////////////////
  115. // PRIVATE MEMBER FUNCTIONS
  116. //////////////////////////////////////////////////////////////////////////
  117. //////////////////////////////////////////////////////////////////////////
  118. void UiSliceManager::MakeSliceFromEntities(AzToolsFramework::EntityIdList& entities, bool inheritSlices)
  119. {
  120. // expand the list of entities to include all child entities
  121. AzToolsFramework::EntityIdSet entitiesAndDescendants = GatherEntitiesAndAllDescendents(entities);
  122. const AZStd::string slicesAssetsPath = "@projectroot@/UI/Slices";
  123. if (!gEnv->pFileIO->Exists(slicesAssetsPath.c_str()))
  124. {
  125. gEnv->pFileIO->CreatePath(slicesAssetsPath.c_str());
  126. }
  127. char path[AZ_MAX_PATH_LEN] = { 0 };
  128. gEnv->pFileIO->ResolvePath(slicesAssetsPath.c_str(), path, AZ_MAX_PATH_LEN);
  129. MakeNewSlice(entitiesAndDescendants, path, inheritSlices);
  130. }
  131. //////////////////////////////////////////////////////////////////////////
  132. bool UiSliceManager::MakeNewSlice(
  133. const AzToolsFramework::EntityIdSet& entities,
  134. const char* targetDirectory,
  135. bool inheritSlices,
  136. AZ::SerializeContext* serializeContext)
  137. {
  138. AZ_PROFILE_FUNCTION(AzToolsFramework);
  139. if (entities.empty())
  140. {
  141. return false;
  142. }
  143. if (!serializeContext)
  144. {
  145. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
  146. AZ_Assert(serializeContext, "Failed to retrieve application serialize context.");
  147. }
  148. // Save a reference to our currently active window since it will be
  149. // temporarily null after QFileDialogs close, which we need in order to
  150. // be able to parent our message dialogs properly
  151. QWidget* activeWindow = QApplication::activeWindow();
  152. //
  153. // Check for entity references outside of selected entities - we don't allow this in UI slices
  154. //
  155. AzToolsFramework::EntityIdSet entitiesToInclude = entities;
  156. {
  157. AzToolsFramework::EntityIdSet allReferencedEntities;
  158. bool hasExternalReferences = false;
  159. AzToolsFramework::SliceUtilities::GatherAllReferencedEntitiesAndCompare(entitiesToInclude, allReferencedEntities, hasExternalReferences, *serializeContext);
  160. if (hasExternalReferences)
  161. {
  162. const AZStd::string message = AZStd::string::format(
  163. "Some of the selected entities reference entities not contained in the selection and its children.\n"
  164. "UI slices cannot contain references to outside of the slice.\n");
  165. QMessageBox::warning(activeWindow, QStringLiteral("Create Slice"),
  166. QString(message.c_str()), QMessageBox::Ok);
  167. return false;
  168. }
  169. }
  170. //
  171. // Verify single root and generate an ordered entity list
  172. //
  173. AzToolsFramework::EntityIdList orderedEntityList;
  174. AZ::Entity* insertBefore = nullptr;
  175. AZ::Entity* commonParent = nullptr;
  176. {
  177. commonParent = ValidateSingleRootAndGenerateOrderedEntityList(entitiesToInclude, orderedEntityList, insertBefore);
  178. if (!commonParent)
  179. {
  180. QMessageBox::warning(activeWindow,
  181. QStringLiteral("Cannot Create UI Slice"),
  182. QString("The slice cannot be created because there is no single element in the selection that is parent "
  183. "to all other elements in the selection."
  184. "Please make sure your slice contains only one root entity.\n\n"
  185. "You may want to create a new entity, and assign it as the parent of your existing root entities."),
  186. QMessageBox::Ok);
  187. return false;
  188. }
  189. AZ_Assert(!orderedEntityList.empty(), "Empty orderedEntityList during UI slice creation!");
  190. }
  191. //
  192. // Determine slice asset file name/path - default to name of root entity, ask user
  193. //
  194. AZStd::string sliceName;
  195. AZStd::string sliceFilePath;
  196. {
  197. AZStd::string suggestedName = "UISlice";
  198. UiElementBus::EventResult(suggestedName, orderedEntityList[0], &UiElementBus::Events::GetName);
  199. if (!AzToolsFramework::SliceUtilities::QueryUserForSliceFilename(suggestedName, targetDirectory, AZ_CRC("UISliceUserSettings", 0x4f30f608), activeWindow, sliceName, sliceFilePath))
  200. {
  201. // User cancelled slice creation or error prevented continuation (related warning dialog boxes, if necessary, already done at this point)
  202. return false;
  203. }
  204. }
  205. //
  206. // Setup and execute transaction for the new slice.
  207. //
  208. {
  209. AZ_PROFILE_SCOPE(AzToolsFramework, "UiSliceManager::MakeNewSlice:SetupAndExecuteTransaction");
  210. using AzToolsFramework::SliceUtilities::SliceTransaction;
  211. // PostSaveCallback for slice creation: kick off async replacement of source entities with an instance of the new slice.
  212. SliceTransaction::PostSaveCallback postSaveCallback =
  213. [this, &entitiesToInclude, &commonParent, &insertBefore]
  214. (SliceTransaction::TransactionPtr transaction, const char* fullPath, const SliceTransaction::SliceAssetPtr& /*asset*/) -> void
  215. {
  216. AZ_PROFILE_SCOPE(AzToolsFramework, "UiSliceManager::MakeNewSlice:PostSaveCallback");
  217. // Once the asset is processed and ready, we can replace the source entities with an instance of the new slice.
  218. UiEditorEntityContextRequestBus::Event(m_entityContextId,
  219. &UiEditorEntityContextRequestBus::Events::QueueSliceReplacement,
  220. fullPath, transaction->GetLiveToAssetEntityIdMap(), entitiesToInclude, commonParent, insertBefore);
  221. };
  222. SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginNewSlice(nullptr, serializeContext);
  223. // Add entities
  224. {
  225. AZ_PROFILE_SCOPE(AzToolsFramework, "UiSliceManager::MakeNewSlice:SetupAndExecuteTransaction:AddEntities");
  226. for (const AZ::EntityId& entityId : orderedEntityList)
  227. {
  228. SliceTransaction::Result addResult = transaction->AddEntity(entityId, !inheritSlices ? SliceTransaction::SliceAddEntityFlags::DiscardSliceAncestry : 0);
  229. if (!addResult)
  230. {
  231. QMessageBox::warning(activeWindow, QStringLiteral("Slice Save Failed"),
  232. QString(addResult.GetError().c_str()), QMessageBox::Ok);
  233. return false;
  234. }
  235. }
  236. }
  237. SliceTransaction::Result result = transaction->Commit(
  238. sliceFilePath.c_str(),
  239. nullptr,
  240. postSaveCallback,
  241. AzToolsFramework::SliceUtilities::SliceTransaction::SliceCommitFlags::DisableUndoCapture);
  242. if (!result)
  243. {
  244. QMessageBox::warning(activeWindow, QStringLiteral("Slice Save Failed"),
  245. QString(result.GetError().c_str()), QMessageBox::Ok);
  246. return false;
  247. }
  248. return true;
  249. }
  250. }
  251. //////////////////////////////////////////////////////////////////////////
  252. void UiSliceManager::GetTopLevelEntities(const AZ::SliceComponent::EntityList& entities, AZ::SliceComponent::EntityList& topLevelEntities)
  253. {
  254. AZStd::unordered_set<AZ::Entity*> allEntities;
  255. allEntities.insert(entities.begin(), entities.end());
  256. for (auto entity : entities)
  257. {
  258. // if this entities parent is not in the set then it is a top-level
  259. AZ::Entity* parentElement = nullptr;
  260. UiElementBus::EventResult(parentElement, entity->GetId(), &UiElementBus::Events::GetParent);
  261. if (parentElement)
  262. {
  263. if (allEntities.count(parentElement) == 0)
  264. {
  265. topLevelEntities.push_back(entity);
  266. }
  267. }
  268. }
  269. }
  270. //////////////////////////////////////////////////////////////////////////
  271. // This is similar to ToolsApplicationRequests::GatherEntitiesAndAllDescendents
  272. // except that function assumes that the entities are supporting the the AZ::TransformBus
  273. // for hierarchy. This UI-specific version uses the UiElementBus
  274. AzToolsFramework::EntityIdSet UiSliceManager::GatherEntitiesAndAllDescendents(const AzToolsFramework::EntityIdList& inputEntities)
  275. {
  276. AzToolsFramework::EntityIdSet output;
  277. AzToolsFramework::EntityIdList tempList;
  278. for (const AZ::EntityId& id : inputEntities)
  279. {
  280. output.insert(id);
  281. LyShine::EntityArray descendants;
  282. UiElementBus::Event(id, &UiElementBus::Events::FindDescendantElements, [](const AZ::Entity*) { return true; }, descendants);
  283. for (auto descendant : descendants)
  284. {
  285. output.insert(descendant->GetId());
  286. }
  287. }
  288. return output;
  289. }
  290. //////////////////////////////////////////////////////////////////////////
  291. // PreSaveCallback for SliceTransactions in Slice Pushes
  292. // Fails pushes if:
  293. // - referenced entities are not included in the slice
  294. // - added entities in push are not referenced as children of entities in slice
  295. // - any entities have become orphaned with selected push options
  296. // - there's more than one root entity
  297. AzToolsFramework::SliceUtilities::SliceTransaction::Result SlicePreSaveCallbackForUiEntities(
  298. AzToolsFramework::SliceUtilities::SliceTransaction::TransactionPtr transaction,
  299. [[maybe_unused]] const char* fullPath,
  300. AzToolsFramework::SliceUtilities::SliceTransaction::SliceAssetPtr& asset)
  301. {
  302. AZ_PROFILE_SCOPE(AzToolsFramework, "SlicePreSaveCallbackForUiEntities");
  303. // we want to ensure that "bad" data never gets pushed to a slice
  304. // This mostly relates to the m_childEntityIdOrder array since this is something that
  305. // the UI Editor manages closely and requires to be consistent.
  306. AZ::SerializeContext* serializeContext = nullptr;
  307. AZ::ComponentApplicationBus::BroadcastResult(serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
  308. AZ_Assert(serializeContext, "Failed to retrieve application serialize context.");
  309. auto& assetDb = AZ::Data::AssetManager::Instance();
  310. AZ::Data::Asset<AZ::SliceAsset> currentAsset =
  311. assetDb.FindAsset<AZ::SliceAsset>(transaction->GetTargetAsset().GetId(), AZ::Data::AssetLoadBehavior::Default);
  312. AZ::SliceComponent* clonedSliceComponent = asset.Get()->GetComponent();
  313. AZ::SliceComponent* currentSliceComponent = currentAsset.Get()->GetComponent();
  314. AZ::SliceComponent::EntityList clonedEntities;
  315. clonedSliceComponent->GetEntities(clonedEntities);
  316. AZ::SliceComponent::EntityList currentEntities;
  317. currentSliceComponent->GetEntities(currentEntities);
  318. // store a set of pairs which are the EntityId being referenced and the Entity that is referencing it
  319. using ReferencedEntityPair = AZStd::pair<AZ::EntityId, AZ::Entity*>;
  320. AZStd::unordered_set<ReferencedEntityPair> referencedEntities;
  321. AZStd::unordered_set<AZ::EntityId> referencedChildEntities;
  322. AZStd::unordered_set<AZ::EntityId> clonedEntityIds;
  323. AZStd::unordered_set<AZ::EntityId> addedEntities;
  324. for (auto clonedEntity : clonedEntities)
  325. {
  326. clonedEntityIds.insert(clonedEntity->GetId());
  327. auto iter = AZStd::find_if(currentEntities.begin(), currentEntities.end(),
  328. [clonedEntity](AZ::Entity* entity) -> bool
  329. {
  330. return entity->GetId() == clonedEntity->GetId();
  331. });
  332. if (iter == currentEntities.end())
  333. {
  334. // this clonedEntity is an addition to the slice
  335. addedEntities.insert(clonedEntity->GetId());
  336. }
  337. AZ::EntityUtils::EnumerateEntityIds(clonedEntity,
  338. [clonedEntity, &referencedEntities, &referencedChildEntities]
  339. (const AZ::EntityId& id, bool isEntityId, const AZ::SerializeContext::ClassElement* elementData) -> void
  340. {
  341. if (!isEntityId && id.IsValid())
  342. {
  343. // Include this id.
  344. referencedEntities.insert({ id, clonedEntity });
  345. // Check if this is a child reference. We can detect that because the EntityId is in the "ChildEntityId"
  346. // member of the ChildEntityIdOrderEntry struct.
  347. if (elementData && !elementData->m_editData)
  348. {
  349. if (strcmp(elementData->m_name, "ChildEntityId") == 0)
  350. {
  351. referencedChildEntities.insert(id);
  352. }
  353. }
  354. }
  355. }, serializeContext);
  356. }
  357. // Issue a warning if any referenced entities are not in the slice being created
  358. for (auto referencedEntityPair : referencedEntities)
  359. {
  360. const AZ::EntityId& referencedEntityId = referencedEntityPair.first;
  361. if (clonedEntityIds.count(referencedEntityId) == 0)
  362. {
  363. const AZ::SliceComponent::EntityIdToEntityIdMap& entityIdMap = transaction->GetLiveToAssetEntityIdMap();
  364. AZ::Entity* referencingEntity = referencedEntityPair.second;
  365. AZ::EntityId referencingEntityId = referencingEntity->GetId();
  366. // in order to get the hierarchical name of the referencing entity we need to find the live version of the entity
  367. // this requires a reverse look up in the entityIdMap
  368. AZ::EntityId liveReferencingEntityId;
  369. for (auto entry : entityIdMap)
  370. {
  371. if (entry.second == referencingEntityId)
  372. {
  373. liveReferencingEntityId = entry.first;
  374. break;
  375. }
  376. }
  377. AZStd::string referencingEntityName;
  378. if (liveReferencingEntityId.IsValid())
  379. {
  380. referencingEntityName = EntityHelpers::GetHierarchicalElementName(liveReferencingEntityId);
  381. }
  382. else
  383. {
  384. // this should not happen, if it does just use the non-hierarchical name
  385. referencingEntityName = referencingEntity->GetName();
  386. }
  387. // Ideally we could find a hierarchical field name like "UiButtonComponent/State Actions/Hover[2]/Color/Target" but
  388. // this just finds "Target" in that example.
  389. AZStd::string fieldName;
  390. AZ::EntityUtils::EnumerateEntityIds(referencingEntity,
  391. [&referencedEntityId, &fieldName]
  392. (const AZ::EntityId& id, bool isEntityId, const AZ::SerializeContext::ClassElement* elementData) -> void
  393. {
  394. if (!isEntityId && id.IsValid() && id == referencedEntityId)
  395. {
  396. // We have found the reference to this external or deleted EntityId
  397. if (elementData)
  398. {
  399. if (elementData->m_editData)
  400. {
  401. fieldName = elementData->m_editData->m_name;
  402. }
  403. else
  404. {
  405. fieldName = elementData->m_name;
  406. }
  407. }
  408. else
  409. {
  410. fieldName = "<Unknown>";
  411. }
  412. }
  413. }, serializeContext);
  414. // see if the entity has been deleted
  415. AZ::Entity* referencedEntity = nullptr;
  416. AZ::ComponentApplicationBus::BroadcastResult(
  417. referencedEntity, &AZ::ComponentApplicationBus::Events::FindEntity, referencedEntityId);
  418. if (referencedEntity)
  419. {
  420. AZStd::string referencedEntityName = EntityHelpers::GetHierarchicalElementName(referencedEntityId);
  421. return AZ::Failure(AZStd::string::format("There are external references. "
  422. "Entity '%s' in the slice being pushed references another entity that will not be in the slice after the push. "
  423. "Referenced entity is '%s'. The name of the property field referencing it is '%s'.",
  424. referencingEntityName.c_str(), referencedEntityName.c_str(), fieldName.c_str()));
  425. }
  426. else
  427. {
  428. return AZ::Failure(AZStd::string::format("There are external references. "
  429. "Entity '%s' in the slice being pushed references another entity that will not be in the slice after the push. "
  430. "Referenced entity no longer exists, it's ID was '%s'. The name of the property field referencing it is '%s'.",
  431. referencingEntityName.c_str(), referencedEntityId.ToString().c_str(), fieldName.c_str()));
  432. }
  433. }
  434. }
  435. // Issue a warning if there are any added entities that are not referenced as children of entities in the slice
  436. for (auto entityId : addedEntities)
  437. {
  438. if (referencedChildEntities.count(entityId) == 0)
  439. {
  440. AZStd::string name = EntityHelpers::GetHierarchicalElementName(entityId);
  441. return AZ::Failure(AZStd::string::format("There are added entities that are unreferenced. "
  442. "An entity is being added to the slice but it is not referenced as "
  443. "the child of another entity in the slice."
  444. "The added entity that is unreferenced is '%s'.", name.c_str()));
  445. }
  446. }
  447. // Check for any entities in the slice that have become orphaned. This can happen is a remove if pushed
  448. // but the entity removal is unchecked while the removal from the m_childEntityIdOrder array is checked
  449. int parentlessEntityCount = 0;
  450. for (auto entityId : clonedEntityIds)
  451. {
  452. if (referencedChildEntities.count(entityId) == 0)
  453. {
  454. // this entity is not a child of any entity
  455. ++parentlessEntityCount;
  456. }
  457. }
  458. // There can only be one "root" entity in a slice - i.e. one entity which is not referenced as a child of another
  459. // entity in the slice
  460. if (parentlessEntityCount > 1)
  461. {
  462. return AZ::Failure(AZStd::string::format("There is more than one root entity. "
  463. "Possibly a child reference is being removed in this push but the child entity is not."));
  464. }
  465. return AZ::Success();
  466. }
  467. //////////////////////////////////////////////////////////////////////////
  468. void UiSliceManager::PushEntitiesModal(const AzToolsFramework::EntityIdList& entities,
  469. AZ::SerializeContext* serializeContext)
  470. {
  471. // Use same SlicePushWidget as world entities do
  472. AzToolsFramework::SlicePushWidgetConfigPtr config = AZStd::make_shared<AzToolsFramework::SlicePushWidgetConfig>();
  473. config->m_defaultAddedEntitiesCheckState = true;
  474. config->m_defaultRemovedEntitiesCheckState = true;
  475. config->m_rootSlice = GetRootSlice();
  476. AZ_Warning("UiSlicePush", config->m_rootSlice != nullptr, "Could not find root slice for Slice Push!");
  477. config->m_preSaveCB = SlicePreSaveCallbackForUiEntities;
  478. config->m_postSaveCB = nullptr;
  479. config->m_deleteEntitiesCB = [this](const AzToolsFramework::EntityIdList& entitiesToRemove) -> void
  480. {
  481. UiEditorEntityContextRequestBus::Event(
  482. this->GetEntityContextId(), &UiEditorEntityContextRequestBus::Events::DeleteElements, entitiesToRemove);
  483. };
  484. config->m_isRootEntityCB = [this](const AZ::Entity* entity) -> bool
  485. {
  486. return this->IsRootEntity(*entity);
  487. };
  488. QDialog* dialog = new QDialog();
  489. QVBoxLayout* mainLayout = new QVBoxLayout();
  490. mainLayout->setContentsMargins(0, 0, 0, 0);
  491. AzToolsFramework::SlicePushWidget* widget = new AzToolsFramework::SlicePushWidget(entities, config, serializeContext);
  492. mainLayout->addWidget(widget);
  493. dialog->setWindowTitle(widget->tr("Save Slice Overrides - Advanced"));
  494. dialog->setMinimumSize(QSize(800, 300));
  495. dialog->resize(QSize(1200, 600));
  496. dialog->setLayout(mainLayout);
  497. QWidget::connect(widget, &AzToolsFramework::SlicePushWidget::OnFinished, dialog,
  498. [dialog]()
  499. {
  500. dialog->accept();
  501. }
  502. );
  503. QWidget::connect(widget, &AzToolsFramework::SlicePushWidget::OnCanceled, dialog,
  504. [dialog]()
  505. {
  506. dialog->reject();
  507. }
  508. );
  509. dialog->exec();
  510. delete dialog;
  511. }
  512. //////////////////////////////////////////////////////////////////////////
  513. void UiSliceManager::DetachSliceEntities(const AzToolsFramework::EntityIdList& entities)
  514. {
  515. if (!entities.empty())
  516. {
  517. QString title;
  518. QString body;
  519. if (entities.size() == 1)
  520. {
  521. title = QObject::tr("Detach Slice Entity");
  522. body = QObject::tr("A detached entity will no longer receive pushes from its slice. The entity will be converted into a non-slice entity. This action cannot be undone.\n\n"
  523. "Are you sure you want to detach the selected entity?");
  524. }
  525. else
  526. {
  527. title = QObject::tr("Detach Slice Entities");
  528. body = QObject::tr("Detached entities no longer receive pushes from their slices. The entities will be converted into non-slice entities. This action cannot be undone.\n\n"
  529. "Are you sure you want to detach the selected entities and their descendants?");
  530. }
  531. if (ConfirmDialog_Detach(title, body))
  532. {
  533. UiEditorEntityContextRequestBus::Event(
  534. m_entityContextId, &UiEditorEntityContextRequestBus::Events::DetachSliceEntities, entities);
  535. }
  536. }
  537. }
  538. //////////////////////////////////////////////////////////////////////////
  539. void UiSliceManager::DetachSliceInstances(const AzToolsFramework::EntityIdList& entities)
  540. {
  541. if (!entities.empty())
  542. {
  543. // Get all slice instances for given entities
  544. AZStd::vector<AZ::SliceComponent::SliceInstanceAddress> sliceInstances;
  545. for (const AZ::EntityId& entityId : entities)
  546. {
  547. AZ::SliceComponent::SliceInstanceAddress sliceAddress;
  548. AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entityId,
  549. &AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
  550. if (sliceAddress.IsValid())
  551. {
  552. if (sliceInstances.end() == AZStd::find(sliceInstances.begin(), sliceInstances.end(), sliceAddress))
  553. {
  554. sliceInstances.push_back(sliceAddress);
  555. }
  556. }
  557. }
  558. QString title;
  559. QString body;
  560. if (sliceInstances.size() == 1)
  561. {
  562. title = QObject::tr("Detach Slice Instance");
  563. body = QObject::tr("A detached instance will no longer receive pushes from its slice. All entities in the slice instance will be converted into non-slice entities. This action cannot be undone.\n\n"
  564. "Are you sure you want to detach the selected instance?");
  565. }
  566. else
  567. {
  568. title = QObject::tr("Detach Slice Instances");
  569. body = QObject::tr("Detached instances no longer receive pushes from their slices. All entities in the slice instances will be converted into non-slice entities. This action cannot be undone.\n\n"
  570. "Are you sure you want to detach the selected instances?");
  571. }
  572. if (ConfirmDialog_Detach(title, body))
  573. {
  574. // Get all instantiated entities for the slice instances
  575. AzToolsFramework::EntityIdList entitiesToDetach = entities;
  576. for (const AZ::SliceComponent::SliceInstanceAddress& sliceInstance : sliceInstances)
  577. {
  578. const AZ::SliceComponent::InstantiatedContainer* instantiated = sliceInstance.GetInstance()->GetInstantiated();
  579. if (instantiated)
  580. {
  581. for (AZ::Entity* entityInSlice : instantiated->m_entities)
  582. {
  583. entitiesToDetach.push_back(entityInSlice->GetId());
  584. }
  585. }
  586. }
  587. // Detach the entities
  588. UiEditorEntityContextRequestBus::Event(
  589. m_entityContextId, &UiEditorEntityContextRequestBus::Events::DetachSliceEntities, entitiesToDetach);
  590. }
  591. }
  592. }
  593. //////////////////////////////////////////////////////////////////////////
  594. AZ::Entity* UiSliceManager::ValidateSingleRootAndGenerateOrderedEntityList(const AzToolsFramework::EntityIdSet& liveEntities, AzToolsFramework::EntityIdList& outOrderedEntityList, AZ::Entity*& insertBefore)
  595. {
  596. // The low-level slice component code has no limit on there being a single root element
  597. // in a slice. It does make it simpler to do so though. Also this is the same limitation
  598. // that we had with the old Prefabs in the UI Editor.
  599. AZStd::unordered_set<AZ::EntityId> childrenOfCommonParent;
  600. AZ::Entity* commonParent = nullptr;
  601. for (auto entity : liveEntities)
  602. {
  603. AZ::Entity* parentElement = nullptr;
  604. UiElementBus::EventResult(parentElement, entity, &UiElementBus::Events::GetParent);
  605. if (parentElement)
  606. {
  607. // if this entities parent is not in the set then it is a top-level
  608. if (liveEntities.count(parentElement->GetId()) == 0)
  609. {
  610. // this is a top level element
  611. if (commonParent)
  612. {
  613. if (commonParent != parentElement)
  614. {
  615. // we have already found a parent
  616. return nullptr;
  617. }
  618. else
  619. {
  620. childrenOfCommonParent.insert(entity);
  621. }
  622. }
  623. else
  624. {
  625. commonParent = parentElement;
  626. childrenOfCommonParent.insert(entity);
  627. }
  628. }
  629. }
  630. }
  631. // At present there must be a single UI element that is the root element of the slice
  632. // This means that there should only be one child of the commonParent (the commonParent is always outside
  633. // of the slice)
  634. if (childrenOfCommonParent.size() != 1)
  635. {
  636. return nullptr;
  637. }
  638. // ensure that the top level entities are in the order that they are children of the common parent
  639. // without this check they would be in the order that they were selected
  640. outOrderedEntityList.clear();
  641. LyShine::EntityArray allChildrenOfCommonParent;
  642. UiElementBus::EventResult(allChildrenOfCommonParent, commonParent->GetId(), &UiElementBus::Events::GetChildElements);
  643. bool justFound = false;
  644. for (auto entity : allChildrenOfCommonParent)
  645. {
  646. // if this child is in the set of top level elements to go in the prefab
  647. // then add it to the vectors so that we have an ordered list in child order
  648. if (childrenOfCommonParent.count(entity->GetId()) > 0)
  649. {
  650. outOrderedEntityList.push_back(entity->GetId());
  651. // we are actually only supporting one child of the common parent
  652. // If this is it, set a flag so we can record the child immediately after it.
  653. // This is used later to insert the slice instance before this child
  654. justFound = true;
  655. }
  656. else
  657. {
  658. if (justFound)
  659. {
  660. insertBefore = entity;
  661. justFound = false;
  662. }
  663. }
  664. }
  665. // now add the rest of the entities (that are not top-level) to the list in any order
  666. for (auto entity : liveEntities)
  667. {
  668. if (childrenOfCommonParent.count(entity) == 0)
  669. {
  670. outOrderedEntityList.push_back(entity);
  671. }
  672. }
  673. return commonParent;
  674. }
  675. //////////////////////////////////////////////////////////////////////////
  676. void UiSliceManager::SetEntityContextId(AzFramework::EntityContextId entityContextId)
  677. {
  678. m_entityContextId = entityContextId;
  679. }
  680. //////////////////////////////////////////////////////////////////////////
  681. AZ::Outcome<void, AZStd::string> UiSliceManager::PushEntitiesBackToSlice(const AzToolsFramework::EntityIdList& entityIdList, const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset)
  682. {
  683. return AzToolsFramework::SliceUtilities::PushEntitiesBackToSlice(entityIdList, sliceAsset, SlicePreSaveCallbackForUiEntities);
  684. }
  685. //////////////////////////////////////////////////////////////////////////
  686. AZ::Outcome<void, AZStd::string> UiSliceManager::QuickPushSliceInstance(const AZ::SliceComponent::SliceInstanceAddress& sliceAddress,
  687. const AzToolsFramework::EntityIdList& entityIdList)
  688. {
  689. // we cannot use SliceUtilities::PushEntitiesBackToSlice because that does not handle adds or deletes
  690. using AzToolsFramework::SliceUtilities::SliceTransaction;
  691. const AZ::Data::Asset<AZ::SliceAsset>& sliceAsset = sliceAddress.GetReference()->GetSliceAsset();
  692. if (!sliceAsset)
  693. {
  694. return AZ::Failure(AZStd::string::format("Asset \"%s\" with id %s is not loaded, or is not a slice.",
  695. sliceAsset.GetHint().c_str(),
  696. sliceAsset.GetId().ToString<AZStd::string>().c_str()));
  697. }
  698. // Not all entities in the list need to be part of the slice instance being pushed (sliceAddress) since we could
  699. // be pushing a new instance into the slice. However, it is an error if there is a second instance of the same slice
  700. // asset that we are pushing to in the entity set
  701. for (AZ::EntityId entityId : entityIdList)
  702. {
  703. AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
  704. AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
  705. &AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
  706. if (entitySliceAddress.IsValid() && entitySliceAddress.GetReference()->GetSliceAsset() == sliceAsset)
  707. {
  708. if (entitySliceAddress != sliceAddress)
  709. {
  710. // error there is a second instance of the same slice asset in the set
  711. return AZ::Failure(AZStd::string::format("Entity with id %s is part of a different slice instance of the same slice asset. A slice cannot contain an instance of itself.",
  712. entityId.ToString().c_str()));
  713. }
  714. }
  715. }
  716. // Check for any invalid slices
  717. bool cancelPush = false;
  718. AZ::SliceComponent* assetComponent = sliceAsset.Get()->GetComponent();
  719. if (assetComponent)
  720. {
  721. // If there are any invalid slices, warn the user and allow them to choose the next step.
  722. const AZ::SliceComponent::SliceList& invalidSlices = assetComponent->GetInvalidSlices();
  723. if (invalidSlices.size() > 0)
  724. {
  725. // Assume an invalid slice count of 1 because this is a quick push, which only has one target.
  726. AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult invalidSliceCheckResult = AzToolsFramework::SliceUtilities::DisplayInvalidSliceReferencesWarning(QApplication::activeWindow(),
  727. /*invalidSliceCount*/ 1,
  728. invalidSlices.size(),
  729. /*showDetailsButton*/ true);
  730. switch (invalidSliceCheckResult)
  731. {
  732. case AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult::Details:
  733. {
  734. cancelPush = true;
  735. PushEntitiesModal(entityIdList, nullptr);
  736. }
  737. break;
  738. case AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult::Save:
  739. {
  740. cancelPush = false;
  741. }
  742. break;
  743. case AzToolsFramework::SliceUtilities::InvalidSliceReferencesWarningResult::Cancel:
  744. default:
  745. {
  746. cancelPush = true;
  747. }
  748. break;
  749. }
  750. }
  751. }
  752. if (cancelPush)
  753. {
  754. return AZ::Success();
  755. }
  756. // Make a transaction targeting the specified slice and add all the entities in this set.
  757. SliceTransaction::TransactionPtr transaction = SliceTransaction::BeginSlicePush(sliceAsset);
  758. if (transaction)
  759. {
  760. AzToolsFramework::EntityIdList entitiesBeingAdded;
  761. for (AZ::EntityId entityId : entityIdList)
  762. {
  763. AZ::SliceComponent::SliceInstanceAddress entitySliceAddress;
  764. AzFramework::SliceEntityRequestBus::EventResult(entitySliceAddress, entityId,
  765. &AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
  766. // Check if this slice is in the slice instance being pushed
  767. if (entitySliceAddress == sliceAddress)
  768. {
  769. const SliceTransaction::Result result = transaction->UpdateEntity(entityId);
  770. if (!result)
  771. {
  772. 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",
  773. entityId.ToString().c_str(),
  774. sliceAsset.GetHint().c_str(),
  775. result.GetError().c_str()));
  776. }
  777. }
  778. else
  779. {
  780. // This entity is not in a slice, treat it as an add
  781. SliceTransaction::Result result = transaction->AddEntity(entityId, SliceTransaction::SliceAddEntityFlags::DiscardSliceAncestry);
  782. if (!result)
  783. {
  784. 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",
  785. entityId.ToString().c_str(),
  786. sliceAsset.GetHint().c_str(),
  787. result.GetError().c_str()));
  788. }
  789. entitiesBeingAdded.push_back(entityId);
  790. }
  791. }
  792. // Check for any entity removals
  793. // We know the slice instance details, compare the entities it contains to the entities
  794. // contained in the underlying asset. If it's missing any entities that exist in the asset,
  795. // we can removal the entity from the base slice.
  796. AZStd::unordered_set<AZ::EntityId> uniqueRemovedEntities;
  797. AZ::SliceComponent::EntityAncestorList ancestorList;
  798. AZ::SliceComponent::EntityList assetEntities;
  799. const AZ::SliceComponent::SliceInstanceAddress& instanceAddr = sliceAddress;
  800. if (instanceAddr.IsValid() && instanceAddr.GetReference()->GetSliceAsset() &&
  801. instanceAddr.GetInstance()->GetInstantiated())
  802. {
  803. const AZ::SliceComponent::EntityList& instanceEntities = instanceAddr.GetInstance()->GetInstantiated()->m_entities;
  804. assetEntities.clear();
  805. instanceAddr.GetReference()->GetSliceAsset().Get()->GetComponent()->GetEntities(assetEntities);
  806. if (assetEntities.size() > instanceEntities.size())
  807. {
  808. // The removed entity is already gone from the instance's map, so we have to do a reverse-lookup
  809. // to pin down which specific entities have been removed in the instance vs the asset.
  810. for (auto assetEntityIter = assetEntities.begin(); assetEntityIter != assetEntities.end(); ++assetEntityIter)
  811. {
  812. AZ::Entity* assetEntity = (*assetEntityIter);
  813. const AZ::EntityId assetEntityId = assetEntity->GetId();
  814. if (uniqueRemovedEntities.end() != uniqueRemovedEntities.find(assetEntityId))
  815. {
  816. continue;
  817. }
  818. // Iterate over the entities left in the instance and if none of them have this
  819. // asset entity as its ancestor, then we want to remove it.
  820. // \todo - Investigate ways to make this non-linear time. Tricky since removed entities
  821. // obviously aren't maintained in any maps.
  822. bool foundAsAncestor = false;
  823. for (const AZ::Entity* instanceEntity : instanceEntities)
  824. {
  825. ancestorList.clear();
  826. instanceAddr.GetReference()->GetInstanceEntityAncestry(instanceEntity->GetId(), ancestorList, 1);
  827. if (!ancestorList.empty() && ancestorList.begin()->m_entity == assetEntity)
  828. {
  829. foundAsAncestor = true;
  830. break;
  831. }
  832. }
  833. if (!foundAsAncestor)
  834. {
  835. // Grab ancestors, which determines which slices the removal can be pushed to.
  836. uniqueRemovedEntities.insert(assetEntityId);
  837. }
  838. }
  839. for (AZ::EntityId entityToRemove : uniqueRemovedEntities)
  840. {
  841. SliceTransaction::Result result = transaction->RemoveEntity(entityToRemove);
  842. if (!result)
  843. {
  844. 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",
  845. entityToRemove.ToString().c_str(),
  846. sliceAsset.GetHint().c_str(),
  847. result.GetError().c_str()));
  848. break;
  849. }
  850. }
  851. }
  852. }
  853. const SliceTransaction::Result result = transaction->Commit(
  854. sliceAsset.GetId(),
  855. SlicePreSaveCallbackForUiEntities,
  856. nullptr);
  857. if (result)
  858. {
  859. // Successful commit
  860. // Remove any entities that were succesfully pushed into a slice (since they'll be brought to life through slice reloading)
  861. UiEditorEntityContextRequestBus::Event(
  862. this->GetEntityContextId(), &UiEditorEntityContextRequestBus::Events::DeleteElements, entitiesBeingAdded);
  863. }
  864. else
  865. {
  866. AZStd::string sliceAssetPath;
  867. AZ::Data::AssetCatalogRequestBus::BroadcastResult(sliceAssetPath, &AZ::Data::AssetCatalogRequests::GetAssetPathById, sliceAsset.GetId());
  868. return AZ::Failure(AZStd::string::format("Failed to to save slice \"%s\". Slice push aborted.\n\nError:\n%s",
  869. sliceAssetPath.c_str(),
  870. result.GetError().c_str()));
  871. }
  872. }
  873. return AZ::Success();
  874. }
  875. //////////////////////////////////////////////////////////////////////////
  876. AZStd::string UiSliceManager::MakeTemporaryFilePathForSave(const char* targetFilename)
  877. {
  878. AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
  879. AZ_Assert(fileIO, "File IO is not initialized.");
  880. AZStd::string devAssetPath = fileIO->GetAlias("@projectroot@");
  881. AZStd::string userPath = fileIO->GetAlias("@user@");
  882. AZStd::string tempPath = targetFilename;
  883. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::Bus::Events::NormalizePath, devAssetPath);
  884. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::Bus::Events::NormalizePath, userPath);
  885. AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::Bus::Events::NormalizePath, tempPath);
  886. AzFramework::StringFunc::Replace(tempPath, "@projectroot@", devAssetPath.c_str());
  887. AzFramework::StringFunc::Replace(tempPath, devAssetPath.c_str(), userPath.c_str());
  888. tempPath.append(".slicetemp");
  889. return tempPath;
  890. }
  891. //////////////////////////////////////////////////////////////////////////
  892. bool UiSliceManager::ConfirmDialog_Detach(const QString& title, const QString& text)
  893. {
  894. QMessageBox questionBox(QApplication::activeWindow());
  895. questionBox.setIcon(QMessageBox::Question);
  896. questionBox.setWindowTitle(title);
  897. questionBox.setText(text);
  898. QAbstractButton* detachButton = questionBox.addButton(QObject::tr("Detach"), QMessageBox::YesRole);
  899. questionBox.addButton(QObject::tr("Cancel"), QMessageBox::NoRole);
  900. questionBox.exec();
  901. return questionBox.clickedButton() == detachButton;
  902. }