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/Editor/ViewportInteraction.cpp

1548 lines
61 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 "EditorCommon.h"
#include "ViewportNudge.h"
#include "ViewportElement.h"
#include "QtHelpers.h"
#include "GuideHelpers.h"
#include "AlignToolbarSection.h"
#include "ViewportMoveInteraction.h"
#include "ViewportMoveGuideInteraction.h"
#include <LyShine/UiComponentTypes.h>
#include <LyShine/Bus/UiEditorCanvasBus.h>
#include <AzCore/Casting/numeric_cast.h>
#include <AzQtComponents/Components/Widgets/ToolBar.h>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QToolButton>
#include <QTextDocumentFragment>
#include <QSettings>
#include <Editor/Resource.h>
#include <Editor/Util/EditorUtils.h>
static const float g_elementEdgeForgiveness = 10.0f;
// The square of the minimum corner-to-corner distance for an area selection
static const float g_minAreaSelectionDistance2 = 100.0f;
#define UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_INTERACTION_MODE_KEY "ViewportWidget::m_interactionMode"
#define UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_INTERACTION_MODE_DEFAULT ( ViewportInteraction::InteractionMode::SELECTION )
#define UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_COORDINATE_SYSTEM_KEY "ViewportWidget::m_coordinateSystem"
#define UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_COORDINATE_SYSTEM_DEFAULT ( ViewportInteraction::CoordinateSystem::LOCAL )
namespace
{
const float defaultCanvasToViewportScaleIncrement = 0.20f;
ViewportInteraction::InteractionMode PersistentGetInteractionMode()
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
int defaultMode = static_cast<int>(UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_INTERACTION_MODE_DEFAULT);
int result = settings.value(
UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_INTERACTION_MODE_KEY,
defaultMode).toInt();
settings.endGroup();
return static_cast<ViewportInteraction::InteractionMode>(result);
}
void PersistentSetInteractionMode(ViewportInteraction::InteractionMode mode)
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
settings.setValue(UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_INTERACTION_MODE_KEY,
static_cast<int>(mode));
settings.endGroup();
}
ViewportInteraction::CoordinateSystem PersistentGetCoordinateSystem()
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
int defaultSystem = static_cast<int>(UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_COORDINATE_SYSTEM_DEFAULT);
int result = settings.value(
UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_COORDINATE_SYSTEM_KEY,
defaultSystem).toInt();
settings.endGroup();
return static_cast<ViewportInteraction::CoordinateSystem>(result);
}
void PersistentSetCoordinateSystem(ViewportInteraction::CoordinateSystem coordinateSystem)
{
QSettings settings(QSettings::IniFormat, QSettings::UserScope, AZ_QCOREAPPLICATION_SETTINGS_ORGANIZATION_NAME);
settings.beginGroup(UICANVASEDITOR_NAME_SHORT);
settings.setValue(UICANVASEDITOR_SETTINGS_VIEWPORTINTERACTION_COORDINATE_SYSTEM_KEY,
static_cast<int>(coordinateSystem));
settings.endGroup();
}
} // anonymous namespace.
class ViewportInteractionExpanderWatcher
: public QObject
{
public:
ViewportInteractionExpanderWatcher(QObject* parent = nullptr)
: QObject(parent)
{
}
bool eventFilter(QObject* obj, QEvent* event) override
{
switch (event->type())
{
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
{
if (qobject_cast<QToolButton*>(obj))
{
auto mouseEvent = static_cast<QMouseEvent*>(event);
auto expansion = qobject_cast<QToolButton*>(obj);
expansion->setPopupMode(QToolButton::InstantPopup);
auto menu = new QMenu(expansion);
auto toolbar = qobject_cast<QToolBar*>(expansion->parentWidget());
for (auto toolbarAction : toolbar->actions())
{
auto actionWidget = toolbar->widgetForAction(toolbarAction);
if (actionWidget && !actionWidget->isVisible() && !toolbarAction->text().isEmpty())
{
QString plainText = QTextDocumentFragment::fromHtml(actionWidget->toolTip()).toPlainText();
toolbarAction->setText(plainText);
menu->addAction(toolbarAction);
}
}
if (menu->actions().count() == 0)
{
QAction* noAction = new QAction(this);
noAction->setEnabled(false);
noAction->setText(tr("Please resize the toolbar to see all the controls."));
menu->addAction(noAction);
}
menu->exec(mouseEvent->globalPos());
return true;
}
break;
}
}
return QObject::eventFilter(obj, event);
}
};
ViewportInteraction::ViewportInteraction(EditorWindow* editorWindow)
: QObject()
, m_editorWindow(editorWindow)
, m_activeElementId()
, m_anchorWhole(new ViewportIcon("Editor/Icons/Viewport/Anchor_Whole.tif"))
, m_pivotIcon(new ViewportIcon("Editor/Icons/Viewport/Pivot.tif"))
, m_interactionMode(PersistentGetInteractionMode())
, m_interactionType(InteractionType::NONE)
, m_coordinateSystem(PersistentGetCoordinateSystem())
, m_spaceBarIsActive(false)
, m_leftButtonIsActive(false)
, m_middleButtonIsActive(false)
, m_reversibleActionStarted(false)
, m_startMouseDragPos(0.0f, 0.0f)
, m_lastMouseDragPos(0.0f, 0.0f)
, m_selectedElementsAtSelectionStart()
, m_canvasViewportMatrixProps()
, m_shouldScaleToFitOnViewportResize(true)
, m_transformComponentType(AZ::Uuid::CreateNull())
, m_grabbedEdges(ViewportHelpers::ElementEdges())
, m_startAnchors(UiTransform2dInterface::Anchors())
, m_grabbedAnchors(ViewportHelpers::SelectedAnchors())
, m_grabbedGizmoParts(ViewportHelpers::GizmoParts())
, m_lineTriangleX(new ViewportIcon("Editor/Icons/Viewport/Transform_Gizmo_Line_Triangle_X.tif"))
, m_lineTriangleY(new ViewportIcon("Editor/Icons/Viewport/Transform_Gizmo_Line_Triangle_Y.tif"))
, m_circle(new ViewportIcon("Editor/Icons/Viewport/Transform_Gizmo_Circle.tif"))
, m_lineSquareX(new ViewportIcon("Editor/Icons/Viewport/Transform_Gizmo_Line_Square_X.tif"))
, m_lineSquareY(new ViewportIcon("Editor/Icons/Viewport/Transform_Gizmo_Line_Square_Y.tif"))
, m_centerSquare(new ViewportIcon("Editor/Icons/Viewport/Transform_Gizmo_Center_Square.tif"))
, m_dottedLine(new ViewportIcon("Editor/Icons/Viewport/DottedLine.tif"))
, m_dragInteraction(nullptr)
, m_expanderWatcher(new ViewportInteractionExpanderWatcher(this))
{
m_cursorRotate = CMFCUtils::LoadCursor(IDC_POINTER_OBJECT_ROTATE);
}
ViewportInteraction::~ViewportInteraction()
{
}
void ViewportInteraction::ClearInteraction(bool clearSpaceBarIsActive)
{
m_activeElementId.SetInvalid();
m_interactionType = InteractionType::NONE;
m_spaceBarIsActive = clearSpaceBarIsActive ? false : m_spaceBarIsActive;
m_leftButtonIsActive = false;
m_middleButtonIsActive = false;
m_startMouseDragPos = AZ::Vector2::CreateZero();
m_lastMouseDragPos = AZ::Vector2::CreateZero();
m_grabbedEdges = ViewportHelpers::ElementEdges();
m_startAnchors = UiTransform2dInterface::Anchors();
m_grabbedAnchors = ViewportHelpers::SelectedAnchors();
m_grabbedGizmoParts = ViewportHelpers::GizmoParts();
m_selectedElementsAtSelectionStart.clear();
m_isAreaSelectionActive = false;
m_reversibleActionStarted = false;
SAFE_DELETE(m_dragInteraction);
}
void ViewportInteraction::Nudge(NudgeDirection direction, NudgeSpeed speed)
{
const AZ::Uuid& transformComponentType = InitAndGetTransformComponentType();
ViewportNudge::Nudge(m_editorWindow,
m_interactionMode,
m_editorWindow->GetViewport(),
direction,
speed,
m_editorWindow->GetHierarchy()->selectedItems(),
m_coordinateSystem,
transformComponentType);
}
void ViewportInteraction::StartObjectPickMode()
{
// Temporarily set the viewport interaction mode to "Selection" and disable the toolbar
m_interactionModeBeforePickMode = GetMode();
SetMode(InteractionMode::SELECTION);
m_editorWindow->GetModeToolbar()->setEnabled(false);
InvalidateHoverElement();
UpdateCursor();
}
void ViewportInteraction::StopObjectPickMode()
{
bool mousePressed = GetLeftButtonIsActive();
m_editorWindow->GetModeToolbar()->setEnabled(true);
SetMode(m_interactionModeBeforePickMode);
SetCursorStr("");
m_hoverElement.SetInvalid();
// Update interaction type and cursor right away if the mouse is already released (user pressed ESC to cancel pick mode)
// instead of waiting for a mouse move/release event
if (!mousePressed)
{
QPoint viewportCursorPos = m_editorWindow->GetViewport()->mapFromGlobal(QCursor::pos());
QTreeWidgetItemRawPtrQList selectedItems = m_editorWindow->GetHierarchy()->selectedItems();
UpdateInteractionType(AZ::Vector2(aznumeric_cast<float>(viewportCursorPos.x()), aznumeric_cast<float>(viewportCursorPos.y())), selectedItems);
}
UpdateCursor();
}
bool ViewportInteraction::GetLeftButtonIsActive()
{
return m_leftButtonIsActive;
}
bool ViewportInteraction::GetSpaceBarIsActive()
{
return m_spaceBarIsActive;
}
void ViewportInteraction::ActivateSpaceBar()
{
m_spaceBarIsActive = true;
UpdateCursor();
if (m_editorWindow->GetViewport()->IsInObjectPickMode())
{
// Don't highlight the hover element during a pan
InvalidateHoverElement();
}
}
void ViewportInteraction::Draw(Draw2dHelper& draw2d,
const QTreeWidgetItemRawPtrQList& selectedItems)
{
// Draw border around hover UI element
if (m_hoverElement.IsValid())
{
m_editorWindow->GetViewport()->GetViewportHighlight()->DrawHover(draw2d, m_hoverElement);
}
// Draw the guide lines
if (m_editorWindow->GetViewport()->AreGuidesShown())
{
GuideHelpers::DrawGuideLines(m_editorWindow->GetCanvas(), m_editorWindow->GetViewport(), draw2d);
}
// Draw the transform gizmo where appropriate
if (m_interactionMode != InteractionMode::SELECTION)
{
LyShine::EntityArray selectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
switch (m_interactionMode)
{
case InteractionMode::MOVE:
case InteractionMode::ANCHOR:
for (auto element : selectedElements)
{
if (!ViewportHelpers::IsControlledByLayout(element))
{
DrawAxisGizmo(draw2d, element, m_coordinateSystem, m_lineTriangleX.get(), m_lineTriangleY.get());
}
}
break;
case InteractionMode::ROTATE:
for (auto element : selectedElements)
{
DrawCircleGizmo(draw2d, element);
}
break;
case InteractionMode::RESIZE:
for (auto element : selectedElements)
{
if (!ViewportHelpers::IsControlledByLayout(element))
{
DrawAxisGizmo(draw2d, element, CoordinateSystem::LOCAL, m_lineSquareX.get(), m_lineSquareY.get());
}
}
break;
}
}
// Draw the area selection, if there is one
if (AreaSelectionIsActive())
{
m_dottedLine->DrawAxisAlignedBoundingBox(draw2d, m_startMouseDragPos, m_lastMouseDragPos);
}
// If there is an active drag interaction give it a chance to render its interaction display
if (m_dragInteraction)
{
m_dragInteraction->Render(draw2d);
}
// Draw the cursor string
if (!m_cursorStr.empty() && m_editorWindow->GetViewport()->underMouse())
{
ViewportHelpers::DrawCursorText(m_cursorStr, draw2d, m_editorWindow->GetViewport());
}
}
bool ViewportInteraction::AreaSelectionIsActive()
{
return m_isAreaSelectionActive;
}
void ViewportInteraction::BeginReversibleAction(const QTreeWidgetItemRawPtrQList& selectedItems)
{
if ((m_reversibleActionStarted) ||
(m_interactionType == InteractionType::NONE || m_interactionType == InteractionType::GUIDE) ||
(m_interactionMode == InteractionMode::SELECTION))
{
// Nothing to do.
return;
}
// we are about to change something and we have not started an undo action yet, start one
m_reversibleActionStarted = true;
// Tell the Properties panel that we're about to do a reversible action
HierarchyClipboard::BeginUndoableEntitiesChange(m_editorWindow, m_selectedEntitiesUndoState);
// Snapping.
bool isSnapping = false;
EBUS_EVENT_ID_RESULT(isSnapping, m_editorWindow->GetCanvas(), UiEditorCanvasBus, GetIsSnapEnabled);
if (isSnapping)
{
// Set all initial non-snapped values.
HierarchyItemRawPtrList items = SelectionHelpers::GetSelectedHierarchyItems(m_editorWindow->GetHierarchy(),
selectedItems);
for (auto i : items)
{
UiTransform2dInterface::Offsets offsets;
EBUS_EVENT_ID_RESULT(offsets, i->GetEntityId(), UiTransform2dBus, GetOffsets);
i->SetNonSnappedOffsets(offsets);
float rotation;
EBUS_EVENT_ID_RESULT(rotation, i->GetEntityId(), UiTransformBus, GetZRotation);
i->SetNonSnappedZRotation(rotation);
}
}
}
void ViewportInteraction::EndReversibleAction()
{
if (!m_reversibleActionStarted)
{
// Nothing to do.
return;
}
m_reversibleActionStarted = false;
if (AreaSelectionIsActive())
{
// Nothing to do.
return;
}
// Note that EndReversibleAction is not used for interactions that handle undo in a m_dragInteraction
// Ideally we will change them all to use m_dragInteraction and handle the undo there.
HierarchyClipboard::EndUndoableEntitiesChange(m_editorWindow, "viewport interaction", m_selectedEntitiesUndoState);
}
void ViewportInteraction::MousePressEvent(QMouseEvent* ev)
{
AZ::Vector2 mousePosition = QtHelpers::QPointFToVector2(ev->localPos());
m_startMouseDragPos = m_lastMouseDragPos = mousePosition;
const bool ctrlKeyPressed = ev->modifiers().testFlag(Qt::ControlModifier);
// Detect whether an entity was picked on the mouse press so that
// mouse move/release events can be handled appropriately
m_entityPickedOnMousePress = false;
// Prepare to handle panning
if ((!m_leftButtonIsActive) &&
(ev->button() == Qt::MiddleButton))
{
m_middleButtonIsActive = true;
}
else if ((!m_middleButtonIsActive) &&
(ev->button() == Qt::LeftButton))
{
// Prepare for clicking and dragging
m_leftButtonIsActive = true;
if (m_activeElementId.IsValid())
{
if (m_grabbedAnchors.Any())
{
// Prepare to move anchors
EBUS_EVENT_ID_RESULT(m_startAnchors, m_activeElementId, UiTransform2dBus, GetAnchors);
}
else if (m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR)
{
// Prepare for moving elements by offsets or anchors
QTreeWidgetItemRawPtrQList selectedItems = m_editorWindow->GetHierarchy()->selectedItems();
m_dragInteraction = new ViewportMoveInteraction(m_editorWindow->GetHierarchy(), selectedItems,
m_editorWindow->GetCanvas(), GetActiveElement(), m_coordinateSystem, m_grabbedGizmoParts, m_interactionMode, m_interactionType, mousePosition);
}
}
else if (m_interactionType == InteractionType::GUIDE)
{
// We are hovering over a guide with the move guide icon displayed so start the move guide interaction
m_dragInteraction = new ViewportMoveGuideInteraction(m_editorWindow, m_editorWindow->GetCanvas(),
m_activeGuideIsVertical, m_activeGuideIndex, mousePosition);
}
}
// If there isn't another interaction happening, try to select an element
if ((!m_spaceBarIsActive && !m_middleButtonIsActive && m_interactionType == InteractionType::NONE) ||
((m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR) && m_interactionType == InteractionType::DIRECT && ctrlKeyPressed))
{
if (m_editorWindow->GetViewport()->IsInObjectPickMode())
{
AZ::Entity* element = nullptr;
EBUS_EVENT_ID_RESULT(element, m_editorWindow->GetCanvas(), UiCanvasBus, PickElement, mousePosition);
m_editorWindow->GetViewport()->PickItem(element ? element->GetId() : AZ::EntityId());
m_entityPickedOnMousePress = true;
}
else
{
QTreeWidgetItemRawPtrQList selectedItems = m_editorWindow->GetHierarchy()->selectedItems();
// Because we draw the Anchors (grayed out) in MOVE mode or when multiple items are selected in ANCHOR mode then it is confusing if you
// click on them, thinking it might do something, and it changes the selection.
// But if the click is inside the element that the anchor belongs to we do want to consider it a select or it would get in the way.
// So a compromise is that, if you click on them, and the click is outside the element they belong to, then the click is ignored.
bool ignoreClickForSelection = false;
if ((m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR) && m_interactionType == InteractionType::NONE)
{
LyShine::EntityArray topLevelSelectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto elementWithAnchors : topLevelSelectedElements)
{
ViewportHelpers::SelectedAnchors grabbedAnchors;
if (!ViewportHelpers::IsControlledByLayout(elementWithAnchors) &&
ViewportElement::PickAnchors(elementWithAnchors, mousePosition, m_anchorWhole->GetTextureSize(), grabbedAnchors))
{
// Hovering over anchors, if the click is outside the element with the anchors then ignore
bool isElementUnderCursor = false;
EBUS_EVENT_ID_RESULT(isElementUnderCursor, elementWithAnchors->GetId(), UiTransformBus, IsPointInRect, mousePosition);
if (!isElementUnderCursor)
{
ignoreClickForSelection = true;
break;
}
}
}
}
if (!ignoreClickForSelection)
{
AZ::Entity* element = nullptr;
EBUS_EVENT_ID_RESULT(element, m_editorWindow->GetCanvas(), UiCanvasBus, PickElement, mousePosition);
HierarchyWidget* hierarchyWidget = m_editorWindow->GetHierarchy();
QTreeWidgetItem* widgetItem = nullptr;
bool itemDeselected = false;
// store the selected items at the start of the selection
m_selectedElementsAtSelectionStart = SelectionHelpers::GetSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
if (element)
{
widgetItem = HierarchyHelpers::ElementToItem(hierarchyWidget, element, false);
// If user is selecting something with the control key pressed, the
// element may need to be de-selected (if it's already selected).
itemDeselected = HierarchyHelpers::HandleDeselect(widgetItem, ctrlKeyPressed);
}
// If the item didn't need to be de-selected, then we should select it
if (!itemDeselected)
{
// Note that widgetItem could still be null at this point, but
// SetSelectedItem will handle this situation for us.
HierarchyHelpers::SetSelectedItem(hierarchyWidget, element);
}
// ClearInteraction gets called if the selection changes to empty but we do not want to clear these since we can start a drag now
m_leftButtonIsActive = true;
m_startMouseDragPos = m_lastMouseDragPos = mousePosition;
m_isAreaSelectionActive = true;
}
}
}
UpdateCursor();
}
void ViewportInteraction::PanOnMouseMoveEvent(const AZ::Vector2& mousePosition)
{
AZ::Vector2 deltaPosition = mousePosition - m_lastMouseDragPos;
AZ::Vector3 mousePosDelta(EntityHelpers::MakeVec3(deltaPosition));
m_canvasViewportMatrixProps.translation += mousePosDelta;
UpdateCanvasToViewportMatrix();
UpdateShouldScaleToFitOnResize();
}
const AZ::Uuid& ViewportInteraction::InitAndGetTransformComponentType()
{
if (m_transformComponentType.IsNull())
{
m_transformComponentType = LyShine::UiTransform2dComponentUuid;
}
return m_transformComponentType;
}
void ViewportInteraction::MouseMoveEvent(QMouseEvent* ev,
const QTreeWidgetItemRawPtrQList& selectedItems)
{
AZ::Vector2 mousePosition = QtHelpers::QPointFToVector2(ev->localPos());
if (m_spaceBarIsActive)
{
if (m_leftButtonIsActive || m_middleButtonIsActive)
{
PanOnMouseMoveEvent(mousePosition);
}
}
else if (m_leftButtonIsActive)
{
if (!m_entityPickedOnMousePress)
{
// Click and drag
ProcessInteraction(mousePosition,
ev->modifiers(),
selectedItems);
}
}
else if (m_middleButtonIsActive)
{
PanOnMouseMoveEvent(mousePosition);
}
else if (ev->buttons() == Qt::NoButton)
{
// Hover
if (m_editorWindow->GetViewport()->IsInObjectPickMode())
{
// Update hover element. We only display the hover element in object pick mode
UpdateHoverElement(mousePosition);
}
else
{
m_interactionType = InteractionType::NONE;
m_grabbedEdges.SetAll(false);
m_grabbedAnchors.SetAll(false);
m_grabbedGizmoParts.SetBoth(false);
UpdateInteractionType(mousePosition,
selectedItems);
UpdateCursor();
}
}
m_lastMouseDragPos = mousePosition;
}
void ViewportInteraction::MouseReleaseEvent(QMouseEvent* ev,
[[maybe_unused]] const QTreeWidgetItemRawPtrQList& selectedItems)
{
if (!m_entityPickedOnMousePress)
{
// if the mouse press and release were in the same position and
// no changes have been made then we can treat it as a mouse-click which can
// do selection. This is useful in the case where we are in move mode but just
// clicked on something that is either:
// - one of multiple things selected and we want to just select this
// - an element in front of something that is selected
// In this case the mouse press will not have been treated as selection in
// MousePressEvent so we need to handle this as a special case
if (!m_reversibleActionStarted && m_lastMouseDragPos == m_startMouseDragPos &&
ev->button() == Qt::LeftButton && !ev->modifiers().testFlag(Qt::ControlModifier) &&
(m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR)
&& (m_interactionType == InteractionType::DIRECT || m_interactionType == InteractionType::TRANSFORM_GIZMO))
{
AZ::Vector2 mousePosition = QtHelpers::QPointFToVector2(ev->localPos());
bool ignoreClick = false;
if (m_interactionType == InteractionType::TRANSFORM_GIZMO)
{
// if we clicked on a gizmo but didn't move then we want to consider this a select click as long as the click
// was inside the active element. (the square part of the gizmo can cover a large area of the element so ignoring
// the click is confusing).
if (m_activeElementId.IsValid())
{
bool isActiveElementUnderCursor = false;
EBUS_EVENT_ID_RESULT(isActiveElementUnderCursor, m_activeElementId, UiTransformBus, IsPointInRect, mousePosition);
if (!isActiveElementUnderCursor)
{
ignoreClick = true;
}
}
}
if (!ignoreClick)
{
AZ::Entity* element = nullptr;
EBUS_EVENT_ID_RESULT(element, m_editorWindow->GetCanvas(), UiCanvasBus, PickElement, mousePosition);
HierarchyWidget* hierarchyWidget = m_editorWindow->GetHierarchy();
if (element)
{
HierarchyHelpers::SetSelectedItem(hierarchyWidget, element);
}
}
}
if (m_dragInteraction)
{
// test to see if the mouse position is inside the viewport on each axis
const QPoint& pos = ev->pos();
const AZ::Vector2 size = m_editorWindow->GetViewport()->GetRenderViewportSize();
ViewportDragInteraction::EndState inWidget;
if (pos.x() >= 0 && pos.x() < size.GetX())
inWidget = pos.y() >= 0 && pos.y() < size.GetY() ? ViewportDragInteraction::EndState::Inside : ViewportDragInteraction::EndState::OutsideY;
else
inWidget = pos.y() >= 0 && pos.y() < size.GetY() ? ViewportDragInteraction::EndState::OutsideX : ViewportDragInteraction::EndState::OutsideXY;
// Some interactions end differently depending on whether the mouse was released inside or outside the viewport
m_dragInteraction->EndInteraction(inWidget);
}
// Tell the Properties panel to update.
// Refresh attributes as well in case this change affects an attribute (ex. anchors affect warning text on scale to device mode)
const AZ::Uuid& transformComponentType = InitAndGetTransformComponentType();
m_editorWindow->GetProperties()->TriggerRefresh(AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues, &transformComponentType);
// Tell the Properties panel that the reversible action is complete
EndReversibleAction();
}
// Reset the interaction
ClearInteraction(false);
if (!m_spaceBarIsActive)
{
// Immediately update the interaction type and cursor (using the possibly new selection)
AZ::Vector2 mousePosition = QtHelpers::QPointFToVector2(ev->localPos());
UpdateInteractionType(mousePosition,
m_editorWindow->GetHierarchy()->selectedItems());
}
UpdateCursor();
}
bool ViewportInteraction::MouseWheelEvent(QWheelEvent* ev)
{
if (m_leftButtonIsActive || m_middleButtonIsActive)
{
// Ignore event.
return false;
}
const QPoint numDegrees(ev->angleDelta());
if (!numDegrees.isNull())
{
// Angle delta returns distance rotated by mouse wheel in eigths of a
// degree.
static const int numStepsPerDegree = 8;
const float numScrollDegress = aznumeric_cast<float>(numDegrees.y() / numStepsPerDegree);
static const float zoomMultiplier = 1 / 100.0f;
Vec2i pivotPoint(
static_cast<int>(ev->position().x()),
static_cast<int>(ev->position().y()));
float newScale = m_canvasViewportMatrixProps.scale + numScrollDegress * zoomMultiplier;
SetCanvasToViewportScale(QuantizeZoomScale(newScale), &pivotPoint);
}
return true;
}
bool ViewportInteraction::KeyPressEvent(QKeyEvent* ev)
{
switch (ev->key())
{
case Qt::Key_Space:
if (!ev->isAutoRepeat())
{
ActivateSpaceBar();
}
return true;
case Qt::Key_Up:
Nudge(ViewportInteraction::NudgeDirection::Up,
(ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow);
return true;
case Qt::Key_Down:
Nudge(ViewportInteraction::NudgeDirection::Down,
(ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow);
return true;
case Qt::Key_Left:
Nudge(ViewportInteraction::NudgeDirection::Left,
(ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow);
return true;
case Qt::Key_Right:
Nudge(ViewportInteraction::NudgeDirection::Right,
(ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow);
return true;
default:
break;
}
return false;
}
bool ViewportInteraction::KeyReleaseEvent(QKeyEvent* ev)
{
if (ev->key() == Qt::Key_Space)
{
if (!ev->isAutoRepeat())
{
ClearInteraction();
UpdateCursor();
if (m_editorWindow->GetViewport()->IsInObjectPickMode())
{
// Update hover element right away in case mouse is over an element
QPoint viewportCursorPos = m_editorWindow->GetViewport()->mapFromGlobal(QCursor::pos());
UpdateHoverElement(AZ::Vector2(aznumeric_cast<float>(viewportCursorPos.x()), aznumeric_cast<float>(viewportCursorPos.y())));
}
}
return true;
}
return false;
}
void ViewportInteraction::SetMode(InteractionMode m)
{
ClearInteraction();
m_interactionMode = m;
PersistentSetInteractionMode(m_interactionMode);
m_editorWindow->GetModeToolbar()->SetCheckedItem(static_cast<int>(m_interactionMode));
m_editorWindow->GetModeToolbar()->GetAlignToolbarSection()->SetIsVisible(
m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR);
UpdateCoordinateSystemToolbarSection();
m_editorWindow->GetViewport()->Refresh();
}
ViewportInteraction::InteractionMode ViewportInteraction::GetMode() const
{
return m_interactionMode;
}
ViewportInteraction::InteractionType ViewportInteraction::GetInteractionType() const
{
return m_interactionType;
}
void ViewportInteraction::SetCoordinateSystem(CoordinateSystem s)
{
m_coordinateSystem = s;
PersistentSetCoordinateSystem(s);
m_editorWindow->GetCoordinateSystemToolbarSection()->SetCurrentIndex(static_cast<int>(m_coordinateSystem));
m_editorWindow->GetViewport()->Refresh();
}
ViewportInteraction::CoordinateSystem ViewportInteraction::GetCoordinateSystem() const
{
return m_coordinateSystem;
}
void ViewportInteraction::InitializeToolbars()
{
m_editorWindow->GetModeToolbar()->SetCheckedItem(static_cast<int>(m_interactionMode));
m_editorWindow->GetModeToolbar()->GetAlignToolbarSection()->SetIsVisible(m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR);
m_editorWindow->GetCoordinateSystemToolbarSection()->SetCurrentIndex(static_cast<int>(m_coordinateSystem));
UpdateCoordinateSystemToolbarSection();
bool canvasLoaded = m_editorWindow->GetCanvas().IsValid();
m_editorWindow->GetMainToolbar()->setEnabled(canvasLoaded);
m_editorWindow->GetModeToolbar()->setEnabled(canvasLoaded);
if (!m_editorWindow->GetModeToolbar()->isEnabled())
{
m_editorWindow->GetCoordinateSystemToolbarSection()->SetIsEnabled(false);
}
m_editorWindow->GetEnterPreviewToolbar()->setEnabled(canvasLoaded);
AZ::Vector2 canvasSize(1280.0f, 720.0f);
EBUS_EVENT_ID_RESULT(canvasSize, m_editorWindow->GetCanvas(), UiCanvasBus, GetCanvasSize);
m_editorWindow->GetCanvasSizeToolbarSection()->SetInitialResolution(canvasSize);
if (!m_editorWindow->GetCanvas().IsValid())
{
SetCanvasToViewportScale(1.0f);
}
{
bool isSnapping = false;
EBUS_EVENT_ID_RESULT(isSnapping, m_editorWindow->GetCanvas(), UiEditorCanvasBus, GetIsSnapEnabled);
m_editorWindow->GetCoordinateSystemToolbarSection()->SetSnapToGridIsChecked(isSnapping);
}
if (QToolButton* expansion = AzQtComponents::ToolBar::getToolBarExpansionButton(m_editorWindow->GetMainToolbar()))
{
expansion->installEventFilter(m_expanderWatcher);
}
if (QToolButton* expansion = AzQtComponents::ToolBar::getToolBarExpansionButton(m_editorWindow->GetModeToolbar()))
{
expansion->installEventFilter(m_expanderWatcher);
}
if (QToolButton* expansion = AzQtComponents::ToolBar::getToolBarExpansionButton(m_editorWindow->GetPreviewToolbar()))
{
expansion->installEventFilter(m_expanderWatcher);
}
if (QToolButton* expansion = AzQtComponents::ToolBar::getToolBarExpansionButton(m_editorWindow->GetEnterPreviewToolbar()))
{
expansion->installEventFilter(m_expanderWatcher);
}
}
const ViewportInteraction::TranslationAndScale& ViewportInteraction::GetCanvasViewportMatrixProps()
{
return m_canvasViewportMatrixProps;
}
void ViewportInteraction::SetCanvasViewportMatrixProps(const TranslationAndScale& canvasViewportMatrixProps)
{
m_canvasViewportMatrixProps = canvasViewportMatrixProps;
UpdateCanvasToViewportMatrix();
UpdateShouldScaleToFitOnResize();
}
void ViewportInteraction::CenterCanvasInViewport(const AZ::Vector2* newCanvasSize)
{
GetScaleToFitTransformProps(newCanvasSize, m_canvasViewportMatrixProps);
// Apply scale and translation changes
UpdateCanvasToViewportMatrix();
m_shouldScaleToFitOnViewportResize = true;
}
void ViewportInteraction::GetScaleToFitTransformProps(const AZ::Vector2* newCanvasSize, TranslationAndScale& propsOut)
{
AZ::Vector2 canvasSize;
// Normally we can just get the canvas size from GetCanvasSize, but if the canvas
// size was recently changed, the caller can choose to provide a new canvas size
// so we don't have to wait for the canvas size to update.
if (newCanvasSize)
{
canvasSize = *newCanvasSize;
}
else
{
EBUS_EVENT_ID_RESULT(canvasSize, m_editorWindow->GetCanvas(), UiCanvasBus, GetCanvasSize);
}
AZ::Vector2 viewportSize = m_editorWindow->GetViewport()->GetRenderViewportSize();
const float viewportWidth = viewportSize.GetX();
const float viewportHeight = viewportSize.GetY();
// We pad the edges of the viewport to allow the user to easily see the borders of
// the canvas edges, which is especially helpful if there are anchors sitting on
// the edges of the canvas.
static const int canvasBorderPaddingInPixels = 32;
AZ::Vector2 viewportPaddedSize(
viewportWidth - canvasBorderPaddingInPixels,
viewportHeight - canvasBorderPaddingInPixels);
// Guard against very small viewports
if (viewportPaddedSize.GetX() <= 0.0f)
{
viewportPaddedSize.SetX(viewportWidth);
}
if (viewportPaddedSize.GetY() <= 0.0f)
{
viewportPaddedSize.SetY(viewportHeight);
}
// Use a "scale to fit" approach
const float canvasToViewportScale = AZ::GetMin<float>(
viewportPaddedSize.GetX() / canvasSize.GetX(),
viewportPaddedSize.GetY() / canvasSize.GetY());
const float scaledCanvasWidth = canvasSize.GetX() * canvasToViewportScale;
const float scaledCanvasHeight = canvasSize.GetY() * canvasToViewportScale;
// Centers the canvas within the viewport
propsOut.translation = AZ::Vector3(
0.5f * (viewportWidth - scaledCanvasWidth),
0.5f * (viewportHeight - scaledCanvasHeight),
0.0f);
propsOut.scale = canvasToViewportScale;
}
void ViewportInteraction::DecreaseCanvasToViewportScale()
{
SetCanvasToViewportScale(QuantizeZoomScale(m_canvasViewportMatrixProps.scale - defaultCanvasToViewportScaleIncrement));
}
void ViewportInteraction::IncreaseCanvasToViewportScale()
{
SetCanvasToViewportScale(QuantizeZoomScale(m_canvasViewportMatrixProps.scale + defaultCanvasToViewportScaleIncrement));
}
void ViewportInteraction::ResetCanvasToViewportScale()
{
SetCanvasToViewportScale(1.0f);
}
void ViewportInteraction::SetCanvasZoomPercent(float percent)
{
SetCanvasToViewportScale(percent / 100.0f);
}
void ViewportInteraction::SetCanvasToViewportScale(float newScale, Vec2i* optionalPivotPoint)
{
static const float minZoom = 0.1f;
static const float maxZoom = 10.0f;
const float currentScale = m_canvasViewportMatrixProps.scale;
m_canvasViewportMatrixProps.scale = AZ::GetClamp(newScale, minZoom, maxZoom);
if (m_editorWindow->GetCanvas().IsValid())
{
// Pivot the zoom based off the center of the viewport's location in canvas space
// Calculate diff between the number of viewport pixels occupied by the current
// scaled canvas view and the new one
AZ::Vector2 canvasSize;
EBUS_EVENT_ID_RESULT(canvasSize, m_editorWindow->GetCanvas(), UiCanvasBus, GetCanvasSize);
const AZ::Vector2 scaledCanvasSize(canvasSize * currentScale);
const AZ::Vector2 newScaledCanvasSize(canvasSize * m_canvasViewportMatrixProps.scale);
const AZ::Vector2 scaledCanvasSizeDiff(newScaledCanvasSize - scaledCanvasSize);
// Use the center of our viewport as the pivot point
Vec2i pivotPoint;
if (optionalPivotPoint)
{
pivotPoint = *optionalPivotPoint;
}
else
{
AZ::Vector2 viewportSize = m_editorWindow->GetViewport()->GetRenderViewportSize();
const float viewportWidth = viewportSize.GetX();
const float viewportHeight = viewportSize.GetY();
pivotPoint = Vec2i(
static_cast<int>(viewportWidth * 0.5f),
static_cast<int>(viewportHeight * 0.5f));
}
// Get the distance between our pivot point and the upper-left corner of the
// canvas (in viewport space)
const Vec2i canvasUpperLeft(
static_cast<int32_t>(m_canvasViewportMatrixProps.translation.GetX()),
static_cast<int32_t>(m_canvasViewportMatrixProps.translation.GetY()));
Vec2i delta = canvasUpperLeft - pivotPoint;
const AZ::Vector2 pivotDiff(aznumeric_cast<float>(delta.x), aznumeric_cast<float>(delta.y));
// Calculate the pivot position relative to the current scaled canvas size. For
// example, if the pivot position is the upper-left corner of the canvas, this
// will be (0, 0), whereas if the pivot position is the bottom-right corner of
// the canvas, this will be (1, 1).
AZ::Vector2 relativePivotPosition(
pivotDiff.GetX() / scaledCanvasSize.GetX(),
pivotDiff.GetY() / scaledCanvasSize.GetY());
// Use the relative pivot position to essentially determine what percentage of
// the difference between the two on-screen canvas sizes should be used to move
// the canvas by to pivot the zoom. For example, if the pivot position is the
// bottom-right corner of the canvas, then we will use 100% of the difference
// in on-screen canvas sizes to move the canvas right and up (to maintain the
// view of the bottom-right corner).
AZ::Vector2 pivotTranslation(
scaledCanvasSizeDiff.GetX() * relativePivotPosition.GetX(),
scaledCanvasSizeDiff.GetY() * relativePivotPosition.GetY());
m_canvasViewportMatrixProps.translation.SetX(m_canvasViewportMatrixProps.translation.GetX() + pivotTranslation.GetX());
m_canvasViewportMatrixProps.translation.SetY(m_canvasViewportMatrixProps.translation.GetY() + pivotTranslation.GetY());
}
UpdateCanvasToViewportMatrix();
UpdateShouldScaleToFitOnResize();
}
float ViewportInteraction::QuantizeZoomScale(float newScale)
{
// Fit to canvas can result in odd zoom scales - when manually zooming we snap it to one of the prefered intervals
// The prefered intervals are in steps of defaultCanvasToViewportScaleIncrement starting at 100% (or a scale of 1.0)
float scaleRelativeto1 = newScale - 1.0f;
float roundedRelative = round(scaleRelativeto1 / defaultCanvasToViewportScaleIncrement) * defaultCanvasToViewportScaleIncrement;
float quantizedScale = roundedRelative + 1.0f;
return quantizedScale;
}
void ViewportInteraction::UpdateZoomFactorLabel()
{
float percentage = m_canvasViewportMatrixProps.scale * 100.0f;
m_editorWindow->GetMainToolbar()->SetZoomPercent(percentage);
}
AZ::Entity* ViewportInteraction::GetActiveElement() const
{
return EntityHelpers::GetEntity(m_activeElementId);
}
const AZ::EntityId& ViewportInteraction::GetActiveElementId() const
{
return m_activeElementId;
}
ViewportHelpers::SelectedAnchors ViewportInteraction::GetGrabbedAnchors() const
{
return m_grabbedAnchors;
}
void ViewportInteraction::UpdateInteractionType(const AZ::Vector2& mousePosition,
const QTreeWidgetItemRawPtrQList& selectedItems)
{
switch (m_interactionMode)
{
case InteractionMode::MOVE:
case InteractionMode::ANCHOR:
{
auto selectedElements = SelectionHelpers::GetSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
if (selectedElements.size() == 1 && m_interactionMode == InteractionMode::ANCHOR)
{
auto selectedElement = selectedElements.front();
if (!ViewportHelpers::IsControlledByLayout(selectedElement) &&
ViewportElement::PickAnchors(selectedElement, mousePosition, m_anchorWhole->GetTextureSize(), m_grabbedAnchors))
{
// Hovering over anchors
m_interactionType = InteractionType::ANCHORS;
m_activeElementId = selectedElement->GetId();
return;
}
}
auto topLevelSelectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto element : topLevelSelectedElements)
{
if (!ViewportHelpers::IsControlledByLayout(element) &&
ViewportElement::PickAxisGizmo(element, m_coordinateSystem, m_interactionMode, mousePosition, m_lineTriangleX->GetTextureSize(), m_grabbedGizmoParts))
{
// Hovering over move gizmo
m_interactionType = InteractionType::TRANSFORM_GIZMO;
m_activeElementId = element->GetId();
return;
}
}
// if hovering over a guide line, then allow moving it or deleting it by moving out of viewport
if (m_editorWindow->GetViewport()->AreGuidesShown() && !GuideHelpers::AreGuidesLocked(m_editorWindow->GetCanvas()))
{
if (GuideHelpers::PickGuide(m_editorWindow->GetCanvas(), mousePosition, m_activeGuideIsVertical, m_activeGuideIndex))
{
m_interactionType = InteractionType::GUIDE;
m_activeElementId.SetInvalid();
return;
}
}
for (auto element : selectedElements)
{
bool isElementUnderCursor = false;
EBUS_EVENT_ID_RESULT(isElementUnderCursor, element->GetId(), UiTransformBus, IsPointInRect, mousePosition);
if (isElementUnderCursor)
{
// Hovering over a selected element
m_interactionType = InteractionType::DIRECT;
m_activeElementId = element->GetId();
return;
}
}
break;
}
case InteractionMode::ROTATE:
{
auto topLevelSelectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto element : topLevelSelectedElements)
{
if (ViewportElement::PickPivot(element, mousePosition, m_pivotIcon->GetTextureSize()))
{
// Hovering over pivot
m_interactionType = InteractionType::PIVOT;
m_activeElementId = element->GetId();
return;
}
}
for (auto element : topLevelSelectedElements)
{
if (ViewportElement::PickCircleGizmo(element, mousePosition, m_circle->GetTextureSize(), m_grabbedGizmoParts))
{
// Hovering over rotate gizmo
m_interactionType = InteractionType::TRANSFORM_GIZMO;
m_activeElementId = element->GetId();
return;
}
}
break;
}
case InteractionMode::RESIZE:
{
auto topLevelSelectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto element : topLevelSelectedElements)
{
if (!ViewportHelpers::IsControlledByLayout(element) &&
ViewportElement::PickAxisGizmo(element, CoordinateSystem::LOCAL, m_interactionMode, mousePosition, m_lineTriangleX->GetTextureSize(), m_grabbedGizmoParts))
{
// Hovering over resize gizmo
m_interactionType = InteractionType::TRANSFORM_GIZMO;
m_activeElementId = element->GetId();
return;
}
}
auto selectedElements = SelectionHelpers::GetSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto element : selectedElements)
{
if (!ViewportHelpers::IsControlledByLayout(element))
{
// Check for grabbing element edges
ViewportElement::PickElementEdges(element, mousePosition, g_elementEdgeForgiveness, m_grabbedEdges);
if (m_grabbedEdges.BothHorizontal() || m_grabbedEdges.BothVertical())
{
// Don't grab both opposite edges
m_grabbedEdges.SetAll(false);
}
if (m_grabbedEdges.Any())
{
m_interactionType = InteractionType::DIRECT;
m_activeElementId = element->GetId();
return;
}
}
}
break;
}
default:
{
// Do nothing
break;
}
} // switch statement
}
void ViewportInteraction::UpdateCursor()
{
QCursor cursor = Qt::ArrowCursor;
if (m_spaceBarIsActive)
{
cursor = (m_leftButtonIsActive || m_middleButtonIsActive ? Qt::ClosedHandCursor : Qt::OpenHandCursor);
}
else if (m_interactionType == InteractionType::GUIDE)
{
if (m_activeGuideIsVertical)
{
cursor = Qt::SplitHCursor; // vertical guide
}
else
{
cursor = Qt::SplitVCursor; // horizontal guide
}
}
else if (m_activeElementId.IsValid())
{
if ((m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR) &&
m_interactionType == InteractionType::DIRECT)
{
cursor = Qt::SizeAllCursor;
}
else if (m_interactionMode == InteractionMode::ROTATE &&
m_interactionType == InteractionType::TRANSFORM_GIZMO)
{
cursor = m_cursorRotate;
}
else if (m_interactionMode == InteractionMode::RESIZE &&
m_interactionType == InteractionType::DIRECT)
{
UiTransformInterface::RectPoints rect;
EBUS_EVENT_ID(m_activeElementId, UiTransformBus, GetViewportSpacePoints, rect);
float topAngle = RAD2DEG(atan2f(rect.TopRight().GetY() - rect.TopLeft().GetY(), rect.TopRight().GetX() - rect.TopLeft().GetX()));
float leftAngle = RAD2DEG(atan2f(rect.TopLeft().GetY() - rect.BottomLeft().GetY(), rect.TopLeft().GetX() - rect.BottomLeft().GetX()));
float topLeftAngle = 0.5f * (topAngle + leftAngle);
float topRightAngle = ViewportHelpers::GetPerpendicularAngle(topLeftAngle);
if (m_grabbedEdges.TopLeft() || m_grabbedEdges.BottomRight())
{
cursor = ViewportHelpers::GetSizingCursor(topLeftAngle);
}
else if (m_grabbedEdges.TopRight() || m_grabbedEdges.BottomLeft())
{
cursor = ViewportHelpers::GetSizingCursor(topRightAngle);
}
else if (m_grabbedEdges.m_left || m_grabbedEdges.m_right)
{
cursor = ViewportHelpers::GetSizingCursor(leftAngle);
}
else if (m_grabbedEdges.m_top || m_grabbedEdges.m_bottom)
{
cursor = ViewportHelpers::GetSizingCursor(topAngle);
}
}
}
else if (m_editorWindow->GetViewport()->IsInObjectPickMode())
{
cursor = m_editorWindow->GetEntityPickerCursor();
}
m_editorWindow->GetViewport()->setCursor(cursor);
}
void ViewportInteraction::UpdateHoverElement(const AZ::Vector2 mousePosition)
{
m_hoverElement.SetInvalid();
AZ::Entity* element = nullptr;
EBUS_EVENT_ID_RESULT(element, m_editorWindow->GetCanvas(), UiCanvasBus, PickElement, mousePosition);
if (element)
{
m_hoverElement = element->GetId();
SetCursorStr(element->GetName());
}
else
{
SetCursorStr("");
}
}
void ViewportInteraction::InvalidateHoverElement()
{
m_hoverElement.SetInvalid();
SetCursorStr("");
}
void ViewportInteraction::SetCursorStr(const AZStd::string& cursorStr)
{
m_cursorStr = cursorStr;
}
void ViewportInteraction::UpdateCanvasToViewportMatrix()
{
AZ::Vector3 scaleVec3(m_canvasViewportMatrixProps.scale, m_canvasViewportMatrixProps.scale, 1.0f);
AZ::Matrix4x4 updatedMatrix = AZ::Matrix4x4::CreateScale(scaleVec3);
updatedMatrix.SetTranslation(m_canvasViewportMatrixProps.translation);
EBUS_EVENT_ID(m_editorWindow->GetCanvas(), UiCanvasBus, SetCanvasToViewportMatrix, updatedMatrix);
UpdateZoomFactorLabel();
// when the zoom or pan changes we need to redraw the rulers
m_editorWindow->GetViewport()->RefreshRulers();
}
void ViewportInteraction::UpdateShouldScaleToFitOnResize()
{
// If the current viewport matrix props match the "scale to fit" props,
// the canvas will scale to fit when the viewport resizes.
TranslationAndScale props;
GetScaleToFitTransformProps(nullptr, props);
m_shouldScaleToFitOnViewportResize = (props == m_canvasViewportMatrixProps);
}
void ViewportInteraction::ProcessInteraction(const AZ::Vector2& mousePosition,
Qt::KeyboardModifiers modifiers,
const QTreeWidgetItemRawPtrQList& selectedItems)
{
// Get the mouse move delta, which is in viewport space.
AZ::Vector2 delta = mousePosition - m_lastMouseDragPos;
AZ::Vector3 mouseTranslation(delta.GetX(), delta.GetY(), 0.0f);
BeginReversibleAction(selectedItems);
bool ctrlIsPressed = modifiers.testFlag(Qt::ControlModifier);
if (m_interactionType == InteractionType::NONE)
{
if (m_isAreaSelectionActive)
{
float mouseDragDistance2 = (mousePosition - m_startMouseDragPos).GetLengthSq();
if (mouseDragDistance2 >= g_minAreaSelectionDistance2)
{
// Area selection
AZ::Vector2 rectMin(min(m_startMouseDragPos.GetX(), mousePosition.GetX()), min(m_startMouseDragPos.GetY(), mousePosition.GetY()));
AZ::Vector2 rectMax(max(m_startMouseDragPos.GetX(), mousePosition.GetX()), max(m_startMouseDragPos.GetY(), mousePosition.GetY()));
LyShine::EntityArray elementsToSelect;
EBUS_EVENT_ID_RESULT(elementsToSelect, m_editorWindow->GetCanvas(), UiCanvasBus, PickElements, rectMin, rectMax);
if (ctrlIsPressed)
{
// NOTE: We are fighting against SetSelectedItems a bit here. SetSelectedItems uses Qt
// to set the selection and the control and shift modifiers affect its behavior.
// When Ctrl is down, unless you pass null or an empty list it adds to the existing
// selected items. To get the behavior we want when ctrl is held down we have to clear
// the selection before setting it. NOTE: if you area select over a group and (during
// same drag) move the cursor so that they are not in the box then they should not
// be added to the selection.
HierarchyHelpers::SetSelectedItem(m_editorWindow->GetHierarchy(), nullptr);
// when control is pressed we add the selected elements in a drag select to the already selected elements
// NOTE: It would be nice to allow ctrl-area-select to deselect already selected items. However, the main
// level editor does not behave that way and we are trying to be consistent (see LMBR-10377)
for (auto element : m_selectedElementsAtSelectionStart)
{
// if not already in the selectedElements then add it
auto iter = AZStd::find(elementsToSelect.begin(), elementsToSelect.end(), element);
if (iter == elementsToSelect.end())
{
elementsToSelect.push_back(element);
}
}
}
HierarchyHelpers::SetSelectedItems(m_editorWindow->GetHierarchy(), &elementsToSelect);
}
else
{
// Selection area too small, ignore
}
}
}
else if (m_interactionType == InteractionType::PIVOT)
{
// Move the pivot that was grabbed
ViewportElement::MovePivot(m_lastMouseDragPos, EntityHelpers::GetEntity(m_activeElementId), mousePosition);
}
else if (m_interactionType == InteractionType::ANCHORS)
{
// Move the anchors of the active element
ViewportElement::MoveAnchors(m_grabbedAnchors, m_startAnchors, m_startMouseDragPos, EntityHelpers::GetEntity(m_activeElementId), mousePosition, ctrlIsPressed);
}
else if (m_interactionType == InteractionType::TRANSFORM_GIZMO)
{
// Transform all selected elements by interacting with one element's transform gizmo
switch (m_interactionMode)
{
case InteractionMode::MOVE:
case InteractionMode::ANCHOR:
if (m_dragInteraction)
{
m_dragInteraction->Update(mousePosition);
}
break;
case InteractionMode::ROTATE:
{
LyShine::EntityArray selectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto element : selectedElements)
{
ViewportElement::Rotate(m_editorWindow->GetHierarchy(), m_editorWindow->GetCanvas(), m_lastMouseDragPos, m_activeElementId, element, mousePosition);
}
}
break;
case InteractionMode::RESIZE:
{
if (!ViewportHelpers::IsControlledByLayout(EntityHelpers::GetEntity(m_activeElementId)))
{
LyShine::EntityArray selectedElements = SelectionHelpers::GetTopLevelSelectedElements(m_editorWindow->GetHierarchy(), selectedItems);
for (auto element : selectedElements)
{
ViewportElement::ResizeByGizmo(m_editorWindow->GetHierarchy(), m_editorWindow->GetCanvas(), m_grabbedGizmoParts, m_activeElementId, element, mouseTranslation);
}
}
}
break;
default:
AZ_Assert(0, "Unexpected combination of m_interactionMode and m_interactionType.");
break;
}
}
else if (m_interactionType == InteractionType::DIRECT)
{
// Transform all selected elements by interacting with one element directly
switch (m_interactionMode)
{
case InteractionMode::MOVE:
case InteractionMode::ANCHOR:
if (m_dragInteraction)
{
m_dragInteraction->Update(mousePosition);
}
break;
case InteractionMode::RESIZE:
// Exception: Direct resizing (grabbing an edge) only affects the element you grabbed
ViewportElement::ResizeDirectly(m_editorWindow->GetHierarchy(), m_editorWindow->GetCanvas(), m_grabbedEdges, EntityHelpers::GetEntity(m_activeElementId), mouseTranslation);
break;
default:
AZ_Assert(0, "Unexpected combination of m_interactionMode and m_interactionType.");
break;
}
}
else if (m_interactionType == InteractionType::GUIDE)
{
if (m_dragInteraction)
{
m_dragInteraction->Update(mousePosition);
}
}
else
{
AZ_Assert(0, "Unexpected value for m_interactionType.");
}
// Tell the Properties panel to update
const AZ::Uuid& transformComponentType = InitAndGetTransformComponentType();
m_editorWindow->GetProperties()->TriggerRefresh(AzToolsFramework::PropertyModificationRefreshLevel::Refresh_Values, &transformComponentType);
}
void ViewportInteraction::DrawAxisGizmo(Draw2dHelper& draw2d, const AZ::Entity* element, CoordinateSystem coordinateSystem, const ViewportIcon* lineTextureX, const ViewportIcon* lineTextureY)
{
if (UiTransformBus::FindFirstHandler(element->GetId()))
{
AZ::Vector2 pivotPosition;
AZ::Matrix4x4 transform;
bool isMoveOrAnchorMode = m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR;
if (coordinateSystem == CoordinateSystem::LOCAL)
{
EBUS_EVENT_ID_RESULT(pivotPosition, element->GetId(), UiTransformBus, GetCanvasSpacePivotNoScaleRotate);
// LOCAL MOVE in the parent element's LOCAL space.
AZ::EntityId elementId(isMoveOrAnchorMode ? EntityHelpers::GetParentElement(element)->GetId() : element->GetId());
EBUS_EVENT_ID(elementId, UiTransformBus, GetTransformToViewport, transform);
}
else
{
// View coordinate system: do everything in viewport space
EBUS_EVENT_ID_RESULT(pivotPosition, element->GetId(), UiTransformBus, GetViewportSpacePivot);
transform = AZ::Matrix4x4::CreateIdentity();
}
// Draw up axis
if (isMoveOrAnchorMode || !ViewportHelpers::IsVerticallyFit(element))
{
AZ::Color color = ((m_activeElementId == element->GetId()) && m_grabbedGizmoParts.m_top) ? ViewportHelpers::highlightColor : ViewportHelpers::yColor;
lineTextureY->Draw(draw2d, pivotPosition, transform, 0.0f, color);
}
// Draw right axis
if (isMoveOrAnchorMode || !ViewportHelpers::IsHorizontallyFit(element))
{
AZ::Color color = ((m_activeElementId == element->GetId()) && m_grabbedGizmoParts.m_right) ? ViewportHelpers::highlightColor : ViewportHelpers::xColor;
lineTextureX->Draw(draw2d, pivotPosition, transform, 0.0f, color);
}
// Draw center square
if (isMoveOrAnchorMode || !ViewportHelpers::IsHorizontallyFit(element) && !ViewportHelpers::IsVerticallyFit(element))
{
AZ::Color color = ((m_activeElementId == element->GetId()) && m_grabbedGizmoParts.Both()) ? ViewportHelpers::highlightColor : ViewportHelpers::zColor;
m_centerSquare->Draw(draw2d, pivotPosition, transform, 0.0f, color);
}
}
}
void ViewportInteraction::DrawCircleGizmo(Draw2dHelper& draw2d, const AZ::Entity* element)
{
if (UiTransformBus::FindFirstHandler(element->GetId()))
{
AZ::Vector2 pivotPosition;
EBUS_EVENT_ID_RESULT(pivotPosition, element->GetId(), UiTransformBus, GetViewportSpacePivot);
// Draw circle
AZ::Color color = ((m_activeElementId == element->GetId()) && m_interactionType == InteractionType::TRANSFORM_GIZMO) ? ViewportHelpers::highlightColor : ViewportHelpers::zColor;
m_circle->Draw(draw2d, pivotPosition, AZ::Matrix4x4::CreateIdentity(), 0.0f, color);
}
}
void ViewportInteraction::UpdateCoordinateSystemToolbarSection()
{
// the coordinate system toolbar should only be enabled in move or anchor mode
bool isMoveOrAnchorMode = m_interactionMode == InteractionMode::MOVE || m_interactionMode == InteractionMode::ANCHOR;
m_editorWindow->GetCoordinateSystemToolbarSection()->SetIsEnabled(isMoveOrAnchorMode);
}
#include <moc_ViewportInteraction.cpp>