You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1058 lines
40 KiB
C++
1058 lines
40 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#include "UiDropdownComponent.h"
|
|
|
|
#include <AzCore/Serialization/SerializeContext.h>
|
|
#include <AzCore/Serialization/EditContext.h>
|
|
#include <AzCore/RTTI/BehaviorContext.h>
|
|
|
|
#include <LyShine/Bus/UiTransformBus.h>
|
|
#include <LyShine/Bus/UiElementBus.h>
|
|
#include <LyShine/Bus/UiTextBus.h>
|
|
#include <LyShine/Bus/UiImageBus.h>
|
|
#include <LyShine/Bus/UiTransform2dBus.h>
|
|
#include <LyShine/Bus/UiDropdownOptionBus.h>
|
|
#include <LyShine/UiSerializeHelpers.h>
|
|
#include "UiNavigationHelpers.h"
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
//! UiDropdownNotificationBusBehaviorHandler Behavior context handler class
|
|
class UiDropdownNotificationBusBehaviorHandler
|
|
: public UiDropdownNotificationBus::Handler
|
|
, public AZ::BehaviorEBusHandler
|
|
{
|
|
public:
|
|
AZ_EBUS_BEHAVIOR_BINDER(UiDropdownNotificationBusBehaviorHandler, "{C936F190-524E-410E-82C9-9B590015B6D5}", AZ::SystemAllocator,
|
|
OnDropdownExpanded, OnDropdownCollapsed, OnDropdownValueChanged);
|
|
|
|
void OnDropdownExpanded() override
|
|
{
|
|
Call(FN_OnDropdownExpanded);
|
|
}
|
|
|
|
void OnDropdownCollapsed() override
|
|
{
|
|
Call(FN_OnDropdownCollapsed);
|
|
}
|
|
|
|
void OnDropdownValueChanged(AZ::EntityId value) override
|
|
{
|
|
Call(FN_OnDropdownValueChanged, value);
|
|
}
|
|
};
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PUBLIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDropdownComponent::UiDropdownComponent()
|
|
: m_value()
|
|
, m_content()
|
|
, m_expandOnHover(false)
|
|
, m_waitTime(0.3f)
|
|
, m_collapseOnOutsideClick(true)
|
|
, m_expandedParentId()
|
|
, m_textElement()
|
|
, m_iconElement()
|
|
, m_expandedActionName()
|
|
, m_collapsedActionName()
|
|
, m_optionSelectedActionName()
|
|
, m_expanded(false)
|
|
, m_canvasEntityId()
|
|
, m_delayTimer(0.f)
|
|
, m_baseParent()
|
|
, m_expandedByClick(true)
|
|
{
|
|
m_stateActionManager.AddState(&m_expandedStateActions);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDropdownComponent::~UiDropdownComponent()
|
|
{
|
|
// delete all the state actions now rather than letting the base class do it automatically
|
|
// because the m_stateActionManager has pointers to members in this derived class.
|
|
m_stateActionManager.ClearStates();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::GetValue()
|
|
{
|
|
return m_value;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetValue(AZ::EntityId value)
|
|
{
|
|
m_value = value;
|
|
|
|
// Get the text from the newly selection option
|
|
AZ::EntityId optionText;
|
|
EBUS_EVENT_ID_RESULT(optionText, value, UiDropdownOptionBus, GetTextElement);
|
|
if (optionText.IsValid())
|
|
{
|
|
AZStd::string text;
|
|
EBUS_EVENT_ID_RESULT(text, optionText, UiTextBus, GetText);
|
|
// Set our text to that text to show which option was selected
|
|
EBUS_EVENT_ID(m_textElement, UiTextBus, SetTextWithFlags, text, UiTextInterface::SetTextFlags::SetLocalized);
|
|
}
|
|
|
|
// Get the icon from the newly selection option
|
|
AZ::EntityId optionIcon;
|
|
EBUS_EVENT_ID_RESULT(optionIcon, value, UiDropdownOptionBus, GetIconElement);
|
|
if (optionIcon.IsValid())
|
|
{
|
|
ISprite* sprite;
|
|
EBUS_EVENT_ID_RESULT(sprite, optionIcon, UiImageBus, GetSprite);
|
|
// Set our icon to that icon to show which option was selected
|
|
EBUS_EVENT_ID(m_iconElement, UiImageBus, SetSprite, sprite);
|
|
}
|
|
|
|
if (!m_optionSelectedActionName.empty())
|
|
{
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_optionSelectedActionName);
|
|
}
|
|
EBUS_EVENT_ID(GetEntityId(), UiDropdownNotificationBus, OnDropdownValueChanged, value);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::GetContent()
|
|
{
|
|
return m_content;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetContent(AZ::EntityId content)
|
|
{
|
|
m_content = content;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::GetExpandOnHover()
|
|
{
|
|
return m_expandOnHover;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetExpandOnHover(bool expandOnHover)
|
|
{
|
|
m_expandOnHover = expandOnHover;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
float UiDropdownComponent::GetWaitTime()
|
|
{
|
|
return m_waitTime;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetWaitTime(float waitTime)
|
|
{
|
|
m_waitTime = waitTime;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::GetCollapseOnOutsideClick()
|
|
{
|
|
return m_collapseOnOutsideClick;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetCollapseOnOutsideClick(bool collapseOnOutsideClick)
|
|
{
|
|
m_collapseOnOutsideClick = collapseOnOutsideClick;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::GetExpandedParentId()
|
|
{
|
|
return m_expandedParentId;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetExpandedParentId(AZ::EntityId expandedParentId)
|
|
{
|
|
m_expandedParentId = expandedParentId;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::GetTextElement()
|
|
{
|
|
return m_textElement;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetTextElement(AZ::EntityId textElement)
|
|
{
|
|
m_textElement = textElement;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::GetIconElement()
|
|
{
|
|
return m_iconElement;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetIconElement(AZ::EntityId iconElement)
|
|
{
|
|
m_iconElement = iconElement;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::Expand()
|
|
{
|
|
Expand(true);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::Collapse()
|
|
{
|
|
Collapse(true);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const LyShine::ActionName& UiDropdownComponent::GetExpandedActionName()
|
|
{
|
|
return m_expandedActionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetExpandedActionName(const LyShine::ActionName& actionName)
|
|
{
|
|
m_expandedActionName = actionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const LyShine::ActionName& UiDropdownComponent::GetCollapsedActionName()
|
|
{
|
|
return m_collapsedActionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetCollapsedActionName(const LyShine::ActionName& actionName)
|
|
{
|
|
m_collapsedActionName = actionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
const LyShine::ActionName& UiDropdownComponent::GetOptionSelectedActionName()
|
|
{
|
|
return m_optionSelectedActionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::SetOptionSelectedActionName(const LyShine::ActionName& actionName)
|
|
{
|
|
m_optionSelectedActionName = actionName;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::InGamePostActivate()
|
|
{
|
|
// If the dropdown content is an interactable set its navigation to none
|
|
EBUS_EVENT_ID(m_content, UiNavigationBus, SetNavigationMode, UiNavigationInterface::NavigationMode::None);
|
|
|
|
// Hide the dropdown on game start
|
|
EBUS_EVENT_ID(m_content, UiElementBus, SetIsEnabled, false);
|
|
|
|
// Connect to canvas input notifications
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
UiCanvasInputNotificationBus::Handler::BusConnect(canvasEntityId);
|
|
m_canvasEntityId = canvasEntityId;
|
|
|
|
// Save the base parent for the content
|
|
EBUS_EVENT_ID_RESULT(m_baseParent, m_content, UiElementBus, GetParentEntityId);
|
|
|
|
// Get a list of all our submenus (content descendants that have a dropdown component)
|
|
EBUS_EVENT_ID(m_content, UiElementBus, FindDescendantElements,
|
|
[](const AZ::Entity* entity) { return UiDropdownBus::FindFirstHandler(entity->GetId()) != nullptr; },
|
|
m_submenus);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::HandleReleased(AZ::Vector2 point)
|
|
{
|
|
bool isInRect = false;
|
|
EBUS_EVENT_ID_RESULT(isInRect, GetEntityId(), UiTransformBus, IsPointInRect, point);
|
|
if (isInRect)
|
|
{
|
|
return HandleReleasedCommon(point);
|
|
}
|
|
else
|
|
{
|
|
m_isPressed = false;
|
|
|
|
return m_isHandlingEvents;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::HandleEnterReleased()
|
|
{
|
|
AZ::Vector2 point(-1.0f, -1.0f);
|
|
return HandleReleasedCommon(point);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::HandleHoverStart()
|
|
{
|
|
m_isHover = true;
|
|
|
|
UiInteractableComponent::TriggerHoverStartAction();
|
|
|
|
if (m_expandOnHover && !m_expanded)
|
|
{
|
|
// Reset the timer and start listening to tick events to expand the menu
|
|
m_delayTimer = 0.f;
|
|
AZ::TickBus::Handler::BusConnect();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::HandleHoverEnd()
|
|
{
|
|
m_isHover = false;
|
|
|
|
UiInteractableComponent::TriggerHoverEndAction();
|
|
|
|
if (m_expandOnHover)
|
|
{
|
|
if (m_expanded && !m_expandedByClick)
|
|
{
|
|
// Reset the timer and start listening to tick events to collapse the menu
|
|
m_delayTimer = 0.f;
|
|
AZ::TickBus::Handler::BusConnect();
|
|
}
|
|
else if (AZ::TickBus::Handler::BusIsConnected())
|
|
{
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnReceivedHoverByNavigatingFromDescendant([[maybe_unused]] AZ::EntityId descendantEntityId)
|
|
{
|
|
AZ::EntityId entityId = *UiInteractableNotificationBus::GetCurrentBusId();
|
|
|
|
if (entityId == m_tempContentParentInteractable)
|
|
{
|
|
Collapse(true);
|
|
|
|
// Disconnect from the tickbus if we were connected
|
|
if (AZ::TickBus::Handler::BusIsConnected())
|
|
{
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnCanvasPrimaryReleased(AZ::EntityId entityId)
|
|
{
|
|
HandleCanvasReleasedCommon(entityId, true);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnCanvasEnterReleased(AZ::EntityId entityId)
|
|
{
|
|
if (entityId.IsValid())
|
|
{
|
|
HandleCanvasReleasedCommon(entityId, false);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnCanvasHoverStart(AZ::EntityId entityId)
|
|
{
|
|
if (entityId == m_tempContentParentInteractable)
|
|
{
|
|
TransferHoverToDescendant();
|
|
}
|
|
else
|
|
{
|
|
// We only care about hovered things when we're already expanded
|
|
if (m_expandOnHover && m_expanded)
|
|
{
|
|
// Figure out if the hovered entity is a descendant of either our content, or one of our
|
|
// submenus content
|
|
bool contentIsAncestor = ContentIsAncestor(entityId);
|
|
|
|
// If we started hovering over one of our (or submenus) descendants or the dropdown button
|
|
// and we were trying to collapse the menu
|
|
if ((contentIsAncestor || entityId == GetEntityId()) && AZ::TickBus::Handler::BusIsConnected())
|
|
{
|
|
// Stop trying to collapse the menu
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnCanvasHoverEnd(AZ::EntityId entityId)
|
|
{
|
|
// We only care about hovered things when we're already expanded
|
|
if (m_expandOnHover && m_expanded && !m_expandedByClick)
|
|
{
|
|
// Figure out if the hovered entity is a descendant of either our content, or one of our
|
|
// submenus content
|
|
bool contentIsAncestor = ContentIsAncestor(entityId);
|
|
|
|
// If we stopped hovering over one of our (or submenus) descendants
|
|
if (contentIsAncestor)
|
|
{
|
|
// Reset the timer and start listening to tick events
|
|
m_delayTimer = 0.f;
|
|
AZ::TickBus::Handler::BusConnect();
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time)
|
|
{
|
|
m_delayTimer += deltaTime;
|
|
|
|
// If we went over the wait time
|
|
if (m_delayTimer >= m_waitTime)
|
|
{
|
|
// If we were waiting to expand
|
|
if (!m_expanded)
|
|
{
|
|
m_expandedByClick = false;
|
|
Expand();
|
|
}
|
|
// Else we were waiting to collapse
|
|
else
|
|
{
|
|
Collapse();
|
|
}
|
|
// (we won't listen to the tick bus if we are not in either case)
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PROTECTED MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::Activate()
|
|
{
|
|
UiInteractableComponent::Activate();
|
|
UiDropdownBus::Handler::BusConnect(GetEntityId());
|
|
UiInitializationBus::Handler::BusConnect(GetEntityId());
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::Deactivate()
|
|
{
|
|
UiInteractableComponent::Deactivate();
|
|
UiDropdownBus::Handler::BusDisconnect(GetEntityId());
|
|
UiInitializationBus::Handler::BusDisconnect(GetEntityId());
|
|
if (m_canvasEntityId.IsValid())
|
|
{
|
|
UiCanvasInputNotificationBus::Handler::BusDisconnect(m_canvasEntityId);
|
|
}
|
|
if (AZ::TickBus::Handler::BusIsConnected())
|
|
{
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
}
|
|
if (m_tempContentParentInteractable.IsValid())
|
|
{
|
|
UiInteractableNotificationBus::MultiHandler::BusDisconnect(m_tempContentParentInteractable);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PROTECTED STATIC MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
void UiDropdownComponent::Reflect(AZ::ReflectContext* context)
|
|
{
|
|
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
|
|
|
|
if (serializeContext)
|
|
{
|
|
serializeContext->Class<UiDropdownComponent, UiInteractableComponent>()
|
|
->Version(1)
|
|
// Elements group
|
|
->Field("Content", &UiDropdownComponent::m_content)
|
|
->Field("ExpandedParent", &UiDropdownComponent::m_expandedParentId)
|
|
->Field("TextElement", &UiDropdownComponent::m_textElement)
|
|
->Field("IconElement", &UiDropdownComponent::m_iconElement)
|
|
// Options group
|
|
->Field("ExpandOnHover", &UiDropdownComponent::m_expandOnHover)
|
|
->Field("WaitTime", &UiDropdownComponent::m_waitTime)
|
|
->Field("CollapseOnOutsideClick", &UiDropdownComponent::m_collapseOnOutsideClick)
|
|
// Dropdown States group
|
|
->Field("ExpandedStateActions", &UiDropdownComponent::m_expandedStateActions)
|
|
// Actions group
|
|
->Field("ExpandedActionName", &UiDropdownComponent::m_expandedActionName)
|
|
->Field("CollapsedActionName", &UiDropdownComponent::m_collapsedActionName)
|
|
->Field("OptionSelectedActionName", &UiDropdownComponent::m_optionSelectedActionName);
|
|
|
|
AZ::EditContext* ec = serializeContext->GetEditContext();
|
|
if (ec)
|
|
{
|
|
auto editInfo = ec->Class<UiDropdownComponent>("Dropdown", "An interactable component for Dropdown behavior.");
|
|
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
|
|
->Attribute(AZ::Edit::Attributes::Category, "UI")
|
|
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiDropdown.png")
|
|
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiDropdown.png")
|
|
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
// Elements group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Elements")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiDropdownComponent::m_content, "Content", "The element that contains the dropdown list.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeValidate, &UiDropdownComponent::ValidatePotentialContent)
|
|
->Attribute(AZ::Edit::Attributes::EnumValues, &UiDropdownComponent::PopulateChildEntityList);
|
|
|
|
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)."
|
|
"This is used for layering, to display the dropdown content over other elements in the canvas that might be after it in the hierarchy.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeValidate, &UiDropdownComponent::ValidatePotentialExpandedParent);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiDropdownComponent::m_textElement, "Text Element", "The text element to use to display which option is selected.")
|
|
->Attribute(AZ::Edit::Attributes::EnumValues, &UiDropdownComponent::PopulateChildEntityList);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::ComboBox, &UiDropdownComponent::m_iconElement, "Icon Element", "The icon element to use to display which option is selected.")
|
|
->Attribute(AZ::Edit::Attributes::EnumValues, &UiDropdownComponent::PopulateChildEntityList);
|
|
}
|
|
|
|
// Options group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Options")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiDropdownComponent::m_expandOnHover, "Expand on Hover", "Whether this dropdown should be expanded upon hover, and collapse upon exit.")
|
|
->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));
|
|
|
|
editInfo->DataElement(0, &UiDropdownComponent::m_waitTime, "Wait Time", "How long the dropdown should wait before expanding on hover or collapsing on exit.")
|
|
->Attribute(AZ::Edit::Attributes::Visibility, &UiDropdownComponent::GetExpandOnHover);
|
|
|
|
editInfo->DataElement(AZ::Edit::UIHandlers::CheckBox, &UiDropdownComponent::m_collapseOnOutsideClick, "Collapse on Outside Click", "Whether this dropdown should be collapsed upon clicking outside the menu.");
|
|
}
|
|
|
|
// Dropdown States group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Dropdown States")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(0, &UiDropdownComponent::m_expandedStateActions, "Expanded", "The expanded state actions.")
|
|
->Attribute(AZ::Edit::Attributes::AddNotify, &UiDropdownComponent::OnExpandedStateActionsChanged);
|
|
}
|
|
|
|
// Actions group
|
|
{
|
|
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Actions")
|
|
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
|
|
|
|
editInfo->DataElement(0, &UiDropdownComponent::m_expandedActionName, "Expanded", "The action triggered when the dropdown is expanded.");
|
|
editInfo->DataElement(0, &UiDropdownComponent::m_collapsedActionName, "Collapsed", "The action triggered when the dropdown is collapsed.");
|
|
editInfo->DataElement(0, &UiDropdownComponent::m_optionSelectedActionName, "Option Selected", "The action triggered when an option is selected.");
|
|
}
|
|
}
|
|
}
|
|
|
|
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
|
|
if (behaviorContext)
|
|
{
|
|
behaviorContext->EBus<UiDropdownBus>("UiDropdownBus")
|
|
->Event("GetValue", &UiDropdownBus::Events::GetValue)
|
|
->Event("SetValue", &UiDropdownBus::Events::SetValue)
|
|
->Event("GetContent", &UiDropdownBus::Events::GetContent)
|
|
->Event("SetContent", &UiDropdownBus::Events::SetContent)
|
|
->Event("GetExpandOnHover", &UiDropdownBus::Events::GetExpandOnHover)
|
|
->Event("SetExpandOnHover", &UiDropdownBus::Events::SetExpandOnHover)
|
|
->Event("GetWaitTime", &UiDropdownBus::Events::GetWaitTime)
|
|
->Event("SetWaitTime", &UiDropdownBus::Events::SetWaitTime)
|
|
->Event("GetCollapseOnOutsideClick", &UiDropdownBus::Events::GetCollapseOnOutsideClick)
|
|
->Event("SetCollapseOnOutsideClick", &UiDropdownBus::Events::SetCollapseOnOutsideClick)
|
|
->Event("GetExpandedParentId", &UiDropdownBus::Events::GetExpandedParentId)
|
|
->Event("SetExpandedParentId", &UiDropdownBus::Events::SetExpandedParentId)
|
|
->Event("GetTextElement", &UiDropdownBus::Events::GetTextElement)
|
|
->Event("SetTextElement", &UiDropdownBus::Events::SetTextElement)
|
|
->Event("GetIconElement", &UiDropdownBus::Events::GetIconElement)
|
|
->Event("SetIconElement", &UiDropdownBus::Events::SetIconElement)
|
|
->Event("Expand", &UiDropdownBus::Events::Expand)
|
|
->Event("Collapse", &UiDropdownBus::Events::Collapse)
|
|
->Event("GetExpandedActionName", &UiDropdownBus::Events::GetExpandedActionName)
|
|
->Event("SetExpandedActionName", &UiDropdownBus::Events::SetExpandedActionName)
|
|
->Event("GetCollapsedActionName", &UiDropdownBus::Events::GetCollapsedActionName)
|
|
->Event("SetCollapsedActionName", &UiDropdownBus::Events::SetCollapsedActionName)
|
|
->Event("GetOptionSelectedActionName", &UiDropdownBus::Events::GetOptionSelectedActionName)
|
|
->Event("SetOptionSelectedActionName", &UiDropdownBus::Events::SetOptionSelectedActionName);
|
|
|
|
behaviorContext->EBus<UiDropdownNotificationBus>("UiDropdownNotificationBus")
|
|
->Handler<UiDropdownNotificationBusBehaviorHandler>();
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// PRIVATE MEMBER FUNCTIONS
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiDropdownComponent::EntityComboBoxVec UiDropdownComponent::PopulateChildEntityList()
|
|
{
|
|
EntityComboBoxVec result;
|
|
|
|
// add a first entry for "None"
|
|
result.push_back(AZStd::make_pair(AZ::EntityId(AZ::EntityId()), "<None>"));
|
|
|
|
// Get a list of all child elements
|
|
LyShine::EntityArray children;
|
|
EBUS_EVENT_ID_RESULT(children, GetEntityId(), UiElementBus, GetChildElements);
|
|
|
|
// add their names to the StringList and their IDs to the id list
|
|
for (auto childEntity : children)
|
|
{
|
|
result.push_back(AZStd::make_pair(AZ::EntityId(childEntity->GetId()), childEntity->GetName()));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::OnExpandedStateActionsChanged()
|
|
{
|
|
m_stateActionManager.InitInteractableEntityForStateActions(m_expandedStateActions);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::Expand(bool transferHover)
|
|
{
|
|
m_expanded = true;
|
|
|
|
// Enable the dropdown menu
|
|
EBUS_EVENT_ID(m_content, UiElementBus, SetIsEnabled, true);
|
|
|
|
// Disconnect from the tickbus if we were connected
|
|
if (AZ::TickBus::Handler::BusIsConnected())
|
|
{
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
}
|
|
|
|
// Save the current viewport position and scale
|
|
AZ::Vector2 viewportPosition;
|
|
EBUS_EVENT_ID_RESULT(viewportPosition, m_content, UiTransformBus, GetViewportPosition);
|
|
|
|
// Create a temporary content parent interactable that's a child of the given expanded parent
|
|
// or the canvas if no expanded parent was specified.
|
|
// The content element needs a parent interactable to constrain navigation between the content's
|
|
// descendant interactables.
|
|
m_tempContentParentInteractable = CreateContentParentInteractable();
|
|
|
|
// Reparent the dropdown content to the content parent interactable
|
|
if (m_tempContentParentInteractable.IsValid())
|
|
{
|
|
UiInteractableNotificationBus::MultiHandler::BusConnect(m_tempContentParentInteractable);
|
|
EBUS_EVENT_ID(m_content, UiElementBus, ReparentByEntityId, m_tempContentParentInteractable, AZ::EntityId());
|
|
}
|
|
|
|
EBUS_EVENT_ID(m_content, UiTransformBus, SetViewportPosition, viewportPosition);
|
|
|
|
if (transferHover && IsNavigationSupported())
|
|
{
|
|
// Set the first descendant interactable to have the hover
|
|
TransferHoverToDescendant();
|
|
}
|
|
|
|
if (!m_expandedActionName.empty())
|
|
{
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_expandedActionName);
|
|
}
|
|
EBUS_EVENT_ID(GetEntityId(), UiDropdownNotificationBus, OnDropdownExpanded);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::Collapse(bool transferHover)
|
|
{
|
|
bool curHoverInteractableIsAncestor = false;
|
|
|
|
AZ::EntityId hoverInteractable;
|
|
EBUS_EVENT_ID_RESULT(hoverInteractable, m_canvasEntityId, UiCanvasBus, GetHoverInteractable);
|
|
if (hoverInteractable.IsValid() && hoverInteractable != GetEntityId())
|
|
{
|
|
if (ContentIsAncestor(hoverInteractable))
|
|
{
|
|
curHoverInteractableIsAncestor = true;
|
|
}
|
|
}
|
|
|
|
if (IsNavigationSupported() && curHoverInteractableIsAncestor)
|
|
{
|
|
if (transferHover)
|
|
{
|
|
// Regain the hover
|
|
EBUS_EVENT_ID(m_canvasEntityId, UiCanvasBus, ForceHoverInteractable, GetEntityId());
|
|
}
|
|
else
|
|
{
|
|
// Make sure a soon to be disabled interactable doesn't remain the hover interactable
|
|
EBUS_EVENT_ID(m_canvasEntityId, UiCanvasBus, ForceHoverInteractable, AZ::EntityId());
|
|
}
|
|
}
|
|
|
|
m_expanded = false;
|
|
|
|
// This is for Expand to always work the same way when called by script
|
|
m_expandedByClick = true;
|
|
|
|
// Disable the dropdown menu
|
|
EBUS_EVENT_ID(m_content, UiElementBus, SetIsEnabled, false);
|
|
|
|
// Disconnect from the tickbus if we were connected
|
|
if (AZ::TickBus::Handler::BusIsConnected())
|
|
{
|
|
AZ::TickBus::Handler::BusDisconnect();
|
|
}
|
|
|
|
// Save the current viewport position and scale
|
|
AZ::Vector2 viewportPosition;
|
|
EBUS_EVENT_ID_RESULT(viewportPosition, m_content, UiTransformBus, GetViewportPosition);
|
|
|
|
// Reparent the dropdown content to the base collapsed parent
|
|
if (m_baseParent.IsValid())
|
|
{
|
|
EBUS_EVENT_ID(m_content, UiElementBus, ReparentByEntityId, m_baseParent, AZ::EntityId());
|
|
}
|
|
// If the dropdown content had no base collapsed parent, reparent to canvas
|
|
else
|
|
{
|
|
EBUS_EVENT_ID(m_content, UiElementBus, Reparent, nullptr, nullptr);
|
|
}
|
|
|
|
// Destroy the temporary content parent interactable
|
|
if (m_tempContentParentInteractable.IsValid())
|
|
{
|
|
UiInteractableNotificationBus::MultiHandler::BusDisconnect(m_tempContentParentInteractable);
|
|
EBUS_EVENT_ID(m_tempContentParentInteractable, UiElementBus, DestroyElement);
|
|
m_tempContentParentInteractable.SetInvalid();
|
|
}
|
|
|
|
EBUS_EVENT_ID(m_content, UiTransformBus, SetViewportPosition, viewportPosition);
|
|
|
|
if (!m_collapsedActionName.empty())
|
|
{
|
|
AZ::EntityId canvasEntityId;
|
|
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
|
|
EBUS_EVENT_ID(canvasEntityId, UiCanvasNotificationBus, OnAction, GetEntityId(), m_collapsedActionName);
|
|
}
|
|
EBUS_EVENT_ID(GetEntityId(), UiDropdownNotificationBus, OnDropdownCollapsed);
|
|
|
|
// Let all our submenus know they should collapse
|
|
for (auto submenu : m_submenus)
|
|
{
|
|
EBUS_EVENT_ID(submenu->GetId(), UiDropdownBus, Collapse);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Outcome<void, AZStd::string> UiDropdownComponent::ValidateTypeIsEntityId(const AZ::Uuid& valueType)
|
|
{
|
|
if (azrtti_typeid<AZ::EntityId>() != valueType)
|
|
{
|
|
AZ_Assert(false, "Unexpected value type");
|
|
return AZ::Failure(AZStd::string("Trying to set an entity ID to something that isn't an entity ID!"));
|
|
}
|
|
return AZ::Success();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Outcome<void, AZStd::string> UiDropdownComponent::ValidatePotentialContent(void* newValue, const AZ::Uuid& valueType)
|
|
{
|
|
auto typeValidation = ValidateTypeIsEntityId(valueType);
|
|
|
|
if (!typeValidation.IsSuccess()) {
|
|
return typeValidation;
|
|
}
|
|
|
|
AZ::EntityId actualValue = *static_cast<AZ::EntityId*>(newValue);
|
|
|
|
// Don't allow the change if it will result in a cycle hierarchy
|
|
if (actualValue.IsValid() && actualValue == m_expandedParentId)
|
|
{
|
|
return AZ::Failure(AZStd::string("You cannot set content to be the same as expanded parent!"));
|
|
}
|
|
|
|
if (ContentIsAncestor(m_expandedParentId, actualValue))
|
|
{
|
|
return AZ::Failure(AZStd::string("You cannot set content to be an ancestor of expanded parent!"));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::Outcome<void, AZStd::string> UiDropdownComponent::ValidatePotentialExpandedParent(void* newValue, const AZ::Uuid& valueType)
|
|
{
|
|
auto typeValidation = ValidateTypeIsEntityId(valueType);
|
|
|
|
if (!typeValidation.IsSuccess()) {
|
|
return typeValidation;
|
|
}
|
|
|
|
AZ::EntityId actualValue = *static_cast<AZ::EntityId*>(newValue);
|
|
|
|
// Don't allow the change if it will result in a cycle hierarchy
|
|
if (actualValue.IsValid() && actualValue == m_content)
|
|
{
|
|
return AZ::Failure(AZStd::string("You cannot set expanded parent to be the same as content!"));
|
|
}
|
|
|
|
if (ContentIsAncestor(actualValue))
|
|
{
|
|
return AZ::Failure(AZStd::string("You cannot set expanded parent to be a child of content!"));
|
|
}
|
|
|
|
return AZ::Success();
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::FailureValue<AZStd::string> FailureMessage(AZStd::string message)
|
|
{
|
|
return AZ::Failure(AZStd::string(message));
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::HandleReleasedCommon(const AZ::Vector2& point)
|
|
{
|
|
if (m_isHandlingEvents)
|
|
{
|
|
UiInteractableComponent::TriggerReleasedAction();
|
|
|
|
bool transferHover = (point == AZ::Vector2(-1.0f, -1.0f));
|
|
|
|
if (!m_expanded)
|
|
{
|
|
if (m_expandOnHover)
|
|
{
|
|
m_expandedByClick = true;
|
|
}
|
|
Expand(transferHover);
|
|
}
|
|
else
|
|
{
|
|
// Only collapse if it's not an expand on hover dropdown or if it was expanded
|
|
// by a click if it is an expand on hover dropdown
|
|
if (!m_expandOnHover || m_expandedByClick)
|
|
{
|
|
Collapse(transferHover);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_isPressed = false;
|
|
|
|
return m_isHandlingEvents;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::HandleCanvasReleasedCommon(AZ::EntityId entityId, bool positionalInput)
|
|
{
|
|
if (m_expanded)
|
|
{
|
|
// Collapse the menu in the following cases:
|
|
// - the user clicked on the dropdown button
|
|
// - the user clicked on an option
|
|
// - the user clicked outside the dropdown
|
|
|
|
// If the user clicked on the dropdown button
|
|
if (entityId == GetEntityId())
|
|
{
|
|
// Let HandleReleasedCommon handle it
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
bool transferHover = !positionalInput;
|
|
|
|
// Get the dropdown the option belongs to
|
|
AZ::EntityId owningDropdown;
|
|
EBUS_EVENT_ID_RESULT(owningDropdown, entityId, UiDropdownOptionBus, GetOwningDropdown);
|
|
// If one of our options was clicked
|
|
if (owningDropdown == GetEntityId())
|
|
{
|
|
Collapse(transferHover);
|
|
return;
|
|
}
|
|
else if (m_collapseOnOutsideClick)
|
|
{
|
|
if (entityId != m_content)
|
|
{
|
|
// Figure out if the clicked entity is a descendant of either our content, or one of our
|
|
// submenus content
|
|
bool contentIsAncestor = ContentIsAncestor(entityId);
|
|
// If it was not an ancestor, then we clicked outside the dropdown
|
|
if (!contentIsAncestor)
|
|
{
|
|
Collapse(transferHover);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void UiDropdownComponent::TransferHoverToDescendant()
|
|
{
|
|
// Find the first descendant interactable of the content element
|
|
AZ::EntityId descendantInteractable = FindFirstDescendantInteractable(m_content);
|
|
if (descendantInteractable.IsValid())
|
|
{
|
|
EBUS_EVENT_ID(m_canvasEntityId, UiCanvasBus, ForceHoverInteractable, descendantInteractable);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::FindFirstDescendantInteractable(AZ::EntityId parentEntityId)
|
|
{
|
|
AZ::EntityId firstDescendant;
|
|
|
|
AZStd::vector<AZ::EntityId> childEntityIds;
|
|
EBUS_EVENT_ID_RESULT(childEntityIds, parentEntityId, UiElementBus, GetChildEntityIds);
|
|
|
|
for (auto childEntityId : childEntityIds)
|
|
{
|
|
if (UiNavigationHelpers::IsElementInteractableAndNavigable(childEntityId))
|
|
{
|
|
firstDescendant = childEntityId;
|
|
break;
|
|
}
|
|
|
|
firstDescendant = FindFirstDescendantInteractable(childEntityId);
|
|
if (firstDescendant.IsValid())
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
return firstDescendant;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZ::EntityId UiDropdownComponent::CreateContentParentInteractable()
|
|
{
|
|
AZ::Entity* button = nullptr;
|
|
if (m_expandedParentId.IsValid())
|
|
{
|
|
EBUS_EVENT_ID_RESULT(button, m_expandedParentId, UiElementBus, CreateChildElement, "InternalContentParentInteractable");
|
|
}
|
|
else
|
|
{
|
|
EBUS_EVENT_ID_RESULT(button, m_canvasEntityId, UiCanvasBus, CreateChildElement, "InternalContentParentInteractable");
|
|
}
|
|
|
|
AZ::EntityId buttonId;
|
|
if (button)
|
|
{
|
|
// Set up the button element
|
|
button->Deactivate();
|
|
button->CreateComponent(LyShine::UiTransform2dComponentUuid);
|
|
button->CreateComponent(LyShine::UiButtonComponentUuid);
|
|
button->Activate();
|
|
|
|
buttonId = button->GetId();
|
|
|
|
AZ_Assert(UiTransform2dBus::FindFirstHandler(buttonId), "Transform2d component missing");
|
|
|
|
UiTransform2dInterface::Anchors anchors(0.5f, 0.5f, 0.5f, 0.5f);
|
|
UiTransform2dInterface::Offsets offsets(0.0f, 0.0f, 0.0f, 0.0f);
|
|
AZ::Vector2 pivot(0.5f, 0.5f);
|
|
EBUS_EVENT_ID(buttonId, UiTransform2dBus, SetAnchors, anchors, false, false);
|
|
EBUS_EVENT_ID(buttonId, UiTransform2dBus, SetOffsets, offsets);
|
|
EBUS_EVENT_ID(buttonId, UiTransformBus, SetPivot, pivot);
|
|
|
|
UiTransformInterface::RectPoints contentPoints;
|
|
EBUS_EVENT_ID(m_content, UiTransformBus, GetViewportSpacePoints, contentPoints);
|
|
EBUS_EVENT_ID(buttonId, UiTransformBus, SetViewportPosition, contentPoints.GetCenter());
|
|
}
|
|
|
|
return buttonId;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::ContentIsAncestor(AZ::EntityId entityId)
|
|
{
|
|
return ContentIsAncestor(entityId, m_content);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::ContentIsAncestor(AZ::EntityId entityId, AZ::EntityId contentId)
|
|
{
|
|
bool contentIsAncestor = false;
|
|
EBUS_EVENT_ID_RESULT(contentIsAncestor, entityId, UiElementBus, IsAncestor, contentId);
|
|
if (contentIsAncestor)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
for (auto submenu : m_submenus)
|
|
{
|
|
AZ::EntityId submenuContent;
|
|
EBUS_EVENT_ID_RESULT(submenuContent, submenu->GetId(), UiDropdownBus, GetContent);
|
|
bool submenuIsAncestor = false;
|
|
EBUS_EVENT_ID_RESULT(submenuIsAncestor, entityId, UiElementBus, IsAncestor, submenuContent);
|
|
if (submenuIsAncestor)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
bool UiDropdownComponent::IsNavigationSupported()
|
|
{
|
|
bool isNavigationSupported = false;
|
|
EBUS_EVENT_ID_RESULT(isNavigationSupported, m_canvasEntityId, UiCanvasBus, GetIsNavigationSupported);
|
|
|
|
return isNavigationSupported;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
UiInteractableStatesInterface::State UiDropdownComponent::ComputeInteractableState()
|
|
{
|
|
UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal;
|
|
|
|
if (!m_isHandlingEvents)
|
|
{
|
|
state = UiInteractableStatesInterface::StateDisabled;
|
|
}
|
|
else if (m_isPressed)
|
|
{
|
|
state = UiInteractableStatesInterface::StatePressed;
|
|
}
|
|
else if (m_isHover)
|
|
{
|
|
state = UiInteractableStatesInterface::StateHover;
|
|
}
|
|
else if (m_expanded)
|
|
{
|
|
state = DropdownStateExpanded;
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|