PropertiesContainer.cpp 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  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 "CanvasHelpers.h"
  10. #include <AzQtComponents/Components/Style.h>
  11. #include <AzToolsFramework/Slice/SliceUtilities.h>
  12. #include <AzToolsFramework/Entity/EditorEntityInfoBus.h>
  13. #include <AzToolsFramework/ToolsComponents/EditorOnlyEntityComponent.h>
  14. #include <LyShine/Bus/UiSystemBus.h>
  15. #include <QApplication>
  16. #include <QBoxLayout>
  17. #include <QCheckBox>
  18. #include <QLineEdit>
  19. #include <QMouseEvent>
  20. #include <QPainter>
  21. #include <QContextMenuEvent>
  22. //-------------------------------------------------------------------------------
  23. //we require an overlay widget to act as a canvas to draw on top of everything in the inspector
  24. //attaching to inspector rather than component editors so we can draw outside of bounds
  25. class PropertyContainerOverlay : public QWidget
  26. {
  27. public:
  28. PropertyContainerOverlay(PropertiesContainer* editor, QWidget* parent)
  29. : QWidget(parent)
  30. , m_editor(editor)
  31. , m_dropIndicatorOffset(8)
  32. {
  33. setPalette(Qt::transparent);
  34. setWindowFlags(Qt::FramelessWindowHint);
  35. setAttribute(Qt::WA_NoSystemBackground);
  36. setAttribute(Qt::WA_TranslucentBackground);
  37. setAttribute(Qt::WA_TransparentForMouseEvents);
  38. }
  39. protected:
  40. void paintEvent(QPaintEvent* event) override
  41. {
  42. const int TopMargin = 1;
  43. const int RightMargin = 2;
  44. const int BottomMargin = 5;
  45. const int LeftMargin = 2;
  46. QWidget::paintEvent(event);
  47. QPainter painter(this);
  48. painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
  49. QRect currRect;
  50. bool drag = false;
  51. bool drop = false;
  52. for (auto componentEditor : m_editor->m_componentEditors)
  53. {
  54. if (!componentEditor->isVisible())
  55. {
  56. continue;
  57. }
  58. QRect globalRect = m_editor->GetWidgetGlobalRect(componentEditor);
  59. currRect = QRect(
  60. QPoint(mapFromGlobal(globalRect.topLeft()) + QPoint(LeftMargin, TopMargin)),
  61. QPoint(mapFromGlobal(globalRect.bottomRight()) - QPoint(RightMargin, BottomMargin))
  62. );
  63. currRect.setWidth(currRect.width() - 1);
  64. currRect.setHeight(currRect.height() - 1);
  65. if (componentEditor->IsDragged())
  66. {
  67. QStyleOption opt;
  68. opt.init(this);
  69. opt.rect = currRect;
  70. static_cast<AzQtComponents::Style*>(style())->drawDragIndicator(&opt, &painter, this);
  71. drag = true;
  72. }
  73. if (componentEditor->IsDropTarget())
  74. {
  75. QRect dropRect = currRect;
  76. dropRect.setTop(currRect.top() - m_dropIndicatorOffset);
  77. dropRect.setHeight(0);
  78. QStyleOption opt;
  79. opt.init(this);
  80. opt.rect = dropRect;
  81. style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &opt, &painter, this);
  82. drop = true;
  83. }
  84. }
  85. if (drag && !drop)
  86. {
  87. QRect dropRect = currRect;
  88. dropRect.setTop(currRect.top() - m_dropIndicatorOffset);
  89. dropRect.setHeight(0);
  90. QStyleOption opt;
  91. opt.init(this);
  92. opt.rect = dropRect;
  93. style()->drawPrimitive(QStyle::PE_IndicatorItemViewItemDrop, &opt, &painter, this);
  94. }
  95. }
  96. private:
  97. PropertiesContainer* m_editor;
  98. int m_dropIndicatorOffset;
  99. };
  100. //-------------------------------------------------------------------------------
  101. PropertiesContainer::PropertiesContainer(PropertiesWidget* propertiesWidget, EditorWindow* editorWindow)
  102. : QScrollArea(propertiesWidget)
  103. , m_propertiesWidget(propertiesWidget)
  104. , m_editorWindow(editorWindow)
  105. , m_selectedEntityDisplayNameWidget(nullptr)
  106. , m_selectionHasChanged(false)
  107. , m_isCanvasSelected(false)
  108. , m_selectionEventAccepted(false)
  109. , m_componentEditorLastSelectedIndex(-1)
  110. , m_serializeContext(nullptr)
  111. {
  112. setFocusPolicy(Qt::ClickFocus);
  113. setFrameShape(QFrame::NoFrame);
  114. setFrameShadow(QFrame::Plain);
  115. setLineWidth(0);
  116. setWidgetResizable(true);
  117. m_componentListContents = new QWidget();
  118. m_componentListContents->setGeometry(QRect(0, 0, 382, 537));
  119. QSizePolicy sizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
  120. sizePolicy.setHeightForWidth(m_componentListContents->sizePolicy().hasHeightForWidth());
  121. m_componentListContents->setSizePolicy(sizePolicy);
  122. m_rowLayout = new QVBoxLayout(m_componentListContents);
  123. m_rowLayout->setSpacing(10);
  124. m_rowLayout->setContentsMargins(0, 0, 0, 0);
  125. m_rowLayout->setAlignment(Qt::AlignTop);
  126. setWidget(m_componentListContents);
  127. m_overlay = new PropertyContainerOverlay(this, m_componentListContents);
  128. UpdateOverlay();
  129. CreateActions();
  130. // Get the serialize context.
  131. AZ::ComponentApplicationBus::BroadcastResult(m_serializeContext, &AZ::ComponentApplicationBus::Events::GetSerializeContext);
  132. AZ_Assert(m_serializeContext, "We should have a valid context!");
  133. QObject::connect(m_editorWindow->GetHierarchy(),
  134. &HierarchyWidget::editorOnlyStateChangedOnSelectedElements,
  135. [this]()
  136. {
  137. UpdateEditorOnlyCheckbox();
  138. });
  139. }
  140. void PropertiesContainer::resizeEvent(QResizeEvent* event)
  141. {
  142. QScrollArea::resizeEvent(event);
  143. UpdateOverlay();
  144. }
  145. void PropertiesContainer::contextMenuEvent(QContextMenuEvent* event)
  146. {
  147. OnDisplayUiComponentEditorMenu(event->globalPos());
  148. event->accept();
  149. }
  150. //overridden to intercept application level mouse events for component editor selection
  151. bool PropertiesContainer::eventFilter(QObject* object, QEvent* event)
  152. {
  153. HandleSelectionEvents(object, event);
  154. return false;
  155. }
  156. bool PropertiesContainer::HandleSelectionEvents(QObject* object, QEvent* event)
  157. {
  158. (void)object;
  159. if (m_selectedEntities.empty())
  160. {
  161. return false;
  162. }
  163. if (event->type() != QEvent::MouseButtonPress &&
  164. event->type() != QEvent::MouseButtonDblClick &&
  165. event->type() != QEvent::MouseButtonRelease)
  166. {
  167. return false;
  168. }
  169. QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
  170. //selection now occurs on mouse released
  171. //reset selection flag when mouse is clicked to allow additional selection changes
  172. if (event->type() == QEvent::MouseButtonPress)
  173. {
  174. m_selectionEventAccepted = false;
  175. return false;
  176. }
  177. //reject input if selection already occurred for this click
  178. if (m_selectionEventAccepted)
  179. {
  180. return false;
  181. }
  182. //reject input if a popup or modal window is active
  183. if (QApplication::activeModalWidget() != nullptr ||
  184. QApplication::activePopupWidget() != nullptr)
  185. {
  186. return false;
  187. }
  188. const QRect globalRect(mouseEvent->globalPos(), mouseEvent->globalPos());
  189. //reject input outside of the inspector's component list
  190. if (!DoesOwnFocus() ||
  191. !DoesIntersectWidget(globalRect, this))
  192. {
  193. return false;
  194. }
  195. //reject input from other buttons
  196. if ((mouseEvent->button() != Qt::LeftButton) &&
  197. (mouseEvent->button() != Qt::RightButton))
  198. {
  199. return false;
  200. }
  201. //right click is allowed if the component editor under the mouse is not selected
  202. if (mouseEvent->button() == Qt::RightButton)
  203. {
  204. if (DoesIntersectSelectedComponentEditor(globalRect))
  205. {
  206. return false;
  207. }
  208. ClearComponentEditorSelection();
  209. SelectIntersectingComponentEditors(globalRect, true);
  210. }
  211. else if (mouseEvent->button() == Qt::LeftButton)
  212. {
  213. //if shift or control is pressed this is a multi=-select operation, otherwise reset the selection
  214. if (mouseEvent->modifiers() & Qt::ControlModifier)
  215. {
  216. ToggleIntersectingComponentEditors(globalRect);
  217. }
  218. else if (mouseEvent->modifiers() & Qt::ShiftModifier)
  219. {
  220. ComponentEditorVector intersections = GetIntersectingComponentEditors(globalRect);
  221. if (!intersections.empty())
  222. {
  223. SelectRangeOfComponentEditors(m_componentEditorLastSelectedIndex, GetComponentEditorIndex(intersections.front()), true);
  224. }
  225. }
  226. else
  227. {
  228. ClearComponentEditorSelection();
  229. SelectIntersectingComponentEditors(globalRect, true);
  230. }
  231. }
  232. UpdateInternalState();
  233. //ensure selection logic executes only once per click since eventFilter may execute multiple times for a single click
  234. m_selectionEventAccepted = true;
  235. return true;
  236. }
  237. AZ::Entity::ComponentArrayType PropertiesContainer::GetSelectedComponents()
  238. {
  239. ComponentEditorVector selectedComponentEditors;
  240. selectedComponentEditors.reserve(m_componentEditors.size());
  241. for (auto componentEditor : m_componentEditors)
  242. {
  243. if (componentEditor->isVisible() && componentEditor->IsSelected())
  244. {
  245. selectedComponentEditors.push_back(componentEditor);
  246. }
  247. }
  248. AZ::Entity::ComponentArrayType selectedComponents;
  249. for (auto componentEditor : selectedComponentEditors)
  250. {
  251. const auto& components = componentEditor->GetComponents();
  252. selectedComponents.insert(selectedComponents.end(), components.begin(), components.end());
  253. }
  254. return selectedComponents;
  255. }
  256. void PropertiesContainer::BuildSharedComponentList(ComponentTypeMap& sharedComponentsByType, const AzToolsFramework::EntityIdList& entitiesShown)
  257. {
  258. // For single selection of a slice-instanced entity, gather the direct slice ancestor
  259. // so we can visualize per-component differences.
  260. m_compareToEntity.reset();
  261. if (1 == entitiesShown.size())
  262. {
  263. AZ::SliceComponent::SliceInstanceAddress address;
  264. AzFramework::SliceEntityRequestBus::EventResult(address, entitiesShown[0],
  265. &AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
  266. if (address.IsValid())
  267. {
  268. AZ::SliceComponent::EntityAncestorList ancestors;
  269. address.GetReference()->GetInstanceEntityAncestry(entitiesShown[0], ancestors, 1);
  270. if (!ancestors.empty())
  271. {
  272. m_compareToEntity = AzToolsFramework::SliceUtilities::CloneSliceEntityForComparison(*ancestors[0].m_entity, *address.GetInstance(), *m_serializeContext);
  273. }
  274. }
  275. }
  276. // Create a SharedComponentInfo for each component
  277. // that selected entities have in common.
  278. // See comments on SharedComponentInfo for more details
  279. for (AZ::EntityId entityId : entitiesShown)
  280. {
  281. AZ::Entity* entity = nullptr;
  282. AZ::ComponentApplicationBus::BroadcastResult(entity, &AZ::ComponentApplicationBus::Events::FindEntity, entityId);
  283. AZ_Assert(entity, "Entity was selected but no such entity exists?");
  284. if (!entity)
  285. {
  286. continue;
  287. }
  288. // Track how many of each component-type we've seen on this entity
  289. AZStd::unordered_map<AZ::Uuid, size_t> entityComponentCounts;
  290. for (AZ::Component* component : entity->GetComponents())
  291. {
  292. const AZ::Uuid& componentType = azrtti_typeid(component);
  293. const AZ::SerializeContext::ClassData* classData = m_serializeContext->FindClassData(componentType);
  294. // Skip components without edit data
  295. if (!classData || !classData->m_editData)
  296. {
  297. continue;
  298. }
  299. // Skip components that are set to invisible.
  300. if (const AZ::Edit::ElementData* editorDataElement = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData))
  301. {
  302. if (AZ::Edit::Attribute* visibilityAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::Visibility))
  303. {
  304. AzToolsFramework::PropertyAttributeReader reader(component, visibilityAttribute);
  305. AZ::u32 visibilityValue;
  306. if (reader.Read<AZ::u32>(visibilityValue))
  307. {
  308. if (visibilityValue == AZ_CRC_CE("PropertyVisibility_Hide"))
  309. {
  310. continue;
  311. }
  312. }
  313. }
  314. }
  315. // The sharedComponentList is created based on the first entity.
  316. if (entityId == *entitiesShown.begin())
  317. {
  318. // Add new SharedComponentInfo
  319. SharedComponentInfo sharedComponent;
  320. sharedComponent.m_classData = classData;
  321. sharedComponentsByType[componentType].push_back(sharedComponent);
  322. }
  323. // Skip components that don't correspond to a type from the first entity.
  324. if (sharedComponentsByType.find(componentType) == sharedComponentsByType.end())
  325. {
  326. continue;
  327. }
  328. // Update entityComponentCounts (may be multiple components of this type)
  329. auto entityComponentCountsIt = entityComponentCounts.find(componentType);
  330. size_t componentIndex = (entityComponentCountsIt == entityComponentCounts.end())
  331. ? 0
  332. : entityComponentCountsIt->second;
  333. entityComponentCounts[componentType] = componentIndex + 1;
  334. // Skip component if the first entity didn't have this many.
  335. if (componentIndex >= sharedComponentsByType[componentType].size())
  336. {
  337. continue;
  338. }
  339. // Component accepted! Add it as an instance
  340. SharedComponentInfo& sharedComponent = sharedComponentsByType[componentType][componentIndex];
  341. sharedComponent.m_instances.push_back(component);
  342. // If specified, locate the corresponding component in the comparison entity to
  343. // visualize differences.
  344. if (m_compareToEntity && !sharedComponent.m_compareInstance)
  345. {
  346. size_t compareComponentIndex = 0;
  347. for (AZ::Component* compareComponent : m_compareToEntity.get()->GetComponents())
  348. {
  349. const AZ::Uuid& compareComponentType = azrtti_typeid(compareComponent);
  350. if (componentType == compareComponentType)
  351. {
  352. if (componentIndex == compareComponentIndex)
  353. {
  354. sharedComponent.m_compareInstance = compareComponent;
  355. break;
  356. }
  357. compareComponentIndex++;
  358. }
  359. }
  360. }
  361. }
  362. }
  363. // Cull any SharedComponentInfo that doesn't fit all our requirements
  364. ComponentTypeMap::iterator sharedComponentsByTypeIterator = sharedComponentsByType.begin();
  365. while (sharedComponentsByTypeIterator != sharedComponentsByType.end())
  366. {
  367. AZStd::vector<SharedComponentInfo>& sharedComponents = sharedComponentsByTypeIterator->second;
  368. // Remove component if it doesn't exist on every entity
  369. AZStd::vector<SharedComponentInfo>::iterator sharedComponentIterator = sharedComponents.begin();
  370. while (sharedComponentIterator != sharedComponents.end())
  371. {
  372. if (sharedComponentIterator->m_instances.size() != entitiesShown.size()
  373. || sharedComponentIterator->m_instances.empty())
  374. {
  375. sharedComponentIterator = sharedComponents.erase(sharedComponentIterator);
  376. }
  377. else
  378. {
  379. ++sharedComponentIterator;
  380. }
  381. }
  382. // Remove entry if all its components were culled
  383. if (sharedComponents.size() == 0)
  384. {
  385. sharedComponentsByTypeIterator = sharedComponentsByType.erase(sharedComponentsByTypeIterator);
  386. }
  387. else
  388. {
  389. ++sharedComponentsByTypeIterator;
  390. }
  391. }
  392. }
  393. void PropertiesContainer::BuildSharedComponentUI(ComponentTypeMap& sharedComponentsByType, const AzToolsFramework::EntityIdList& entitiesShown)
  394. {
  395. (void)entitiesShown;
  396. // At this point in time:
  397. // - Each SharedComponentInfo should contain one component instance
  398. // from each selected entity.
  399. // - Any pre-existing m_componentEditor entries should be
  400. // cleared of component instances.
  401. // Add each component instance to its corresponding editor
  402. // We add them in the order that the component factories were registered in, this provides
  403. // a consistent order of components. It doesn't appear to be the case that components always
  404. // stay in the order they were added to the entity in, some of our slices do not have the
  405. // UiElementComponent first for example.
  406. const AZStd::vector<AZ::Uuid>* componentTypes;
  407. UiSystemBus::BroadcastResult(componentTypes, &UiSystemBus::Events::GetComponentTypesForMenuOrdering);
  408. // There could be components that were not registered for component ordering. We don't
  409. // want to hide them. So add them at the end of the list
  410. AZStd::vector<AZ::Uuid> componentOrdering;
  411. componentOrdering = *componentTypes;
  412. for (auto sharedComponentMapEntry : sharedComponentsByType)
  413. {
  414. if (AZStd::find(componentOrdering.begin(), componentOrdering.end(), sharedComponentMapEntry.first) == componentOrdering.end())
  415. {
  416. componentOrdering.push_back(sharedComponentMapEntry.first);
  417. }
  418. }
  419. m_componentEditors.clear();
  420. for (auto& componentType : componentOrdering)
  421. {
  422. if (sharedComponentsByType.count(componentType) <= 0)
  423. {
  424. continue; // there are no components of this type in the sharedComponentsByType map
  425. }
  426. const auto& sharedComponents = sharedComponentsByType[componentType];
  427. for (size_t sharedComponentIndex = 0; sharedComponentIndex < sharedComponents.size(); ++sharedComponentIndex)
  428. {
  429. const SharedComponentInfo& sharedComponent = sharedComponents[sharedComponentIndex];
  430. AZ_Assert(sharedComponent.m_instances.size() == entitiesShown.size()
  431. && !sharedComponent.m_instances.empty(),
  432. "sharedComponentsByType should only contain valid entries at this point");
  433. // Create an editor if necessary
  434. ComponentEditorVector& componentEditors = m_componentEditorsByType[componentType];
  435. if (sharedComponentIndex >= componentEditors.size())
  436. {
  437. componentEditors.push_back(CreateComponentEditor(*sharedComponent.m_instances[0]));
  438. }
  439. else
  440. {
  441. // Place existing editor in correct order
  442. m_rowLayout->removeWidget(componentEditors[sharedComponentIndex]);
  443. m_rowLayout->addWidget(componentEditors[sharedComponentIndex]);
  444. }
  445. AzToolsFramework::ComponentEditor* componentEditor = componentEditors[sharedComponentIndex];
  446. // Save a list of components in order shown
  447. m_componentEditors.push_back(componentEditor);
  448. // Add instances to componentEditor
  449. auto& componentInstances = sharedComponent.m_instances;
  450. for (AZ::Component* componentInstance : componentInstances)
  451. {
  452. // non-first instances are aggregated under the first instance
  453. AZ::Component* aggregateInstance = componentInstance != componentInstances.front() ? componentInstances.front() : nullptr;
  454. // Reference the slice entity if we are a slice so we can indicate differences from base
  455. AZ::Component* compareInstance = sharedComponent.m_compareInstance;
  456. componentEditor->AddInstance(componentInstance, aggregateInstance, compareInstance);
  457. }
  458. // Refresh editor
  459. componentEditor->InvalidateAll();
  460. componentEditor->show();
  461. }
  462. }
  463. }
  464. AzToolsFramework::ComponentEditor* PropertiesContainer::CreateComponentEditor([[maybe_unused]] const AZ::Component& componentInstance)
  465. {
  466. AzToolsFramework::ComponentEditor* editor = new AzToolsFramework::ComponentEditor(m_serializeContext, m_propertiesWidget, this);
  467. connect(editor, &AzToolsFramework::ComponentEditor::OnDisplayComponentEditorMenu, this, &PropertiesContainer::OnDisplayUiComponentEditorMenu);
  468. m_rowLayout->addWidget(editor);
  469. editor->hide();
  470. return editor;
  471. }
  472. bool PropertiesContainer::DoesOwnFocus() const
  473. {
  474. QWidget* widget = QApplication::focusWidget();
  475. return this == widget || isAncestorOf(widget);
  476. }
  477. QRect PropertiesContainer::GetWidgetGlobalRect(const QWidget* widget) const
  478. {
  479. return QRect(
  480. widget->mapToGlobal(widget->rect().topLeft()),
  481. widget->mapToGlobal(widget->rect().bottomRight()));
  482. }
  483. bool PropertiesContainer::DoesIntersectWidget(const QRect& globalRect, const QWidget* widget) const
  484. {
  485. return widget->isVisible() && globalRect.intersects(GetWidgetGlobalRect(widget));
  486. }
  487. bool PropertiesContainer::DoesIntersectSelectedComponentEditor(const QRect& globalRect) const
  488. {
  489. for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
  490. {
  491. if (componentEditor->IsSelected())
  492. {
  493. return true;
  494. }
  495. }
  496. return false;
  497. }
  498. bool PropertiesContainer::DoesIntersectNonSelectedComponentEditor(const QRect& globalRect) const
  499. {
  500. for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
  501. {
  502. if (!componentEditor->IsSelected())
  503. {
  504. return true;
  505. }
  506. }
  507. return false;
  508. }
  509. void PropertiesContainer::ClearComponentEditorSelection()
  510. {
  511. AZ_PROFILE_FUNCTION(AzToolsFramework);
  512. for (auto componentEditor : m_componentEditors)
  513. {
  514. componentEditor->SetSelected(false);
  515. }
  516. UpdateInternalState();
  517. }
  518. void PropertiesContainer::SelectRangeOfComponentEditors(const AZ::s32 index1, const AZ::s32 index2, bool selected)
  519. {
  520. if (index1 >= 0 && index2 >= 0)
  521. {
  522. const AZ::s32 min = AZStd::min(index1, index2);
  523. const AZ::s32 max = AZStd::max(index1, index2);
  524. for (AZ::s32 index = min; index <= max; ++index)
  525. {
  526. m_componentEditors[index]->SetSelected(selected);
  527. }
  528. m_componentEditorLastSelectedIndex = index2;
  529. }
  530. UpdateInternalState();
  531. }
  532. void PropertiesContainer::SelectIntersectingComponentEditors(const QRect& globalRect, bool selected)
  533. {
  534. for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
  535. {
  536. componentEditor->SetSelected(selected);
  537. m_componentEditorLastSelectedIndex = GetComponentEditorIndex(componentEditor);
  538. }
  539. UpdateInternalState();
  540. }
  541. void PropertiesContainer::ToggleIntersectingComponentEditors(const QRect& globalRect)
  542. {
  543. for (auto componentEditor : GetIntersectingComponentEditors(globalRect))
  544. {
  545. componentEditor->SetSelected(!componentEditor->IsSelected());
  546. m_componentEditorLastSelectedIndex = GetComponentEditorIndex(componentEditor);
  547. }
  548. UpdateInternalState();
  549. }
  550. AZ::s32 PropertiesContainer::GetComponentEditorIndex(const AzToolsFramework::ComponentEditor* componentEditor) const
  551. {
  552. AZ::s32 index = 0;
  553. for (auto componentEditorToCompare : m_componentEditors)
  554. {
  555. if (componentEditorToCompare == componentEditor)
  556. {
  557. return index;
  558. }
  559. ++index;
  560. }
  561. return -1;
  562. }
  563. AZStd::vector<AzToolsFramework::ComponentEditor*> PropertiesContainer::GetIntersectingComponentEditors(const QRect& globalRect) const
  564. {
  565. ComponentEditorVector intersectingComponentEditors;
  566. intersectingComponentEditors.reserve(m_componentEditors.size());
  567. for (auto componentEditor : m_componentEditors)
  568. {
  569. if (DoesIntersectWidget(globalRect, componentEditor))
  570. {
  571. intersectingComponentEditors.push_back(componentEditor);
  572. }
  573. }
  574. return intersectingComponentEditors;
  575. }
  576. void PropertiesContainer::CreateActions()
  577. {
  578. QAction* seperator1 = new QAction(this);
  579. seperator1->setSeparator(true);
  580. addAction(seperator1);
  581. m_actionToAddComponents = new QAction(tr("Add component"), this);
  582. m_actionToAddComponents->setShortcutContext(Qt::WidgetWithChildrenShortcut);
  583. connect(m_actionToAddComponents, &QAction::triggered, this, &PropertiesContainer::OnAddComponent);
  584. addAction(m_actionToAddComponents);
  585. m_actionToDeleteComponents = ComponentHelpers::CreateRemoveComponentsAction(this);
  586. addAction(m_actionToDeleteComponents);
  587. QAction* seperator2 = new QAction(this);
  588. seperator2->setSeparator(true);
  589. addAction(seperator2);
  590. m_actionToCutComponents = ComponentHelpers::CreateCutComponentsAction(this);
  591. addAction(m_actionToCutComponents);
  592. m_actionToCopyComponents = ComponentHelpers::CreateCopyComponentsAction(this);
  593. addAction(m_actionToCopyComponents);
  594. m_actionToPasteComponents = ComponentHelpers::CreatePasteComponentsAction(this);
  595. addAction(m_actionToPasteComponents);
  596. UpdateInternalState();
  597. }
  598. void PropertiesContainer::UpdateActions()
  599. {
  600. ComponentHelpers::UpdateRemoveComponentsAction(m_actionToDeleteComponents);
  601. ComponentHelpers::UpdateCutComponentsAction(m_actionToCutComponents);
  602. ComponentHelpers::UpdateCopyComponentsAction(m_actionToCopyComponents);
  603. // The paste components action always remains enabled except for when the context menu is up
  604. // This is so a paste can be performed after a copy operation was made via the shortcut keys (since we don't know when a copy was performed)
  605. }
  606. void PropertiesContainer::UpdateOverlay()
  607. {
  608. m_overlay->setVisible(true);
  609. m_overlay->setGeometry(m_componentListContents->rect());
  610. m_overlay->raise();
  611. m_overlay->update();
  612. }
  613. void PropertiesContainer::UpdateInternalState()
  614. {
  615. UpdateActions();
  616. UpdateOverlay();
  617. }
  618. void PropertiesContainer::OnAddComponent()
  619. {
  620. HierarchyMenu contextMenu(m_editorWindow->GetHierarchy(),
  621. HierarchyMenu::Show::kAddComponents,
  622. true);
  623. contextMenu.exec(QCursor::pos());
  624. }
  625. void PropertiesContainer::OnDisplayUiComponentEditorMenu(const QPoint& position)
  626. {
  627. ShowContextMenu(position);
  628. }
  629. void PropertiesContainer::ShowContextMenu(const QPoint& position)
  630. {
  631. UpdateInternalState();
  632. // The paste components action is only updated right before the context menu appears, otherwise it remains enabled
  633. ComponentHelpers::UpdatePasteComponentsAction(m_actionToPasteComponents);
  634. HierarchyMenu contextMenu(m_editorWindow->GetHierarchy(),
  635. HierarchyMenu::Show::kPushToSlice,
  636. false);
  637. contextMenu.addActions(actions());
  638. if (!contextMenu.isEmpty())
  639. {
  640. contextMenu.exec(position);
  641. }
  642. // Keep the paste components action enabled when the context menu is not showing in order to handle pastes after a copy was performed
  643. m_actionToPasteComponents->setEnabled(true);
  644. }
  645. void PropertiesContainer::Update()
  646. {
  647. size_t selectedEntitiesAmount = m_selectedEntities.size();
  648. QString displayName;
  649. if (selectedEntitiesAmount == 0)
  650. {
  651. displayName = "No Canvas Loaded";
  652. }
  653. else if (selectedEntitiesAmount == 1)
  654. {
  655. // Either only one element selected, or none (still is 1 because it selects the canvas instead)
  656. // If the canvas was selected
  657. if (m_isCanvasSelected)
  658. {
  659. displayName = "Canvas";
  660. }
  661. // Else one element was selected
  662. else
  663. {
  664. // Set the name in the properties pane to the name of the element
  665. AZ::EntityId selectedElement = m_selectedEntities.front();
  666. AZStd::string selectedEntityName;
  667. UiElementBus::EventResult(selectedEntityName, selectedElement, &UiElementBus::Events::GetName);
  668. displayName = selectedEntityName.c_str();
  669. }
  670. }
  671. else // more than one entity selected
  672. {
  673. displayName = QString::number(selectedEntitiesAmount) + " elements selected";
  674. }
  675. // Update the selected element display name
  676. if (m_selectedEntityDisplayNameWidget != nullptr)
  677. {
  678. m_selectedEntityDisplayNameWidget->setText(displayName);
  679. // Preventing renaming entities if the canvas entity is selected or
  680. // multiple entities are selected.
  681. if (m_isCanvasSelected || selectedEntitiesAmount > 1)
  682. {
  683. m_selectedEntityDisplayNameWidget->setEnabled(false);
  684. }
  685. else
  686. {
  687. m_selectedEntityDisplayNameWidget->setEnabled(true);
  688. }
  689. }
  690. // Clear content.
  691. {
  692. for (int j = m_rowLayout->count(); j > 0; --j)
  693. {
  694. AzToolsFramework::ComponentEditor* editor = static_cast<AzToolsFramework::ComponentEditor*>(m_rowLayout->itemAt(j - 1)->widget());
  695. editor->hide();
  696. editor->ClearInstances(true);
  697. }
  698. m_compareToEntity.reset();
  699. }
  700. UpdateEditorOnlyCheckbox();
  701. if (m_selectedEntities.empty())
  702. {
  703. return; // nothing to do
  704. }
  705. ComponentTypeMap sharedComponentList;
  706. BuildSharedComponentList(sharedComponentList, m_selectedEntities);
  707. BuildSharedComponentUI(sharedComponentList, m_selectedEntities);
  708. UpdateInternalState();
  709. }
  710. void PropertiesContainer::UpdateEditorOnlyCheckbox()
  711. {
  712. if (m_isCanvasSelected)
  713. {
  714. // The canvas itself can't be editor-only, so don't show the checkbox when the
  715. // canvas is displayed in the properties pane.
  716. m_editorOnlyCheckbox->setVisible(false);
  717. }
  718. else
  719. {
  720. QSignalBlocker noSignals(m_editorOnlyCheckbox);
  721. if (m_selectedEntities.empty())
  722. {
  723. m_editorOnlyCheckbox->setVisible(false);
  724. return;
  725. }
  726. m_editorOnlyCheckbox->setVisible(true);
  727. bool allEditorOnly = true;
  728. bool noneEditorOnly = true;
  729. for (AZ::EntityId id : m_selectedEntities)
  730. {
  731. // If any of the entities is a slice root, grey out the checkbox.
  732. bool isSliceRoot = false;
  733. AzToolsFramework::EditorEntityInfoRequestBus::EventResult(isSliceRoot, id, &AzToolsFramework::EditorEntityInfoRequestBus::Events::IsSliceRoot);
  734. if (isSliceRoot)
  735. {
  736. m_editorOnlyCheckbox->setChecked(false);
  737. m_editorOnlyCheckbox->setEnabled(false);
  738. return;
  739. }
  740. bool isEditorOnly = false;
  741. AzToolsFramework::EditorOnlyEntityComponentRequestBus::EventResult(isEditorOnly, id, &AzToolsFramework::EditorOnlyEntityComponentRequests::IsEditorOnlyEntity);
  742. allEditorOnly &= isEditorOnly;
  743. noneEditorOnly &= !isEditorOnly;
  744. }
  745. m_editorOnlyCheckbox->setEnabled(true);
  746. if (allEditorOnly)
  747. {
  748. m_editorOnlyCheckbox->setCheckState(Qt::CheckState::Checked);
  749. }
  750. else if (noneEditorOnly)
  751. {
  752. m_editorOnlyCheckbox->setCheckState(Qt::CheckState::Unchecked);
  753. }
  754. else // Some marked editor-only, some not
  755. {
  756. m_editorOnlyCheckbox->setCheckState(Qt::CheckState::PartiallyChecked);
  757. }
  758. }
  759. }
  760. void PropertiesContainer::Refresh(AzToolsFramework::PropertyModificationRefreshLevel refreshLevel, const AZ::Uuid* componentType)
  761. {
  762. if (m_selectionHasChanged)
  763. {
  764. Update();
  765. m_selectionHasChanged = false;
  766. }
  767. else
  768. {
  769. for (auto& componentEditorsPair : m_componentEditorsByType)
  770. {
  771. if (!componentType || (*componentType == componentEditorsPair.first))
  772. {
  773. for (AzToolsFramework::ComponentEditor* editor : componentEditorsPair.second)
  774. {
  775. if (editor->isVisible())
  776. {
  777. editor->QueuePropertyEditorInvalidation(refreshLevel);
  778. }
  779. }
  780. }
  781. }
  782. // If the selection has not changed, but a refresh was prompted then the name of the currently selected entity might
  783. // have changed.
  784. size_t selectedEntitiesAmount = m_selectedEntities.size();
  785. // Check if only one entity is selected and that it is an element
  786. if (selectedEntitiesAmount == 1 && !m_isCanvasSelected)
  787. {
  788. // Set the name in the properties pane to the name of the element
  789. AZ::EntityId selectedElement = m_selectedEntities.front();
  790. AZStd::string selectedEntityName;
  791. UiElementBus::EventResult(selectedEntityName, selectedElement, &UiElementBus::Events::GetName);
  792. // Update the selected element display name
  793. if (m_selectedEntityDisplayNameWidget != nullptr)
  794. {
  795. m_selectedEntityDisplayNameWidget->setText(selectedEntityName.c_str());
  796. }
  797. }
  798. }
  799. }
  800. void PropertiesContainer::SelectionChanged(HierarchyItemRawPtrList* items)
  801. {
  802. ClearComponentEditorSelection();
  803. m_selectedEntities.clear();
  804. if (items)
  805. {
  806. for (auto i : *items)
  807. {
  808. m_selectedEntities.push_back(i->GetEntityId());
  809. }
  810. }
  811. m_isCanvasSelected = false;
  812. if (m_selectedEntities.empty())
  813. {
  814. // Add the canvas
  815. AZ::EntityId canvasId = m_editorWindow->GetCanvas();
  816. if (canvasId.IsValid())
  817. {
  818. m_selectedEntities.push_back(canvasId);
  819. m_isCanvasSelected = true;
  820. }
  821. }
  822. m_selectionHasChanged = true;
  823. }
  824. void PropertiesContainer::SelectedEntityPointersChanged()
  825. {
  826. m_selectionHasChanged = true;
  827. Refresh();
  828. }
  829. void PropertiesContainer::RequestPropertyContextMenu([[maybe_unused]] AzToolsFramework::InstanceDataNode* node, const QPoint& globalPos)
  830. {
  831. ShowContextMenu(globalPos);
  832. }
  833. void PropertiesContainer::SetSelectedEntityDisplayNameWidget(QLineEdit* selectedEntityDisplayNameWidget)
  834. {
  835. if (selectedEntityDisplayNameWidget)
  836. {
  837. if (m_selectedEntityDisplayNameWidget)
  838. {
  839. QObject::disconnect(m_selectedEntityDisplayNameWidget);
  840. }
  841. m_selectedEntityDisplayNameWidget = selectedEntityDisplayNameWidget;
  842. // Listen for changes to the line edit field
  843. QObject::connect(m_selectedEntityDisplayNameWidget,
  844. &QLineEdit::editingFinished,
  845. [this]()
  846. {
  847. // Ignore editing when this field is read-only or if there is more than one
  848. // entity selected.
  849. if (!m_selectedEntityDisplayNameWidget->isEnabled() || m_selectedEntities.size() != 1)
  850. {
  851. return;
  852. }
  853. AZ::EntityId selectedEntityId = m_selectedEntities.front();
  854. AZ::Entity* selectedEntity = nullptr;
  855. AZ::ComponentApplicationBus::BroadcastResult(
  856. selectedEntity, &AZ::ComponentApplicationBus::Events::FindEntity, selectedEntityId);
  857. if (selectedEntity)
  858. {
  859. AZStd::string currentName = selectedEntity->GetName();
  860. AZStd::string newName = m_selectedEntityDisplayNameWidget->text().toUtf8().constData();
  861. CommandHierarchyItemRename::Push(m_editorWindow->GetActiveStack(),
  862. m_editorWindow->GetHierarchy(),
  863. selectedEntityId,
  864. currentName.c_str(),
  865. newName.c_str());
  866. }
  867. });
  868. }
  869. }
  870. void PropertiesContainer::SetEditorOnlyCheckbox(QCheckBox* editorOnlyCheckbox)
  871. {
  872. m_editorOnlyCheckbox = editorOnlyCheckbox;
  873. QObject::connect(m_editorOnlyCheckbox,
  874. &QCheckBox::stateChanged,
  875. [this](int value)
  876. {
  877. QSignalBlocker blocker(this);
  878. QMetaObject::invokeMethod(m_editorWindow->GetHierarchy(), "SetEditorOnlyForSelectedItems", Qt::QueuedConnection, Q_ARG(bool, value));
  879. }
  880. );
  881. }
  882. #include <moc_PropertiesContainer.cpp>