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.
o3de/Gems/LyShine/Code/Source/UiDraggableComponent.cpp

916 lines
35 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 "UiDraggableComponent.h"
#include <AzCore/Math/Crc.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <LyShine/Bus/UiElementBus.h>
#include <LyShine/Bus/UiTransformBus.h>
#include <LyShine/Bus/UiCanvasBus.h>
#include <LyShine/Bus/UiCanvasManagerBus.h>
#include <LyShine/Bus/UiDropTargetBus.h>
#include <LyShine/Bus/UiInteractionMaskBus.h>
#include <LyShine/Bus/UiNavigationBus.h>
#include "UiNavigationHelpers.h"
////////////////////////////////////////////////////////////////////////////////////////////////////
//! UiDraggableNotificationBus Behavior context handler class
class UiDraggableNotificationBusBehaviorHandler
: public UiDraggableNotificationBus::Handler
, public AZ::BehaviorEBusHandler
{
public:
AZ_EBUS_BEHAVIOR_BINDER(UiDraggableNotificationBusBehaviorHandler, "{7EEA2A71-AB29-4F1D-AC76-4BE7237AB99B}", AZ::SystemAllocator,
OnDragStart, OnDrag, OnDragEnd);
void OnDragStart(AZ::Vector2 position) override
{
Call(FN_OnDragStart, position);
}
void OnDrag(AZ::Vector2 position) override
{
Call(FN_OnDrag, position);
}
void OnDragEnd(AZ::Vector2 position) override
{
Call(FN_OnDragEnd, position);
}
};
////////////////////////////////////////////////////////////////////////////////////////////////////
// PUBLIC MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
UiDraggableComponent::UiDraggableComponent()
{
// Must be called in the same order as the states defined in UiDraggableInterface
m_stateActionManager.AddState(&m_dragNormalStateActions);
m_stateActionManager.AddState(&m_dragValidStateActions);
m_stateActionManager.AddState(&m_dragInvalidStateActions);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
UiDraggableComponent::~UiDraggableComponent()
{
// 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();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDraggableComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive)
{
bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive);
if (handled)
{
// NOTE: Drag start does not happen until the mouse actually starts moving so HandlePressed does
// not do much. Reset these member variables just in case they did not get reset in end drag
m_isDragging = false;
m_dragState = DragState::Normal;
m_hoverDropTarget.SetInvalid();
}
return handled;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDraggableComponent::HandleReleased(AZ::Vector2 point)
{
EndDragOperation(point, false);
if (m_isPressed && m_isHandlingEvents)
{
UiInteractableComponent::TriggerReleasedAction();
}
m_isPressed = false;
m_pressedPoint = AZ::Vector2(0.0f, 0.0f);
return m_isHandlingEvents;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDraggableComponent::HandleEnterPressed(bool& shouldStayActive)
{
bool handled = UiInteractableComponent::HandleEnterPressed(shouldStayActive);
if (handled)
{
AZ::Vector2 point(0.0f, 0.0f);
EBUS_EVENT_ID_RESULT(point, GetEntityId(), UiTransformBus, GetViewportSpacePivot);
// if we are not yet in the dragging state do some tests to see if we should be
if (!m_isDragging)
{
// the draggable will stay active after released so that arrow keys can be used to place it
// over a drop target
shouldStayActive = true;
m_isActive = true;
// the drag was valid for this draggable, we are now dragging
m_isDragging = true;
m_dragState = DragState::Normal;
EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, point);
m_hoverDropTarget.SetInvalid();
// find closest drop target to the draggable's center
AZ::EntityId closestDropTarget = FindClosestNavigableDropTarget();
if (closestDropTarget.IsValid())
{
EBUS_EVENT_ID_RESULT(point, closestDropTarget, UiTransformBus, GetViewportPosition);
}
DoDrag(point, true);
}
}
return handled;
}
/////////////////////////////////////////////////////////////////
bool UiDraggableComponent::HandleKeyInputBegan(const AzFramework::InputChannel::Snapshot& inputSnapshot, AzFramework::ModifierKeyMask activeModifierKeys)
{
if (!m_isHandlingEvents)
{
return false;
}
// don't accept key input while in pressed state
if (m_isPressed)
{
return false;
}
bool result = false;
const UiNavigationHelpers::Command command = UiNavigationHelpers::MapInputChannelIdToUiNavigationCommand(inputSnapshot.m_channelId, activeModifierKeys);
if (command == UiNavigationHelpers::Command::Up ||
command == UiNavigationHelpers::Command::Down ||
command == UiNavigationHelpers::Command::Left ||
command == UiNavigationHelpers::Command::Right)
{
AZ::EntityId closestDropTarget = FindClosestNavigableDropTarget();
AZ::EntityId newElement;
if (m_hoverDropTarget.IsValid())
{
LyShine::EntityArray navigableElements;
FindNavigableDropTargetElements(m_hoverDropTarget, navigableElements);
auto isValidDropTarget = [](AZ::EntityId entityId)
{
bool isEnabled = false;
EBUS_EVENT_ID_RESULT(isEnabled, entityId, UiElementBus, IsEnabled);
if (isEnabled && UiDropTargetBus::FindFirstHandler(entityId))
{
return true;
}
return false;
};
newElement = UiNavigationHelpers::GetNextElement(m_hoverDropTarget, command,
navigableElements, closestDropTarget, isValidDropTarget);
}
else
{
// find closest drop target to the draggable's center
newElement = closestDropTarget;
}
if (newElement.IsValid())
{
AZ::Vector2 point(0.0f, 0.0f);
EBUS_EVENT_ID_RESULT(point, newElement, UiTransformBus, GetViewportSpacePivot);
DoDrag(point, true);
}
result = true;
}
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::InputPositionUpdate(AZ::Vector2 point)
{
if (m_isPressed)
{
// if we are not yet in the dragging state do some tests to see if we should be
if (!m_isDragging)
{
bool handOffDone = false;
bool dragDetected = CheckForDragOrHandOffToParent(GetEntityId(), m_pressedPoint, point, 0.0f, handOffDone);
if (dragDetected)
{
if (handOffDone)
{
// the drag was handed off to a parent, this draggable is no longer active
m_isPressed = false;
}
else
{
// the drag was valid for this draggable, we are now dragging
m_isDragging = true;
m_dragState = DragState::Normal;
EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, point);
m_hoverDropTarget.SetInvalid();
}
}
}
// if we are now in the dragging state do the drag update and handle start/end of drop hover
if (m_isDragging)
{
DoDrag(point, false);
}
}
}
/////////////////////////////////////////////////////////////////
bool UiDraggableComponent::DoesSupportDragHandOff(AZ::Vector2 startPoint)
{
// this component does support hand-off, so long as the start point is in its bounds
// i.e. if there is a child interactable element such as a button or checkbox and the user
// drags it, then the drag can get handed off to the parent draggable element
bool isPointInRect = false;
EBUS_EVENT_ID_RESULT(isPointInRect, GetEntityId(), UiTransformBus, IsPointInRect, startPoint);
return isPointInRect;
}
/////////////////////////////////////////////////////////////////
bool UiDraggableComponent::OfferDragHandOff(AZ::EntityId currentActiveInteractable, AZ::Vector2 startPoint, AZ::Vector2 currentPoint, float dragThreshold)
{
// A child interactable element is offering to hand-off a drag interaction to this element
bool handedOffToParent = false;
bool dragDetected = CheckForDragOrHandOffToParent(currentActiveInteractable, startPoint, currentPoint, dragThreshold, handedOffToParent);
if (dragDetected)
{
if (!handedOffToParent)
{
// a drag was detected and it was not handed off to a parent, so this draggable is now taking the handoff
m_isPressed = true;
m_pressedPoint = startPoint;
// tell the canvas that this is now the active interactable
EBUS_EVENT_ID(currentActiveInteractable, UiInteractableActiveNotificationBus, ActiveChanged, GetEntityId(), false);
// start the drag
m_isDragging = true;
m_dragState = DragState::Normal;
EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, currentPoint);
m_hoverDropTarget.SetInvalid();
// Send the OnDrag and any OnDropHoverStart immediately so that it doesn't require another frame to
// update.
DoDrag(currentPoint, false);
}
}
return dragDetected;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::LostActiveStatus()
{
// this is called when keyboard or console operation is being used and Enter was used to end the
// operation.
UiInteractableComponent::LostActiveStatus();
AZ::Vector2 viewportPoint;
EBUS_EVENT_ID_RESULT(viewportPoint, GetEntityId(), UiTransformBus, GetViewportSpacePivot);
EndDragOperation(viewportPoint, true);
m_isActive = false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
UiDraggableInterface::DragState UiDraggableComponent::GetDragState()
{
return m_dragState;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::SetDragState(DragState dragState)
{
m_dragState = dragState;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::RedoDrag(AZ::Vector2 point)
{
DoDrag(point, true);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::SetAsProxy(AZ::EntityId originalDraggableId, AZ::Vector2 point)
{
// find the originalDraggable by Id
AZ::Entity* originalDraggable = nullptr;
EBUS_EVENT_RESULT(originalDraggable, AZ::ComponentApplicationBus, FindEntity, originalDraggableId);
if (!originalDraggable)
{
AZ_Warning("UI", false, "SetAsProxy: Cannot find original draggable");
return;
}
// Find the UiDraggableComponent on the originalDraggable
UiDraggableComponent* originalComponent = originalDraggable->FindComponent<UiDraggableComponent>();
if (!originalComponent)
{
AZ_Warning("UI", false, "SetAsProxy: Cannot find draggable component");
return;
}
// Set the isProxyFor member variable, this indicates that this is a proxy
m_isProxyFor = originalDraggableId;
// put this draggable into the drag state and copy some of the state from the original
m_isPressed = true;
m_pressedPoint = originalComponent->m_pressedPoint;
m_isActive = originalComponent->m_isActive;
// tell the proxy draggable's canvas that this is now the active interactable
AZ::EntityId canvasEntityId;
EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);
EBUS_EVENT_ID(canvasEntityId, UiCanvasBus, ForceActiveInteractable, GetEntityId(), m_isActive, m_pressedPoint);
// start the drag on the proxy
m_isDragging = true;
m_dragState = DragState::Normal;
EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragStart, point);
m_hoverDropTarget.SetInvalid();
// Send the OnDrag and any OnDropHoverStart immediately so that it doesn't require another frame to
// update.
DoDrag(point, false);
// Turn of these flags on the original, this stops it responding to HandleReleased, InputPositionUpdate, etc
// If the original is on a different canvas to the proxy then the original withh still get these functions called.
// They just won't do anything.
originalComponent->m_isDragging = false;
originalComponent->m_isPressed = false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::ProxyDragEnd(AZ::Vector2 point)
{
AZ::Entity* originalDraggable = nullptr;
EBUS_EVENT_RESULT(originalDraggable, AZ::ComponentApplicationBus, FindEntity, m_isProxyFor);
if (!originalDraggable)
{
AZ_Warning("UI", false, "ProxyDragEnd: Cannot find original draggable");
return;
}
UiDraggableComponent* originalComponent = originalDraggable->FindComponent<UiDraggableComponent>();
if (!originalComponent)
{
AZ_Warning("UI", false, "ProxyDragEnd: Cannot find draggable component on original");
return;
}
// we don't want the proxy to get in the way of the search for a drop target under the original
// draggable so disable interaction on it
m_isHandlingEvents = false;
originalComponent->m_isPressed = true;
originalComponent->m_isDragging = true;
originalComponent->HandleReleased(point);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDraggableComponent::IsProxy()
{
return (m_isProxyFor.IsValid()) ? true : false;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDraggableComponent::GetOriginalFromProxy()
{
return m_isProxyFor;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDraggableComponent::GetCanDropOnAnyCanvas()
{
return m_canDropOnAnyCanvas;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::SetCanDropOnAnyCanvas(bool anyCanvas)
{
m_canDropOnAnyCanvas = anyCanvas;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// PROTECTED MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::Activate()
{
UiInteractableComponent::Activate();
UiDraggableBus::Handler::BusConnect(m_entity->GetId());
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::Deactivate()
{
UiInteractableComponent::Deactivate();
UiDraggableBus::Handler::BusDisconnect();
}
////////////////////////////////////////////////////////////////////////////////////////////////////
UiInteractableStatesInterface::State UiDraggableComponent::ComputeInteractableState()
{
UiInteractableStatesInterface::State state = UiInteractableStatesInterface::StateNormal;
if (!m_isHandlingEvents)
{
state = UiInteractableStatesInterface::StateDisabled;
}
else if (m_isDragging)
{
switch (m_dragState)
{
case DragState::Normal:
state = StateDragNormal;
break;
case DragState::Valid:
state = StateDragValid;
break;
case DragState::Invalid:
state = StateDragInvalid;
break;
}
}
else if (m_isPressed || m_isActive)
{
// To support keyboard/console we stay in pressed state when active
state = UiInteractableStatesInterface::StatePressed;
}
else if (m_isHover)
{
state = UiInteractableStatesInterface::StateHover;
}
return state;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::OnDragNormalStateActionsChanged()
{
m_stateActionManager.InitInteractableEntityForStateActions(m_dragNormalStateActions);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::OnDragValidStateActionsChanged()
{
m_stateActionManager.InitInteractableEntityForStateActions(m_dragValidStateActions);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::OnDragInvalidStateActionsChanged()
{
m_stateActionManager.InitInteractableEntityForStateActions(m_dragInvalidStateActions);
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// PROTECTED STATIC MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::Reflect(AZ::ReflectContext* context)
{
AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);
if (serializeContext)
{
serializeContext->Class<UiDraggableComponent, UiInteractableComponent>()
->Version(1)
->Field("DragNormalStateActions", &UiDraggableComponent::m_dragNormalStateActions)
->Field("DragValidStateActions", &UiDraggableComponent::m_dragValidStateActions)
->Field("DragInvalidStateActions", &UiDraggableComponent::m_dragInvalidStateActions);
AZ::EditContext* ec = serializeContext->GetEditContext();
if (ec)
{
auto editInfo = ec->Class<UiDraggableComponent>("Draggable", "An interactable component for drag and drop behavior");
editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
->Attribute(AZ::Edit::Attributes::Category, "UI")
->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiDraggable.png")
->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiDraggable.png")
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Drag States")
->Attribute(AZ::Edit::Attributes::AutoExpand, true);
editInfo->DataElement(0, &UiDraggableComponent::m_dragNormalStateActions, "Normal", "The normal drag state actions")
->Attribute(AZ::Edit::Attributes::AddNotify, &UiDraggableComponent::OnDragNormalStateActionsChanged);
editInfo->DataElement(0, &UiDraggableComponent::m_dragValidStateActions, "Valid", "The valid drag state actions")
->Attribute(AZ::Edit::Attributes::AddNotify, &UiDraggableComponent::OnDragValidStateActionsChanged);
editInfo->DataElement(0, &UiDraggableComponent::m_dragInvalidStateActions, "Invalid", "The invalid drag state actions")
->Attribute(AZ::Edit::Attributes::AddNotify, &UiDraggableComponent::OnDragInvalidStateActionsChanged);
}
}
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
if (behaviorContext)
{
behaviorContext->Enum<(int)UiDraggableInterface::DragState::Normal>("eUiDragState_Normal")
->Enum<(int)UiDraggableInterface::DragState::Valid>("eUiDragState_Valid")
->Enum<(int)UiDraggableInterface::DragState::Invalid>("eUiDragState_Invalid");
behaviorContext->EBus<UiDraggableBus>("UiDraggableBus")
->Event("GetDragState", &UiDraggableBus::Events::GetDragState)
->Event("SetDragState", &UiDraggableBus::Events::SetDragState)
->Event("RedoDrag", &UiDraggableBus::Events::RedoDrag)
->Event("SetAsProxy", &UiDraggableBus::Events::SetAsProxy)
->Event("ProxyDragEnd", &UiDraggableBus::Events::ProxyDragEnd)
->Event("IsProxy", &UiDraggableBus::Events::IsProxy)
->Event("GetOriginalFromProxy", &UiDraggableBus::Events::GetOriginalFromProxy)
->Event("GetCanDropOnAnyCanvas", &UiDraggableBus::Events::GetCanDropOnAnyCanvas)
->Event("SetCanDropOnAnyCanvas", &UiDraggableBus::Events::SetCanDropOnAnyCanvas);
behaviorContext->EBus<UiDraggableNotificationBus>("UiDraggableNotificationBus")
->Handler<UiDraggableNotificationBusBehaviorHandler>();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDraggableComponent::GetDropTargetUnderDraggable(AZ::Vector2 point, bool ignoreInteractables)
{
AZ::EntityId result;
AZ::EntityId canvasEntity;
EBUS_EVENT_ID_RESULT(canvasEntity, GetEntityId(), UiElementBus, GetCanvasEntityId);
// We will ignore this element and all its children in the search
AZ::EntityId ignoreElement = GetEntityId();
// Look for a drop target under the mouse position
// recursively check the children of the canvas (in reverse order since children are in front of parent)
if (m_canDropOnAnyCanvas)
{
result = FindDropTargetOrInteractableOnAllCanvases(point, ignoreElement, ignoreInteractables);
}
else
{
result = FindDropTargetOrInteractableOnCanvas(canvasEntity, point, ignoreElement, ignoreInteractables);
}
// The result could be an interactable that is not a drop target since an interactable in front of a drop target
// can block dropping on it (unless it is the child of the drop target)
if (!UiDropTargetBus::FindFirstHandler(result))
{
result.SetInvalid();
}
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDraggableComponent::CheckForDragOrHandOffToParent([[maybe_unused]] AZ::EntityId currentActiveInteractable, AZ::Vector2 startPoint, AZ::Vector2 currentPoint, float childDragThreshold, [[maybe_unused]] bool& handOffDone)
{
// Currently a draggable never hands off the drag to a parent since a drag in any direction is valid.
// Potentially this could change if we allowed, for example, a scroll box containing draggables where
// dragging up and down scrolled the scroll box and dragging left and right initiated drag and drop.
// In that case we would need a property to say in which direction a draggable can be dragged.
bool result = false;
// Possibly this should be a user defined property since it defines how much movement constitutes a drag start
const float normalDragThreshold = 3.0f;
float dragThreshold = normalDragThreshold;
if (childDragThreshold > 0.0f)
{
dragThreshold = childDragThreshold;
}
float dragThresholdSq = dragThreshold * dragThreshold;
// calculate how much we have dragged
AZ::Vector2 dragVector = currentPoint - startPoint;
float dragDistanceSq = dragVector.GetLengthSq();
if (dragDistanceSq > dragThresholdSq)
{
// we dragged above the threshold value
result = true;
}
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::DoDrag(AZ::Vector2 viewportPoint, bool ignoreInteractables)
{
// In the case where a proxy has been created in the OnDragStart handler we would no longer
// be in the dragging state, in that case do nothing here
if (!m_isDragging)
{
return;
}
// Send the OnDrag notification
EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDrag, viewportPoint);
AZ::EntityId dropEntity = GetDropTargetUnderDraggable(viewportPoint, ignoreInteractables);
// if we have a drop hover entity and we are no longer hovering over it
if (m_hoverDropTarget.IsValid() && m_hoverDropTarget != dropEntity)
{
// end the drop hover
EBUS_EVENT_ID(m_hoverDropTarget, UiDropTargetBus, HandleDropHoverEnd, GetEntityId());
m_hoverDropTarget.SetInvalid();
}
// if we do not have a drop hover entity and we are hovering over a drop target
if (!m_hoverDropTarget.IsValid() && dropEntity.IsValid())
{
// start a drop hover
EBUS_EVENT_ID(dropEntity, UiDropTargetBus, HandleDropHoverStart, GetEntityId());
m_hoverDropTarget = dropEntity;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::EndDragOperation(AZ::Vector2 viewportPoint, bool ignoreInteractables)
{
if (m_isDragging)
{
// If we were hovering over a drop target then forget it, we will recompute what we are over now
if (m_hoverDropTarget.IsValid())
{
EBUS_EVENT_ID(m_hoverDropTarget, UiDropTargetBus, HandleDropHoverEnd, GetEntityId());
m_hoverDropTarget.SetInvalid();
}
// Search for a drop target before calling OnDragEnd in case OnDragEnd moves the drop target that we are over
AZ::EntityId dropEntity = GetDropTargetUnderDraggable(viewportPoint, ignoreInteractables);
// send a drag end notification
EBUS_QUEUE_EVENT_ID(GetEntityId(), UiDraggableNotificationBus, OnDragEnd, viewportPoint);
// If there was a drop target under the cursor then send it a message to handle this draggable being dropped on it
if (dropEntity.IsValid())
{
EBUS_EVENT_ID(dropEntity, UiDropTargetBus, HandleDrop, GetEntityId());
}
m_isDragging = false;
m_dragState = DragState::Normal;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDraggableComponent::FindNavigableDropTargetElements(AZ::EntityId ignoreElement, LyShine::EntityArray& result)
{
AZ::EntityId canvasEntity;
EBUS_EVENT_ID_RESULT(canvasEntity, GetEntityId(), UiElementBus, GetCanvasEntityId);
LyShine::EntityArray elements;
EBUS_EVENT_ID_RESULT(elements, canvasEntity, UiCanvasBus, GetChildElements);
AZStd::list<AZ::Entity*> elementList(elements.begin(), elements.end());
while (!elementList.empty())
{
auto entity = elementList.front();
elementList.pop_front();
if (ignoreElement.IsValid() && entity->GetId() == ignoreElement)
{
continue; // this is the element to ignore, ignore its children also
}
// Check if the element is enabled
bool isEnabled = false;
EBUS_EVENT_ID_RESULT(isEnabled, entity->GetId(), UiElementBus, IsEnabled);
if (!isEnabled)
{
continue;
}
bool isDropTarget = false;
if (UiDropTargetBus::FindFirstHandler(entity->GetId()))
{
isDropTarget = true;
}
UiNavigationInterface::NavigationMode navigationMode = UiNavigationInterface::NavigationMode::None;
EBUS_EVENT_ID_RESULT(navigationMode, entity->GetId(), UiNavigationBus, GetNavigationMode);
bool isNavigable = (navigationMode != UiNavigationInterface::NavigationMode::None);
if (isDropTarget && isNavigable)
{
result.push_back(entity);
}
else
{
LyShine::EntityArray childElements;
EBUS_EVENT_ID_RESULT(childElements, entity->GetId(), UiElementBus, GetChildElements);
elementList.insert(elementList.end(), childElements.begin(), childElements.end());
}
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDraggableComponent::FindClosestNavigableDropTarget()
{
UiTransformInterface::RectPoints srcPoints;
EBUS_EVENT_ID(GetEntityId(), UiTransformBus, GetViewportSpacePoints, srcPoints);
AZ::Vector2 srcCenter = srcPoints.GetCenter();
LyShine::EntityArray dropTargets;
FindNavigableDropTargetElements(AZ::EntityId(), dropTargets);
float shortestDist = FLT_MAX;
AZ::EntityId closestElement;
for (auto dropTarget : dropTargets)
{
UiTransformInterface::RectPoints destPoints;
EBUS_EVENT_ID(dropTarget->GetId(), UiTransformBus, GetViewportSpacePoints, destPoints);
AZ::Vector2 destCenter = destPoints.GetCenter();
float dist = (destCenter - srcCenter).GetLengthSq();
if (dist < shortestDist)
{
shortestDist = dist;
closestElement = dropTarget->GetId();
}
}
return closestElement;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE STATIC FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDraggableComponent::FindDropTargetOrInteractableOnAllCanvases(
AZ::Vector2 point, AZ::EntityId ignoreElement, bool ignoreInteractables)
{
AZ::EntityId result;
UiCanvasManagerInterface::CanvasEntityList canvases;
EBUS_EVENT_RESULT(canvases, UiCanvasManagerBus, GetLoadedCanvases);
// reverse iterate over the loaded canvases so that the front most canvas gets first chance to
// handle the event
for (auto iter = canvases.rbegin(); iter != canvases.rend() && !result.IsValid(); ++iter)
{
AZ::EntityId canvasEntityId = *iter;
result = FindDropTargetOrInteractableOnCanvas(canvasEntityId, point, ignoreElement, ignoreInteractables);
}
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDraggableComponent::FindDropTargetOrInteractableOnCanvas(AZ::EntityId canvasEntityId,
AZ::Vector2 point, AZ::EntityId ignoreElement, bool ignoreInteractables)
{
AZ::EntityId result;
// recursively check the children of the canvas (in reverse order since children are in front of parent)
int numChildren = 0;
EBUS_EVENT_ID_RESULT(numChildren, canvasEntityId, UiCanvasBus, GetNumChildElements);
for (int i = numChildren - 1; !result.IsValid() && i >= 0; i--)
{
AZ::EntityId child;
EBUS_EVENT_ID_RESULT(child, canvasEntityId, UiCanvasBus, GetChildElementEntityId, i);
if (child != ignoreElement)
{
result = FindDropTargetOrInteractableUnderCursor(child, point, ignoreElement, ignoreInteractables);
}
}
return result;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDraggableComponent::FindDropTargetOrInteractableUnderCursor(AZ::EntityId element,
AZ::Vector2 point, AZ::EntityId ignoreElement, bool ignoreInteractables)
{
AZ::EntityId result;
bool isEnabled = false;
EBUS_EVENT_ID_RESULT(isEnabled, element, UiElementBus, IsEnabled);
if (!isEnabled)
{
// Nothing to do
return result;
}
// first check the children (in reverse order since children are in front of parent)
{
// if this element is masking children at this point then don't check the children
bool isMasked = false;
EBUS_EVENT_ID_RESULT(isMasked, element, UiInteractionMaskBus, IsPointMasked, point);
if (!isMasked)
{
int numChildren = 0;
EBUS_EVENT_ID_RESULT(numChildren, element, UiElementBus, GetNumChildElements);
for (int i = numChildren - 1; !result.IsValid() && i >= 0; i--)
{
AZ::EntityId child;
EBUS_EVENT_ID_RESULT(child, element, UiElementBus, GetChildEntityId, i);
if (child != ignoreElement)
{
result = FindDropTargetOrInteractableUnderCursor(child, point, ignoreElement, ignoreInteractables);
}
}
}
}
// if no match then check this element
if (!result.IsValid())
{
// if the point is in this element's rect
bool isInRect = false;
EBUS_EVENT_ID_RESULT(isInRect, element, UiTransformBus, IsPointInRect, point);
if (isInRect)
{
// If this element has a drop target component
if (UiDropTargetBus::FindFirstHandler(element))
{
// this is the drop target under the cursor
result = element;
}
// else if this element has an interactable component
else if (!ignoreInteractables && UiInteractableBus::FindFirstHandler(element))
{
// check if this interactable component is in a state where it can handle an event at the given point
bool canHandle = false;
EBUS_EVENT_ID_RESULT(canHandle, element, UiInteractableBus, CanHandleEvent, point);
if (canHandle)
{
// in this case the interaction is blocked unless this interactable has a parent that is
// a drop target
AZ::EntityId parent;
EBUS_EVENT_ID_RESULT(parent, element, UiElementBus, GetParentEntityId);
while (parent.IsValid())
{
bool isInParentRect = false;
EBUS_EVENT_ID_RESULT(isInParentRect, parent, UiTransformBus, IsPointInRect, point);
if (isInParentRect && UiDropTargetBus::FindFirstHandler(parent))
{
// We found a parent drop target and the cursor is in its rect,
// this is considered the drop target under the cursor
result = parent;
break;
}
EBUS_EVENT_ID_RESULT(parent, parent, UiElementBus, GetParentEntityId);
}
if (!result.IsValid())
{
// no parent drop target was found, return this blocking interactable
result = element;
}
}
}
}
}
return result;
}