3
0

UiDropdownComponent.cpp 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058
  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 "UiDropdownComponent.h"
  9. #include <AzCore/Serialization/SerializeContext.h>
  10. #include <AzCore/Serialization/EditContext.h>
  11. #include <AzCore/RTTI/BehaviorContext.h>
  12. #include <LyShine/Bus/UiTransformBus.h>
  13. #include <LyShine/Bus/UiElementBus.h>
  14. #include <LyShine/Bus/UiTextBus.h>
  15. #include <LyShine/Bus/UiImageBus.h>
  16. #include <LyShine/Bus/UiTransform2dBus.h>
  17. #include <LyShine/Bus/UiDropdownOptionBus.h>
  18. #include <LyShine/UiSerializeHelpers.h>
  19. #include "UiNavigationHelpers.h"
  20. ////////////////////////////////////////////////////////////////////////////////////////////////////
  21. //! UiDropdownNotificationBusBehaviorHandler Behavior context handler class
  22. class UiDropdownNotificationBusBehaviorHandler
  23. : public UiDropdownNotificationBus::Handler
  24. , public AZ::BehaviorEBusHandler
  25. {
  26. public:
  27. AZ_EBUS_BEHAVIOR_BINDER(UiDropdownNotificationBusBehaviorHandler, "{C936F190-524E-410E-82C9-9B590015B6D5}", AZ::SystemAllocator,
  28. OnDropdownExpanded, OnDropdownCollapsed, OnDropdownValueChanged);
  29. void OnDropdownExpanded() override
  30. {
  31. Call(FN_OnDropdownExpanded);
  32. }
  33. void OnDropdownCollapsed() override
  34. {
  35. Call(FN_OnDropdownCollapsed);
  36. }
  37. void OnDropdownValueChanged(AZ::EntityId value) override
  38. {
  39. Call(FN_OnDropdownValueChanged, value);
  40. }
  41. };
  42. ////////////////////////////////////////////////////////////////////////////////////////////////////
  43. // PUBLIC MEMBER FUNCTIONS
  44. ////////////////////////////////////////////////////////////////////////////////////////////////////
  45. ////////////////////////////////////////////////////////////////////////////////////////////////////
  46. UiDropdownComponent::UiDropdownComponent()
  47. : m_value()
  48. , m_content()
  49. , m_expandOnHover(false)
  50. , m_waitTime(0.3f)
  51. , m_collapseOnOutsideClick(true)
  52. , m_expandedParentId()
  53. , m_textElement()
  54. , m_iconElement()
  55. , m_expandedActionName()
  56. , m_collapsedActionName()
  57. , m_optionSelectedActionName()
  58. , m_expanded(false)
  59. , m_canvasEntityId()
  60. , m_delayTimer(0.f)
  61. , m_baseParent()
  62. , m_expandedByClick(true)
  63. {
  64. m_stateActionManager.AddState(&m_expandedStateActions);
  65. }
  66. ////////////////////////////////////////////////////////////////////////////////////////////////////
  67. UiDropdownComponent::~UiDropdownComponent()
  68. {
  69. // delete all the state actions now rather than letting the base class do it automatically
  70. // because the m_stateActionManager has pointers to members in this derived class.
  71. m_stateActionManager.ClearStates();
  72. }
  73. ////////////////////////////////////////////////////////////////////////////////////////////////////
  74. AZ::EntityId UiDropdownComponent::GetValue()
  75. {
  76. return m_value;
  77. }
  78. ////////////////////////////////////////////////////////////////////////////////////////////////////
  79. void UiDropdownComponent::SetValue(AZ::EntityId value)
  80. {
  81. m_value = value;
  82. // Get the text from the newly selection option
  83. AZ::EntityId optionText;
  84. UiDropdownOptionBus::EventResult(optionText, value, &UiDropdownOptionBus::Events::GetTextElement);
  85. if (optionText.IsValid())
  86. {
  87. AZStd::string text;
  88. UiTextBus::EventResult(text, optionText, &UiTextBus::Events::GetText);
  89. // Set our text to that text to show which option was selected
  90. UiTextBus::Event(m_textElement, &UiTextBus::Events::SetTextWithFlags, text, UiTextInterface::SetTextFlags::SetLocalized);
  91. }
  92. // Get the icon from the newly selection option
  93. AZ::EntityId optionIcon;
  94. UiDropdownOptionBus::EventResult(optionIcon, value, &UiDropdownOptionBus::Events::GetIconElement);
  95. if (optionIcon.IsValid())
  96. {
  97. ISprite* sprite;
  98. UiImageBus::EventResult(sprite, optionIcon, &UiImageBus::Events::GetSprite);
  99. // Set our icon to that icon to show which option was selected
  100. UiImageBus::Event(m_iconElement, &UiImageBus::Events::SetSprite, sprite);
  101. }
  102. if (!m_optionSelectedActionName.empty())
  103. {
  104. AZ::EntityId canvasEntityId;
  105. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  106. UiCanvasNotificationBus::Event(
  107. canvasEntityId, &UiCanvasNotificationBus::Events::OnAction, GetEntityId(), m_optionSelectedActionName);
  108. }
  109. UiDropdownNotificationBus::Event(GetEntityId(), &UiDropdownNotificationBus::Events::OnDropdownValueChanged, value);
  110. }
  111. ////////////////////////////////////////////////////////////////////////////////////////////////////
  112. AZ::EntityId UiDropdownComponent::GetContent()
  113. {
  114. return m_content;
  115. }
  116. ////////////////////////////////////////////////////////////////////////////////////////////////////
  117. void UiDropdownComponent::SetContent(AZ::EntityId content)
  118. {
  119. m_content = content;
  120. }
  121. ////////////////////////////////////////////////////////////////////////////////////////////////////
  122. bool UiDropdownComponent::GetExpandOnHover()
  123. {
  124. return m_expandOnHover;
  125. }
  126. ////////////////////////////////////////////////////////////////////////////////////////////////////
  127. void UiDropdownComponent::SetExpandOnHover(bool expandOnHover)
  128. {
  129. m_expandOnHover = expandOnHover;
  130. }
  131. ////////////////////////////////////////////////////////////////////////////////////////////////////
  132. float UiDropdownComponent::GetWaitTime()
  133. {
  134. return m_waitTime;
  135. }
  136. ////////////////////////////////////////////////////////////////////////////////////////////////////
  137. void UiDropdownComponent::SetWaitTime(float waitTime)
  138. {
  139. m_waitTime = waitTime;
  140. }
  141. ////////////////////////////////////////////////////////////////////////////////////////////////////
  142. bool UiDropdownComponent::GetCollapseOnOutsideClick()
  143. {
  144. return m_collapseOnOutsideClick;
  145. }
  146. ////////////////////////////////////////////////////////////////////////////////////////////////////
  147. void UiDropdownComponent::SetCollapseOnOutsideClick(bool collapseOnOutsideClick)
  148. {
  149. m_collapseOnOutsideClick = collapseOnOutsideClick;
  150. }
  151. ////////////////////////////////////////////////////////////////////////////////////////////////////
  152. AZ::EntityId UiDropdownComponent::GetExpandedParentId()
  153. {
  154. return m_expandedParentId;
  155. }
  156. ////////////////////////////////////////////////////////////////////////////////////////////////////
  157. void UiDropdownComponent::SetExpandedParentId(AZ::EntityId expandedParentId)
  158. {
  159. m_expandedParentId = expandedParentId;
  160. }
  161. ////////////////////////////////////////////////////////////////////////////////////////////////////
  162. AZ::EntityId UiDropdownComponent::GetTextElement()
  163. {
  164. return m_textElement;
  165. }
  166. ////////////////////////////////////////////////////////////////////////////////////////////////////
  167. void UiDropdownComponent::SetTextElement(AZ::EntityId textElement)
  168. {
  169. m_textElement = textElement;
  170. }
  171. ////////////////////////////////////////////////////////////////////////////////////////////////////
  172. AZ::EntityId UiDropdownComponent::GetIconElement()
  173. {
  174. return m_iconElement;
  175. }
  176. ////////////////////////////////////////////////////////////////////////////////////////////////////
  177. void UiDropdownComponent::SetIconElement(AZ::EntityId iconElement)
  178. {
  179. m_iconElement = iconElement;
  180. }
  181. ////////////////////////////////////////////////////////////////////////////////////////////////////
  182. void UiDropdownComponent::Expand()
  183. {
  184. Expand(true);
  185. }
  186. ////////////////////////////////////////////////////////////////////////////////////////////////////
  187. void UiDropdownComponent::Collapse()
  188. {
  189. Collapse(true);
  190. }
  191. ////////////////////////////////////////////////////////////////////////////////////////////////////
  192. const LyShine::ActionName& UiDropdownComponent::GetExpandedActionName()
  193. {
  194. return m_expandedActionName;
  195. }
  196. ////////////////////////////////////////////////////////////////////////////////////////////////////
  197. void UiDropdownComponent::SetExpandedActionName(const LyShine::ActionName& actionName)
  198. {
  199. m_expandedActionName = actionName;
  200. }
  201. ////////////////////////////////////////////////////////////////////////////////////////////////////
  202. const LyShine::ActionName& UiDropdownComponent::GetCollapsedActionName()
  203. {
  204. return m_collapsedActionName;
  205. }
  206. ////////////////////////////////////////////////////////////////////////////////////////////////////
  207. void UiDropdownComponent::SetCollapsedActionName(const LyShine::ActionName& actionName)
  208. {
  209. m_collapsedActionName = actionName;
  210. }
  211. ////////////////////////////////////////////////////////////////////////////////////////////////////
  212. const LyShine::ActionName& UiDropdownComponent::GetOptionSelectedActionName()
  213. {
  214. return m_optionSelectedActionName;
  215. }
  216. ////////////////////////////////////////////////////////////////////////////////////////////////////
  217. void UiDropdownComponent::SetOptionSelectedActionName(const LyShine::ActionName& actionName)
  218. {
  219. m_optionSelectedActionName = actionName;
  220. }
  221. ////////////////////////////////////////////////////////////////////////////////////////////////////
  222. void UiDropdownComponent::InGamePostActivate()
  223. {
  224. // If the dropdown content is an interactable set its navigation to none
  225. UiNavigationBus::Event(m_content, &UiNavigationBus::Events::SetNavigationMode, UiNavigationInterface::NavigationMode::None);
  226. // Hide the dropdown on game start
  227. UiElementBus::Event(m_content, &UiElementBus::Events::SetIsEnabled, false);
  228. // Connect to canvas input notifications
  229. AZ::EntityId canvasEntityId;
  230. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  231. UiCanvasInputNotificationBus::Handler::BusConnect(canvasEntityId);
  232. m_canvasEntityId = canvasEntityId;
  233. // Save the base parent for the content
  234. UiElementBus::EventResult(m_baseParent, m_content, &UiElementBus::Events::GetParentEntityId);
  235. // Get a list of all our submenus (content descendants that have a dropdown component)
  236. UiElementBus::Event(
  237. m_content,
  238. &UiElementBus::Events::FindDescendantElements,
  239. [](const AZ::Entity* entity)
  240. {
  241. return UiDropdownBus::FindFirstHandler(entity->GetId()) != nullptr;
  242. },
  243. m_submenus);
  244. }
  245. ////////////////////////////////////////////////////////////////////////////////////////////////////
  246. bool UiDropdownComponent::HandleReleased(AZ::Vector2 point)
  247. {
  248. bool isInRect = false;
  249. UiTransformBus::EventResult(isInRect, GetEntityId(), &UiTransformBus::Events::IsPointInRect, point);
  250. if (isInRect)
  251. {
  252. return HandleReleasedCommon(point);
  253. }
  254. else
  255. {
  256. m_isPressed = false;
  257. return m_isHandlingEvents;
  258. }
  259. }
  260. ////////////////////////////////////////////////////////////////////////////////////////////////////
  261. bool UiDropdownComponent::HandleEnterReleased()
  262. {
  263. AZ::Vector2 point(-1.0f, -1.0f);
  264. return HandleReleasedCommon(point);
  265. }
  266. ////////////////////////////////////////////////////////////////////////////////////////////////////
  267. void UiDropdownComponent::HandleHoverStart()
  268. {
  269. m_isHover = true;
  270. UiInteractableComponent::TriggerHoverStartAction();
  271. if (m_expandOnHover && !m_expanded)
  272. {
  273. // Reset the timer and start listening to tick events to expand the menu
  274. m_delayTimer = 0.f;
  275. AZ::TickBus::Handler::BusConnect();
  276. }
  277. }
  278. ////////////////////////////////////////////////////////////////////////////////////////////////////
  279. void UiDropdownComponent::HandleHoverEnd()
  280. {
  281. m_isHover = false;
  282. UiInteractableComponent::TriggerHoverEndAction();
  283. if (m_expandOnHover)
  284. {
  285. if (m_expanded && !m_expandedByClick)
  286. {
  287. // Reset the timer and start listening to tick events to collapse the menu
  288. m_delayTimer = 0.f;
  289. AZ::TickBus::Handler::BusConnect();
  290. }
  291. else if (AZ::TickBus::Handler::BusIsConnected())
  292. {
  293. AZ::TickBus::Handler::BusDisconnect();
  294. }
  295. }
  296. }
  297. ////////////////////////////////////////////////////////////////////////////////////////////////////
  298. void UiDropdownComponent::OnReceivedHoverByNavigatingFromDescendant([[maybe_unused]] AZ::EntityId descendantEntityId)
  299. {
  300. AZ::EntityId entityId = *UiInteractableNotificationBus::GetCurrentBusId();
  301. if (entityId == m_tempContentParentInteractable)
  302. {
  303. Collapse(true);
  304. // Disconnect from the tickbus if we were connected
  305. if (AZ::TickBus::Handler::BusIsConnected())
  306. {
  307. AZ::TickBus::Handler::BusDisconnect();
  308. }
  309. }
  310. }
  311. ////////////////////////////////////////////////////////////////////////////////////////////////////
  312. void UiDropdownComponent::OnCanvasPrimaryReleased(AZ::EntityId entityId)
  313. {
  314. HandleCanvasReleasedCommon(entityId, true);
  315. }
  316. ////////////////////////////////////////////////////////////////////////////////////////////////////
  317. void UiDropdownComponent::OnCanvasEnterReleased(AZ::EntityId entityId)
  318. {
  319. if (entityId.IsValid())
  320. {
  321. HandleCanvasReleasedCommon(entityId, false);
  322. }
  323. }
  324. ////////////////////////////////////////////////////////////////////////////////////////////////////
  325. void UiDropdownComponent::OnCanvasHoverStart(AZ::EntityId entityId)
  326. {
  327. if (entityId == m_tempContentParentInteractable)
  328. {
  329. TransferHoverToDescendant();
  330. }
  331. else
  332. {
  333. // We only care about hovered things when we're already expanded
  334. if (m_expandOnHover && m_expanded)
  335. {
  336. // Figure out if the hovered entity is a descendant of either our content, or one of our
  337. // submenus content
  338. bool contentIsAncestor = ContentIsAncestor(entityId);
  339. // If we started hovering over one of our (or submenus) descendants or the dropdown button
  340. // and we were trying to collapse the menu
  341. if ((contentIsAncestor || entityId == GetEntityId()) && AZ::TickBus::Handler::BusIsConnected())
  342. {
  343. // Stop trying to collapse the menu
  344. AZ::TickBus::Handler::BusDisconnect();
  345. }
  346. }
  347. }
  348. }
  349. ////////////////////////////////////////////////////////////////////////////////////////////////////
  350. void UiDropdownComponent::OnCanvasHoverEnd(AZ::EntityId entityId)
  351. {
  352. // We only care about hovered things when we're already expanded
  353. if (m_expandOnHover && m_expanded && !m_expandedByClick)
  354. {
  355. // Figure out if the hovered entity is a descendant of either our content, or one of our
  356. // submenus content
  357. bool contentIsAncestor = ContentIsAncestor(entityId);
  358. // If we stopped hovering over one of our (or submenus) descendants
  359. if (contentIsAncestor)
  360. {
  361. // Reset the timer and start listening to tick events
  362. m_delayTimer = 0.f;
  363. AZ::TickBus::Handler::BusConnect();
  364. }
  365. }
  366. }
  367. ////////////////////////////////////////////////////////////////////////////////////////////////////
  368. void UiDropdownComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
  369. {
  370. m_delayTimer += deltaTime;
  371. // If we went over the wait time
  372. if (m_delayTimer >= m_waitTime)
  373. {
  374. // If we were waiting to expand
  375. if (!m_expanded)
  376. {
  377. m_expandedByClick = false;
  378. Expand();
  379. }
  380. // Else we were waiting to collapse
  381. else
  382. {
  383. Collapse();
  384. }
  385. // (we won't listen to the tick bus if we are not in either case)
  386. }
  387. }
  388. ////////////////////////////////////////////////////////////////////////////////////////////////////
  389. // PROTECTED MEMBER FUNCTIONS
  390. ////////////////////////////////////////////////////////////////////////////////////////////////////
  391. ////////////////////////////////////////////////////////////////////////////////////////////////////
  392. void UiDropdownComponent::Activate()
  393. {
  394. UiInteractableComponent::Activate();
  395. UiDropdownBus::Handler::BusConnect(GetEntityId());
  396. UiInitializationBus::Handler::BusConnect(GetEntityId());
  397. }
  398. ////////////////////////////////////////////////////////////////////////////////////////////////////
  399. void UiDropdownComponent::Deactivate()
  400. {
  401. UiInteractableComponent::Deactivate();
  402. UiDropdownBus::Handler::BusDisconnect(GetEntityId());
  403. UiInitializationBus::Handler::BusDisconnect(GetEntityId());
  404. if (m_canvasEntityId.IsValid())
  405. {
  406. UiCanvasInputNotificationBus::Handler::BusDisconnect(m_canvasEntityId);
  407. }
  408. if (AZ::TickBus::Handler::BusIsConnected())
  409. {
  410. AZ::TickBus::Handler::BusDisconnect();
  411. }
  412. if (m_tempContentParentInteractable.IsValid())
  413. {
  414. UiInteractableNotificationBus::MultiHandler::BusDisconnect(m_tempContentParentInteractable);
  415. }
  416. }
  417. ////////////////////////////////////////////////////////////////////////////////////////////////////
  418. // PROTECTED STATIC MEMBER FUNCTIONS
  419. ////////////////////////////////////////////////////////////////////////////////////////////////////
  420. void UiDropdownComponent::Reflect(AZ::ReflectContext* context)
  421. {
  422. AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
  423. if (serializeContext)
  424. {
  425. serializeContext->Class<UiDropdownComponent, UiInteractableComponent>()
  426. ->Version(1)
  427. // Elements group
  428. ->Field("Content", &UiDropdownComponent::m_content)
  429. ->Field("ExpandedParent", &UiDropdownComponent::m_expandedParentId)
  430. ->Field("TextElement", &UiDropdownComponent::m_textElement)
  431. ->Field("IconElement", &UiDropdownComponent::m_iconElement)
  432. // Options group
  433. ->Field("ExpandOnHover", &UiDropdownComponent::m_expandOnHover)
  434. ->Field("WaitTime", &UiDropdownComponent::m_waitTime)
  435. ->Field("CollapseOnOutsideClick", &UiDropdownComponent::m_collapseOnOutsideClick)
  436. // Dropdown States group
  437. ->Field("ExpandedStateActions", &UiDropdownComponent::m_expandedStateActions)
  438. // Actions group
  439. ->Field("ExpandedActionName", &UiDropdownComponent::m_expandedActionName)
  440. ->Field("CollapsedActionName", &UiDropdownComponent::m_collapsedActionName)
  441. ->Field("OptionSelectedActionName", &UiDropdownComponent::m_optionSelectedActionName);
  442. AZ::EditContext* ec = serializeContext->GetEditContext();
  443. if (ec)
  444. {
  445. auto editInfo = ec->Class<UiDropdownComponent>("Dropdown", "An interactable component for Dropdown behavior.");
  446. editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
  447. ->Attribute(AZ::Edit::Attributes::Category, "UI")
  448. ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiDropdown.png")
  449. ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiDropdown.png")
  450. ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
  451. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  452. // Elements group
  453. {
  454. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Elements")
  455. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  456. editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiDropdownComponent::m_content, "Content", "The element that contains the dropdown list.")
  457. ->Attribute(AZ::Edit::Attributes::ChangeValidate, &UiDropdownComponent::ValidatePotentialContent)
  458. ->Attribute(AZ::Edit::Attributes::EnumValues, &UiDropdownComponent::PopulateChildEntityList);
  459. editInfo->DataElement(AZ::Edit::UIHandlers::EntityId, &UiDropdownComponent::m_expandedParentId, "Expanded Parent", "The element the dropdown content should parent to when expanded (the canvas by default)."
  460. "This is used for layering, to display the dropdown content over other elements in the canvas that might be after it in the hierarchy.")
  461. ->Attribute(AZ::Edit::Attributes::ChangeValidate, &UiDropdownComponent::ValidatePotentialExpandedParent);
  462. editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiDropdownComponent::m_textElement, "Text Element", "The text element to use to display which option is selected.")
  463. ->Attribute(AZ::Edit::Attributes::EnumValues, &UiDropdownComponent::PopulateChildEntityList);
  464. editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiDropdownComponent::m_iconElement, "Icon Element", "The icon element to use to display which option is selected.")
  465. ->Attribute(AZ::Edit::Attributes::EnumValues, &UiDropdownComponent::PopulateChildEntityList);
  466. }
  467. // Options group
  468. {
  469. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Options")
  470. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  471. editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiDropdownComponent::m_expandOnHover, "Expand on Hover", "Whether this dropdown should be expanded upon hover, and collapse upon exit.")
  472. ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));
  473. editInfo->DataElement(0, &UiDropdownComponent::m_waitTime, "Wait Time", "How long the dropdown should wait before expanding on hover or collapsing on exit.")
  474. ->Attribute(AZ::Edit::Attributes::Visibility, &UiDropdownComponent::GetExpandOnHover);
  475. editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiDropdownComponent::m_collapseOnOutsideClick, "Collapse on Outside Click", "Whether this dropdown should be collapsed upon clicking outside the menu.");
  476. }
  477. // Dropdown States group
  478. {
  479. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Dropdown States")
  480. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  481. editInfo->DataElement(0, &UiDropdownComponent::m_expandedStateActions, "Expanded", "The expanded state actions.")
  482. ->Attribute(AZ::Edit::Attributes::AddNotify, &UiDropdownComponent::OnExpandedStateActionsChanged);
  483. }
  484. // Actions group
  485. {
  486. editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Actions")
  487. ->Attribute(AZ::Edit::Attributes::AutoExpand, true);
  488. editInfo->DataElement(0, &UiDropdownComponent::m_expandedActionName, "Expanded", "The action triggered when the dropdown is expanded.");
  489. editInfo->DataElement(0, &UiDropdownComponent::m_collapsedActionName, "Collapsed", "The action triggered when the dropdown is collapsed.");
  490. editInfo->DataElement(0, &UiDropdownComponent::m_optionSelectedActionName, "Option Selected", "The action triggered when an option is selected.");
  491. }
  492. }
  493. }
  494. AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
  495. if (behaviorContext)
  496. {
  497. behaviorContext->EBus<UiDropdownBus>("UiDropdownBus")
  498. ->Event("GetValue", &UiDropdownBus::Events::GetValue)
  499. ->Event("SetValue", &UiDropdownBus::Events::SetValue)
  500. ->Event("GetContent", &UiDropdownBus::Events::GetContent)
  501. ->Event("SetContent", &UiDropdownBus::Events::SetContent)
  502. ->Event("GetExpandOnHover", &UiDropdownBus::Events::GetExpandOnHover)
  503. ->Event("SetExpandOnHover", &UiDropdownBus::Events::SetExpandOnHover)
  504. ->Event("GetWaitTime", &UiDropdownBus::Events::GetWaitTime)
  505. ->Event("SetWaitTime", &UiDropdownBus::Events::SetWaitTime)
  506. ->Event("GetCollapseOnOutsideClick", &UiDropdownBus::Events::GetCollapseOnOutsideClick)
  507. ->Event("SetCollapseOnOutsideClick", &UiDropdownBus::Events::SetCollapseOnOutsideClick)
  508. ->Event("GetExpandedParentId", &UiDropdownBus::Events::GetExpandedParentId)
  509. ->Event("SetExpandedParentId", &UiDropdownBus::Events::SetExpandedParentId)
  510. ->Event("GetTextElement", &UiDropdownBus::Events::GetTextElement)
  511. ->Event("SetTextElement", &UiDropdownBus::Events::SetTextElement)
  512. ->Event("GetIconElement", &UiDropdownBus::Events::GetIconElement)
  513. ->Event("SetIconElement", &UiDropdownBus::Events::SetIconElement)
  514. ->Event("Expand", &UiDropdownBus::Events::Expand)
  515. ->Event("Collapse", &UiDropdownBus::Events::Collapse)
  516. ->Event("GetExpandedActionName", &UiDropdownBus::Events::GetExpandedActionName)
  517. ->Event("SetExpandedActionName", &UiDropdownBus::Events::SetExpandedActionName)
  518. ->Event("GetCollapsedActionName", &UiDropdownBus::Events::GetCollapsedActionName)
  519. ->Event("SetCollapsedActionName", &UiDropdownBus::Events::SetCollapsedActionName)
  520. ->Event("GetOptionSelectedActionName", &UiDropdownBus::Events::GetOptionSelectedActionName)
  521. ->Event("SetOptionSelectedActionName", &UiDropdownBus::Events::SetOptionSelectedActionName);
  522. behaviorContext->EBus<UiDropdownNotificationBus>("UiDropdownNotificationBus")
  523. ->Handler<UiDropdownNotificationBusBehaviorHandler>();
  524. }
  525. }
  526. ////////////////////////////////////////////////////////////////////////////////////////////////////
  527. // PRIVATE MEMBER FUNCTIONS
  528. ////////////////////////////////////////////////////////////////////////////////////////////////////
  529. ////////////////////////////////////////////////////////////////////////////////////////////////////
  530. UiDropdownComponent::EntityComboBoxVec UiDropdownComponent::PopulateChildEntityList()
  531. {
  532. EntityComboBoxVec result;
  533. // add a first entry for "None"
  534. result.push_back(AZStd::make_pair(AZ::EntityId(AZ::EntityId()), "<None>"));
  535. // Get a list of all child elements
  536. LyShine::EntityArray children;
  537. UiElementBus::EventResult(children, GetEntityId(), &UiElementBus::Events::GetChildElements);
  538. // add their names to the StringList and their IDs to the id list
  539. for (auto childEntity : children)
  540. {
  541. result.push_back(AZStd::make_pair(AZ::EntityId(childEntity->GetId()), childEntity->GetName()));
  542. }
  543. return result;
  544. }
  545. ////////////////////////////////////////////////////////////////////////////////////////////////////
  546. void UiDropdownComponent::OnExpandedStateActionsChanged()
  547. {
  548. m_stateActionManager.InitInteractableEntityForStateActions(m_expandedStateActions);
  549. }
  550. ////////////////////////////////////////////////////////////////////////////////////////////////////
  551. void UiDropdownComponent::Expand(bool transferHover)
  552. {
  553. m_expanded = true;
  554. // Enable the dropdown menu
  555. UiElementBus::Event(m_content, &UiElementBus::Events::SetIsEnabled, true);
  556. // Disconnect from the tickbus if we were connected
  557. if (AZ::TickBus::Handler::BusIsConnected())
  558. {
  559. AZ::TickBus::Handler::BusDisconnect();
  560. }
  561. // Save the current viewport position and scale
  562. AZ::Vector2 viewportPosition;
  563. UiTransformBus::EventResult(viewportPosition, m_content, &UiTransformBus::Events::GetViewportPosition);
  564. // Create a temporary content parent interactable that's a child of the given expanded parent
  565. // or the canvas if no expanded parent was specified.
  566. // The content element needs a parent interactable to constrain navigation between the content's
  567. // descendant interactables.
  568. m_tempContentParentInteractable = CreateContentParentInteractable();
  569. // Reparent the dropdown content to the content parent interactable
  570. if (m_tempContentParentInteractable.IsValid())
  571. {
  572. UiInteractableNotificationBus::MultiHandler::BusConnect(m_tempContentParentInteractable);
  573. UiElementBus::Event(m_content, &UiElementBus::Events::ReparentByEntityId, m_tempContentParentInteractable, AZ::EntityId());
  574. }
  575. UiTransformBus::Event(m_content, &UiTransformBus::Events::SetViewportPosition, viewportPosition);
  576. if (transferHover && IsNavigationSupported())
  577. {
  578. // Set the first descendant interactable to have the hover
  579. TransferHoverToDescendant();
  580. }
  581. if (!m_expandedActionName.empty())
  582. {
  583. AZ::EntityId canvasEntityId;
  584. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  585. UiCanvasNotificationBus::Event(canvasEntityId, &UiCanvasNotificationBus::Events::OnAction, GetEntityId(), m_expandedActionName);
  586. }
  587. UiDropdownNotificationBus::Event(GetEntityId(), &UiDropdownNotificationBus::Events::OnDropdownExpanded);
  588. }
  589. ////////////////////////////////////////////////////////////////////////////////////////////////////
  590. void UiDropdownComponent::Collapse(bool transferHover)
  591. {
  592. bool curHoverInteractableIsAncestor = false;
  593. AZ::EntityId hoverInteractable;
  594. UiCanvasBus::EventResult(hoverInteractable, m_canvasEntityId, &UiCanvasBus::Events::GetHoverInteractable);
  595. if (hoverInteractable.IsValid() && hoverInteractable != GetEntityId())
  596. {
  597. if (ContentIsAncestor(hoverInteractable))
  598. {
  599. curHoverInteractableIsAncestor = true;
  600. }
  601. }
  602. if (IsNavigationSupported() && curHoverInteractableIsAncestor)
  603. {
  604. if (transferHover)
  605. {
  606. // Regain the hover
  607. UiCanvasBus::Event(m_canvasEntityId, &UiCanvasBus::Events::ForceHoverInteractable, GetEntityId());
  608. }
  609. else
  610. {
  611. // Make sure a soon to be disabled interactable doesn't remain the hover interactable
  612. UiCanvasBus::Event(m_canvasEntityId, &UiCanvasBus::Events::ForceHoverInteractable, AZ::EntityId());
  613. }
  614. }
  615. m_expanded = false;
  616. // This is for Expand to always work the same way when called by script
  617. m_expandedByClick = true;
  618. // Disable the dropdown menu
  619. UiElementBus::Event(m_content, &UiElementBus::Events::SetIsEnabled, false);
  620. // Disconnect from the tickbus if we were connected
  621. if (AZ::TickBus::Handler::BusIsConnected())
  622. {
  623. AZ::TickBus::Handler::BusDisconnect();
  624. }
  625. // Save the current viewport position and scale
  626. AZ::Vector2 viewportPosition;
  627. UiTransformBus::EventResult(viewportPosition, m_content, &UiTransformBus::Events::GetViewportPosition);
  628. // Reparent the dropdown content to the base collapsed parent
  629. if (m_baseParent.IsValid())
  630. {
  631. UiElementBus::Event(m_content, &UiElementBus::Events::ReparentByEntityId, m_baseParent, AZ::EntityId());
  632. }
  633. // If the dropdown content had no base collapsed parent, reparent to canvas
  634. else
  635. {
  636. UiElementBus::Event(m_content, &UiElementBus::Events::Reparent, nullptr, nullptr);
  637. }
  638. // Destroy the temporary content parent interactable
  639. if (m_tempContentParentInteractable.IsValid())
  640. {
  641. UiInteractableNotificationBus::MultiHandler::BusDisconnect(m_tempContentParentInteractable);
  642. UiElementBus::Event(m_tempContentParentInteractable, &UiElementBus::Events::DestroyElement);
  643. m_tempContentParentInteractable.SetInvalid();
  644. }
  645. UiTransformBus::Event(m_content, &UiTransformBus::Events::SetViewportPosition, viewportPosition);
  646. if (!m_collapsedActionName.empty())
  647. {
  648. AZ::EntityId canvasEntityId;
  649. UiElementBus::EventResult(canvasEntityId, GetEntityId(), &UiElementBus::Events::GetCanvasEntityId);
  650. UiCanvasNotificationBus::Event(canvasEntityId, &UiCanvasNotificationBus::Events::OnAction, GetEntityId(), m_collapsedActionName);
  651. }
  652. UiDropdownNotificationBus::Event(GetEntityId(), &UiDropdownNotificationBus::Events::OnDropdownCollapsed);
  653. // Let all our submenus know they should collapse
  654. for (auto submenu : m_submenus)
  655. {
  656. UiDropdownBus::Event(submenu->GetId(), &UiDropdownBus::Events::Collapse);
  657. }
  658. }
  659. ////////////////////////////////////////////////////////////////////////////////////////////////////
  660. AZ::Outcome<void, AZStd::string> UiDropdownComponent::ValidateTypeIsEntityId(const AZ::Uuid& valueType)
  661. {
  662. if (azrtti_typeid<AZ::EntityId>() != valueType)
  663. {
  664. AZ_Assert(false, "Unexpected value type");
  665. return AZ::Failure(AZStd::string("Trying to set an entity ID to something that isn't an entity ID!"));
  666. }
  667. return AZ::Success();
  668. }
  669. ////////////////////////////////////////////////////////////////////////////////////////////////////
  670. AZ::Outcome<void, AZStd::string> UiDropdownComponent::ValidatePotentialContent(void* newValue, const AZ::Uuid& valueType)
  671. {
  672. auto typeValidation = ValidateTypeIsEntityId(valueType);
  673. if (!typeValidation.IsSuccess()) {
  674. return typeValidation;
  675. }
  676. AZ::EntityId actualValue = *static_cast<AZ::EntityId*>(newValue);
  677. // Don't allow the change if it will result in a cycle hierarchy
  678. if (actualValue.IsValid() && actualValue == m_expandedParentId)
  679. {
  680. return AZ::Failure(AZStd::string("You cannot set content to be the same as expanded parent!"));
  681. }
  682. if (ContentIsAncestor(m_expandedParentId, actualValue))
  683. {
  684. return AZ::Failure(AZStd::string("You cannot set content to be an ancestor of expanded parent!"));
  685. }
  686. return AZ::Success();
  687. }
  688. ////////////////////////////////////////////////////////////////////////////////////////////////////
  689. AZ::Outcome<void, AZStd::string> UiDropdownComponent::ValidatePotentialExpandedParent(void* newValue, const AZ::Uuid& valueType)
  690. {
  691. auto typeValidation = ValidateTypeIsEntityId(valueType);
  692. if (!typeValidation.IsSuccess()) {
  693. return typeValidation;
  694. }
  695. AZ::EntityId actualValue = *static_cast<AZ::EntityId*>(newValue);
  696. // Don't allow the change if it will result in a cycle hierarchy
  697. if (actualValue.IsValid() && actualValue == m_content)
  698. {
  699. return AZ::Failure(AZStd::string("You cannot set expanded parent to be the same as content!"));
  700. }
  701. if (ContentIsAncestor(actualValue))
  702. {
  703. return AZ::Failure(AZStd::string("You cannot set expanded parent to be a child of content!"));
  704. }
  705. return AZ::Success();
  706. }
  707. ////////////////////////////////////////////////////////////////////////////////////////////////////
  708. bool UiDropdownComponent::HandleReleasedCommon(const AZ::Vector2& point)
  709. {
  710. if (m_isHandlingEvents)
  711. {
  712. UiInteractableComponent::TriggerReleasedAction();
  713. bool transferHover = (point == AZ::Vector2(-1.0f, -1.0f));
  714. if (!m_expanded)
  715. {
  716. if (m_expandOnHover)
  717. {
  718. m_expandedByClick = true;
  719. }
  720. Expand(transferHover);
  721. }
  722. else
  723. {
  724. // Only collapse if it's not an expand on hover dropdown or if it was expanded
  725. // by a click if it is an expand on hover dropdown
  726. if (!m_expandOnHover || m_expandedByClick)
  727. {
  728. Collapse(transferHover);
  729. }
  730. }
  731. }
  732. m_isPressed = false;
  733. return m_isHandlingEvents;
  734. }
  735. ////////////////////////////////////////////////////////////////////////////////////////////////////
  736. void UiDropdownComponent::HandleCanvasReleasedCommon(AZ::EntityId entityId, bool positionalInput)
  737. {
  738. if (m_expanded)
  739. {
  740. // Collapse the menu in the following cases:
  741. // - the user clicked on the dropdown button
  742. // - the user clicked on an option
  743. // - the user clicked outside the dropdown
  744. // If the user clicked on the dropdown button
  745. if (entityId == GetEntityId())
  746. {
  747. // Let HandleReleasedCommon handle it
  748. return;
  749. }
  750. else
  751. {
  752. bool transferHover = !positionalInput;
  753. // Get the dropdown the option belongs to
  754. AZ::EntityId owningDropdown;
  755. UiDropdownOptionBus::EventResult(owningDropdown, entityId, &UiDropdownOptionBus::Events::GetOwningDropdown);
  756. // If one of our options was clicked
  757. if (owningDropdown == GetEntityId())
  758. {
  759. Collapse(transferHover);
  760. return;
  761. }
  762. else if (m_collapseOnOutsideClick)
  763. {
  764. if (entityId != m_content)
  765. {
  766. // Figure out if the clicked entity is a descendant of either our content, or one of our
  767. // submenus content
  768. bool contentIsAncestor = ContentIsAncestor(entityId);
  769. // If it was not an ancestor, then we clicked outside the dropdown
  770. if (!contentIsAncestor)
  771. {
  772. Collapse(transferHover);
  773. return;
  774. }
  775. }
  776. }
  777. }
  778. }
  779. }
  780. ////////////////////////////////////////////////////////////////////////////////////////////////////
  781. void UiDropdownComponent::TransferHoverToDescendant()
  782. {
  783. // Find the first descendant interactable of the content element
  784. AZ::EntityId descendantInteractable = FindFirstDescendantInteractable(m_content);
  785. if (descendantInteractable.IsValid())
  786. {
  787. UiCanvasBus::Event(m_canvasEntityId, &UiCanvasBus::Events::ForceHoverInteractable, descendantInteractable);
  788. }
  789. }
  790. ////////////////////////////////////////////////////////////////////////////////////////////////////
  791. AZ::EntityId UiDropdownComponent::FindFirstDescendantInteractable(AZ::EntityId parentEntityId)
  792. {
  793. AZ::EntityId firstDescendant;
  794. AZStd::vector<AZ::EntityId> childEntityIds;
  795. UiElementBus::EventResult(childEntityIds, parentEntityId, &UiElementBus::Events::GetChildEntityIds);
  796. for (auto childEntityId : childEntityIds)
  797. {
  798. if (UiNavigationHelpers::IsElementInteractableAndNavigable(childEntityId))
  799. {
  800. firstDescendant = childEntityId;
  801. break;
  802. }
  803. firstDescendant = FindFirstDescendantInteractable(childEntityId);
  804. if (firstDescendant.IsValid())
  805. {
  806. break;
  807. }
  808. }
  809. return firstDescendant;
  810. }
  811. ////////////////////////////////////////////////////////////////////////////////////////////////////
  812. AZ::EntityId UiDropdownComponent::CreateContentParentInteractable()
  813. {
  814. AZ::Entity* button = nullptr;
  815. if (m_expandedParentId.IsValid())
  816. {
  817. UiElementBus::EventResult(
  818. button, m_expandedParentId, &UiElementBus::Events::CreateChildElement, "InternalContentParentInteractable");
  819. }
  820. else
  821. {
  822. UiCanvasBus::EventResult(button, m_canvasEntityId, &UiCanvasBus::Events::CreateChildElement, "InternalContentParentInteractable");
  823. }
  824. AZ::EntityId buttonId;
  825. if (button)
  826. {
  827. // Set up the button element
  828. button->Deactivate();
  829. button->CreateComponent(LyShine::UiTransform2dComponentUuid);
  830. button->CreateComponent(LyShine::UiButtonComponentUuid);
  831. button->Activate();
  832. buttonId = button->GetId();
  833. AZ_Assert(UiTransform2dBus::FindFirstHandler(buttonId), "Transform2d component missing");
  834. UiTransform2dInterface::Anchors anchors(0.5f, 0.5f, 0.5f, 0.5f);
  835. UiTransform2dInterface::Offsets offsets(0.0f, 0.0f, 0.0f, 0.0f);
  836. AZ::Vector2 pivot(0.5f, 0.5f);
  837. UiTransform2dBus::Event(buttonId, &UiTransform2dBus::Events::SetAnchors, anchors, false, false);
  838. UiTransform2dBus::Event(buttonId, &UiTransform2dBus::Events::SetOffsets, offsets);
  839. UiTransformBus::Event(buttonId, &UiTransformBus::Events::SetPivot, pivot);
  840. UiTransformInterface::RectPoints contentPoints;
  841. UiTransformBus::Event(m_content, &UiTransformBus::Events::GetViewportSpacePoints, contentPoints);
  842. UiTransformBus::Event(buttonId, &UiTransformBus::Events::SetViewportPosition, contentPoints.GetCenter());
  843. }
  844. return buttonId;
  845. }
  846. ////////////////////////////////////////////////////////////////////////////////////////////////////
  847. bool UiDropdownComponent::ContentIsAncestor(AZ::EntityId entityId)
  848. {
  849. return ContentIsAncestor(entityId, m_content);
  850. }
  851. ////////////////////////////////////////////////////////////////////////////////////////////////////
  852. bool UiDropdownComponent::ContentIsAncestor(AZ::EntityId entityId, AZ::EntityId contentId)
  853. {
  854. bool contentIsAncestor = false;
  855. UiElementBus::EventResult(contentIsAncestor, entityId, &UiElementBus::Events::IsAncestor, contentId);
  856. if (contentIsAncestor)
  857. {
  858. return true;
  859. }
  860. for (auto submenu : m_submenus)
  861. {
  862. AZ::EntityId submenuContent;
  863. UiDropdownBus::EventResult(submenuContent, submenu->GetId(), &UiDropdownBus::Events::GetContent);
  864. bool submenuIsAncestor = false;
  865. UiElementBus::EventResult(submenuIsAncestor, entityId, &UiElementBus::Events::IsAncestor, submenuContent);
  866. if (submenuIsAncestor)
  867. {
  868. return true;
  869. }
  870. }
  871. return false;
  872. }
  873. ////////////////////////////////////////////////////////////////////////////////////////////////////
  874. bool UiDropdownComponent::IsNavigationSupported()
  875. {
  876. bool isNavigationSupported = false;
  877. UiCanvasBus::EventResult(isNavigationSupported, m_canvasEntityId, &UiCanvasBus::Events::GetIsNavigationSupported);
  878. return isNavigationSupported;
  879. }
  880. ////////////////////////////////////////////////////////////////////////////////////////////////////
  881. UiInteractableStatesInterface::State UiDropdownComponent::ComputeInteractableState()
  882. {
  883. UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal;
  884. if (!m_isHandlingEvents)
  885. {
  886. state = UiInteractableStatesInterface::StateDisabled;
  887. }
  888. else if (m_isPressed)
  889. {
  890. state = UiInteractableStatesInterface::StatePressed;
  891. }
  892. else if (m_isHover)
  893. {
  894. state = UiInteractableStatesInterface::StateHover;
  895. }
  896. else if (m_expanded)
  897. {
  898. state = DropdownStateExpanded;
  899. }
  900. return state;
  901. }