Merge pull request #5752 from aws-lumberyard-dev/puvvadar/gitflow_211118_o3de

Merge stabilization/2110
This commit is contained in:
puvvadar
2021-11-19 15:46:16 -08:00
committed by GitHub
387 changed files with 6580 additions and 3688 deletions
+3 -28
View File
@@ -234,7 +234,7 @@ void Q2DViewport::UpdateContent(int flags)
}
//////////////////////////////////////////////////////////////////////////
void Q2DViewport::OnRButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point)
void Q2DViewport::OnRButtonDown([[maybe_unused]] Qt::KeyboardModifiers modifiers, const QPoint& point)
{
if (GetIEditor()->IsInGameMode())
{
@@ -246,9 +246,6 @@ void Q2DViewport::OnRButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& p
setFocus();
}
// Check Edit Tool.
MouseCallback(eMouseRDown, point, modifiers);
SetCurrentCursor(STD_CURSOR_MOVE, QString());
// Save the mouse down position
@@ -273,17 +270,8 @@ void Q2DViewport::OnRButtonUp([[maybe_unused]] Qt::KeyboardModifiers modifiers,
}
//////////////////////////////////////////////////////////////////////////
void Q2DViewport::OnMButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point)
void Q2DViewport::OnMButtonDown([[maybe_unused]] Qt::KeyboardModifiers modifiers, const QPoint& point)
{
////////////////////////////////////////////////////////////////////////
// User pressed the middle mouse button
////////////////////////////////////////////////////////////////////////
// Check Edit Tool.
if (MouseCallback(eMouseMDown, point, modifiers))
{
return;
}
// Save the mouse down position
m_RMouseDownPos = point;
@@ -300,14 +288,8 @@ void Q2DViewport::OnMButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& p
}
//////////////////////////////////////////////////////////////////////////
void Q2DViewport::OnMButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point)
void Q2DViewport::OnMButtonUp([[maybe_unused]] Qt::KeyboardModifiers modifiers, [[maybe_unused]] const QPoint& point)
{
// Check Edit Tool.
if (MouseCallback(eMouseMUp, point, modifiers))
{
return;
}
SetViewMode(NothingMode);
ReleaseMouse();
@@ -547,13 +529,6 @@ QPoint Q2DViewport::WorldToView(const Vec3& wp) const
QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y));
return p;
}
//////////////////////////////////////////////////////////////////////////
QPoint Q2DViewport::WorldToViewParticleEditor(const Vec3& wp, [[maybe_unused]] int width, [[maybe_unused]] int height) const //Eric@conffx implement for the children class of IDisplayViewport
{
Vec3 sp = m_screenTM.TransformPoint(wp);
QPoint p = QPoint(static_cast<int>(sp.x), static_cast<int>(sp.y));
return p;
}
//////////////////////////////////////////////////////////////////////////
Vec3 Q2DViewport::ViewToWorld(const QPoint& vp, [[maybe_unused]] bool* collideWithTerrain, [[maybe_unused]] bool onlyTerrain, [[maybe_unused]] bool bSkipVegetation, [[maybe_unused]] bool bTestRenderMesh, [[maybe_unused]] bool* collideWithObject) const
-2
View File
@@ -50,8 +50,6 @@ public:
//! Map world space position to viewport position.
QPoint WorldToView(const Vec3& wp) const override;
QPoint WorldToViewParticleEditor(const Vec3& wp, int width, int height) const override; //Eric@conffx
//! Map viewport position to world space position.
Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override;
//! Map viewport position to world space ray from camera.
-9
View File
@@ -152,15 +152,6 @@ ActionManager::ActionWrapper& ActionManager::ActionWrapper::SetMenu(DynamicMenu*
return *this;
}
ActionManager::ActionWrapper& ActionManager::ActionWrapper::SetApplyHoverEffect()
{
// Our standard toolbar icons, when hovered on, get a white color effect.
// But for this to work we need .pngs that look good with this effect, so this only works with the standard toolbars
// and looks very ugly for other toolbars, including toolbars loaded from XML (which just show a white rectangle)
m_action->setProperty("IconHasHoverEffect", true);
return *this;
}
ActionManager::ActionWrapper& ActionManager::ActionWrapper::SetReserved()
{
m_action->setProperty("Reserved", true);
-1
View File
@@ -151,7 +151,6 @@ public:
}
ActionWrapper& SetMenu(DynamicMenu* menu);
ActionWrapper& SetApplyHoverEffect();
operator QAction*() const {
return m_action;
@@ -145,6 +145,15 @@ namespace SandboxEditor
}
};
const auto trackingTransform = [viewportId = m_viewportId]
{
bool tracking = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
tracking, viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
return tracking;
};
m_firstPersonRotateCamera = AZStd::make_shared<AzFramework::RotateCameraInput>(SandboxEditor::CameraFreeLookChannelId());
m_firstPersonRotateCamera->m_rotateSpeedFn = []
@@ -152,6 +161,11 @@ namespace SandboxEditor
return SandboxEditor::CameraRotateSpeed();
};
m_firstPersonRotateCamera->m_constrainPitch = [trackingTransform]
{
return !trackingTransform();
};
// default behavior is to hide the cursor but this can be disabled (useful for remote desktop)
// note: See CaptureCursorLook in the Settings Registry
m_firstPersonRotateCamera->SetActivationBeganFn(hideCursor);
@@ -255,6 +269,11 @@ namespace SandboxEditor
return SandboxEditor::CameraOrbitYawRotationInverted();
};
m_orbitRotateCamera->m_constrainPitch = [trackingTransform]
{
return !trackingTransform();
};
m_orbitTranslateCamera = AZStd::make_shared<AzFramework::TranslateCameraInput>(
translateCameraInputChannelIds, AzFramework::LookTranslation, AzFramework::TranslateOffsetOrbit);
@@ -337,12 +356,12 @@ namespace SandboxEditor
AZ::TransformBus::EventResult(worldFromLocal, viewEntityId, &AZ::TransformBus::Events::GetWorldTM);
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, worldFromLocal);
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, worldFromLocal);
}
else
{
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
m_viewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StopTrackingTransform);
}
}
+27 -83
View File
@@ -298,13 +298,9 @@ AzToolsFramework::ViewportInteraction::MousePick EditorViewportWidget::BuildMous
{
AzToolsFramework::ViewportInteraction::MousePick mousePick;
mousePick.m_screenCoordinates = AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(point);
if (const auto& ray = m_renderViewport->ViewportScreenToWorldRay(mousePick.m_screenCoordinates);
ray.has_value())
{
mousePick.m_rayOrigin = ray.value().origin;
mousePick.m_rayDirection = ray.value().direction;
}
const auto[origin, direction] = m_renderViewport->ViewportScreenToWorldRay(mousePick.m_screenCoordinates);
mousePick.m_rayOrigin = origin;
mousePick.m_rayDirection = direction;
return mousePick;
}
@@ -895,23 +891,6 @@ AZ::Vector3 EditorViewportWidget::PickTerrain(const AzFramework::ScreenPoint& po
return LYVec3ToAZVec3(ViewToWorld(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), nullptr, true));
}
AZ::EntityId EditorViewportWidget::PickEntity(const AzFramework::ScreenPoint& point)
{
AZ::EntityId entityId;
HitContext hitInfo;
hitInfo.view = this;
if (HitTest(AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(point), hitInfo))
{
if (hitInfo.object && (hitInfo.object->GetType() == OBJTYPE_AZENTITY))
{
auto entityObject = static_cast<CComponentEntityObject*>(hitInfo.object);
entityId = entityObject->GetAssociatedEntityId();
}
}
return entityId;
}
float EditorViewportWidget::TerrainHeight(const AZ::Vector2& position)
{
return GetIEditor()->GetTerrainElevation(position.GetX(), position.GetY());
@@ -1636,16 +1615,15 @@ void EditorViewportWidget::RenderSelectedRegion()
Vec3 EditorViewportWidget::WorldToView3D(const Vec3& wp, [[maybe_unused]] int nFlags) const
{
Vec3 out(0, 0, 0);
float x, y, z;
float x, y;
ProjectToScreen(wp.x, wp.y, wp.z, &x, &y, &z);
if (_finite(x) && _finite(y) && _finite(z))
ProjectToScreen(wp.x, wp.y, wp.z, &x, &y);
if (_finite(x) && _finite(y))
{
out.x = (x / 100) * m_rcClient.width();
out.y = (y / 100) * m_rcClient.height();
out.x /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen()));
out.y /= static_cast<float>(QHighDpiScaling::factor(windowHandle()->screen()));
out.z = z;
}
return out;
}
@@ -1655,24 +1633,6 @@ QPoint EditorViewportWidget::WorldToView(const Vec3& wp) const
{
return AzToolsFramework::ViewportInteraction::QPointFromScreenPoint(m_renderViewport->ViewportWorldToScreen(LYVec3ToAZVec3(wp)));
}
//////////////////////////////////////////////////////////////////////////
QPoint EditorViewportWidget::WorldToViewParticleEditor(const Vec3& wp, int width, int height) const
{
QPoint p;
float x, y, z;
ProjectToScreen(wp.x, wp.y, wp.z, &x, &y, &z);
if (_finite(x) || _finite(y))
{
p.rx() = static_cast<int>((x / 100) * width);
p.ry() = static_cast<int>((y / 100) * height);
}
else
{
QPoint(0, 0);
}
return p;
}
//////////////////////////////////////////////////////////////////////////
Vec3 EditorViewportWidget::ViewToWorld(
@@ -1688,20 +1648,16 @@ Vec3 EditorViewportWidget::ViewToWorld(
AZ_UNUSED(collideWithObject);
auto ray = m_renderViewport->ViewportScreenToWorldRay(AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(vp));
if (!ray.has_value())
{
return Vec3(0, 0, 0);
}
const float maxDistance = 10000.f;
Vec3 v = AZVec3ToLYVec3(ray.value().direction) * maxDistance;
Vec3 v = AZVec3ToLYVec3(ray.direction) * maxDistance;
if (!_finite(v.x) || !_finite(v.y) || !_finite(v.z))
{
return Vec3(0, 0, 0);
}
Vec3 colp = AZVec3ToLYVec3(ray.value().origin) + 0.002f * v;
Vec3 colp = AZVec3ToLYVec3(ray.origin) + 0.002f * v;
return colp;
}
@@ -1740,21 +1696,19 @@ bool EditorViewportWidget::RayRenderMeshIntersection(IRenderMesh* pRenderMesh, c
return bRes;*/
}
void EditorViewportWidget::UnProjectFromScreen(float sx, float sy, float sz, float* px, float* py, float* pz) const
void EditorViewportWidget::UnProjectFromScreen(float sx, float sy, float* px, float* py, float* pz) const
{
AZ::Vector3 wp;
wp = m_renderViewport->ViewportScreenToWorld(AzFramework::ScreenPoint{(int)sx, m_rcClient.bottom() - ((int)sy)}, sz).value_or(wp);
const AZ::Vector3 wp = m_renderViewport->ViewportScreenToWorld(AzFramework::ScreenPoint{(int)sx, m_rcClient.bottom() - ((int)sy)});
*px = wp.GetX();
*py = wp.GetY();
*pz = wp.GetZ();
}
void EditorViewportWidget::ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy, float* sz) const
void EditorViewportWidget::ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy) const
{
AzFramework::ScreenPoint screenPosition = m_renderViewport->ViewportWorldToScreen(AZ::Vector3{ptx, pty, ptz});
*sx = static_cast<float>(screenPosition.m_x);
*sy = static_cast<float>(screenPosition.m_y);
*sz = 0.f;
}
//////////////////////////////////////////////////////////////////////////
@@ -1764,32 +1718,22 @@ void EditorViewportWidget::ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3&
Vec3 pos0, pos1;
float wx, wy, wz;
UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), 0.0f, &wx, &wy, &wz);
if (!_finite(wx) || !_finite(wy) || !_finite(wz))
{
return;
}
if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
{
return;
}
pos0(wx, wy, wz);
UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), 1.0f, &wx, &wy, &wz);
if (!_finite(wx) || !_finite(wy) || !_finite(wz))
{
return;
}
if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
{
return;
}
pos1(wx, wy, wz);
UnProjectFromScreen(static_cast<float>(vp.x()), static_cast<float>(rc.bottom() - vp.y()), &wx, &wy, &wz);
Vec3 v = (pos1 - pos0);
v = v.GetNormalized();
if (!_finite(wx) || !_finite(wy) || !_finite(wz))
{
return;
}
if (fabs(wx) > 1000000 || fabs(wy) > 1000000 || fabs(wz) > 1000000)
{
return;
}
pos0(wx, wy, wz);
raySrc = pos0;
rayDir = v;
rayDir = (pos0 - AZVec3ToLYVec3(m_renderViewport->GetCameraState().m_position)).GetNormalized();
}
//////////////////////////////////////////////////////////////////////////
@@ -2338,10 +2282,10 @@ void* EditorViewportWidget::GetSystemCursorConstraintWindow() const
return systemCursorConstrained ? renderOverlayHWND() : nullptr;
}
void EditorViewportWidget::BuildDragDropContext(AzQtComponents::ViewportDragContext& context, const QPoint& pt)
void EditorViewportWidget::BuildDragDropContext(
AzQtComponents::ViewportDragContext& context, const AzFramework::ViewportId viewportId, const QPoint& point)
{
const auto scaledPoint = WidgetToViewport(pt);
QtViewport::BuildDragDropContext(context, scaledPoint);
QtViewport::BuildDragDropContext(context, viewportId, point);
}
void EditorViewportWidget::RestoreViewportAfterGameMode()
+4 -5
View File
@@ -165,7 +165,6 @@ private:
Qt::MouseButtons buttons, Qt::KeyboardModifiers modifiers, const QPoint& point) override;
void SetViewportId(int id) override;
QPoint WorldToView(const Vec3& wp) const override;
QPoint WorldToViewParticleEditor(const Vec3& wp, int width, int height) const override;
Vec3 WorldToView3D(const Vec3& wp, int nFlags = 0) const override;
Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const override;
void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const override;
@@ -206,7 +205,6 @@ private:
void* GetSystemCursorConstraintWindow() const override;
// AzToolsFramework::MainEditorViewportInteractionRequestBus overrides ...
AZ::EntityId PickEntity(const AzFramework::ScreenPoint& point) override;
AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) override;
float TerrainHeight(const AZ::Vector2& position) override;
bool ShowingWorldSpace() override;
@@ -273,7 +271,8 @@ private:
bool CheckRespondToInput() const;
void BuildDragDropContext(AzQtComponents::ViewportDragContext& context, const QPoint& pt) override;
void BuildDragDropContext(
AzQtComponents::ViewportDragContext& context, AzFramework::ViewportId viewportId, const QPoint& point) override;
void SetAsActiveViewport();
void PushDisableRendering();
@@ -304,8 +303,8 @@ private:
const DisplayContext& GetDisplayContext() const { return m_displayContext; }
CBaseObject* GetCameraObject() const;
void UnProjectFromScreen(float sx, float sy, float sz, float* px, float* py, float* pz) const;
void ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy, float* sz) const;
void UnProjectFromScreen(float sx, float sy, float* px, float* py, float* pz) const;
void ProjectToScreen(float ptx, float pty, float ptz, float* sx, float* sy) const;
AZ::RPI::ViewPtr GetCurrentAtomView() const;
-1
View File
@@ -45,7 +45,6 @@ struct IDisplayViewport
virtual const Matrix34& GetViewTM() const = 0;
virtual const Matrix34& GetScreenTM() const = 0;
virtual QPoint WorldToView(const Vec3& worldPoint) const = 0;
virtual QPoint WorldToViewParticleEditor(const Vec3& worldPoint, int width, int height) const = 0;
virtual Vec3 WorldToView3D(const Vec3& worldPoint, int flags = 0) const = 0;
virtual Vec3 ViewToWorld(const QPoint& vp, bool* collideWithTerrain = nullptr, bool onlyTerrain = false, bool bSkipVegetation = false, bool bTestRenderMesh = false, bool* collideWithObject = nullptr) const = 0;
virtual void ViewToWorldRay(const QPoint& vp, Vec3& raySrc, Vec3& rayDir) const = 0;
@@ -27,7 +27,9 @@ namespace UnitTest
AZ::Entity* m_entity = nullptr;
AZ::ComponentDescriptor* m_transformComponent = nullptr;
static const AzFramework::ViewportId TestViewportId;
static inline constexpr AzFramework::ViewportId TestViewportId = 2345;
static inline constexpr float HalfInterpolateToTransformDuration =
AtomToolsFramework::ModularViewportCameraControllerRequests::InterpolateToTransformDuration * 0.5f;
void SetUp() override
{
@@ -76,8 +78,6 @@ namespace UnitTest
}
};
const AzFramework::ViewportId EditorCameraFixture::TestViewportId = AzFramework::ViewportId(1337);
TEST_F(EditorCameraFixture, ModularViewportCameraControllerReferenceFrameUpdatedWhenViewportEntityisChanged)
{
// Given
@@ -91,8 +91,8 @@ namespace UnitTest
&Camera::EditorCameraNotificationBus::Events::OnViewportViewEntityChanged, m_entity->GetId());
// ensure the viewport updates after the viewport view entity change
const float deltaTime = 1.0f / 60.0f;
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() });
// note: do a large step to ensure smoothing finishes (e.g. not 1.0f/60.0f)
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(2.0f), AZ::ScriptTimePoint() });
// retrieve updated camera transform
const AZ::Transform cameraTransform = m_cameraViewportContextView->GetCameraTransform();
@@ -102,61 +102,40 @@ namespace UnitTest
EXPECT_THAT(cameraTransform, IsClose(entityTransform));
}
TEST_F(EditorCameraFixture, ReferenceFrameRemainsIdentityAfterExternalCameraTransformChangeWhenNotSet)
TEST_F(EditorCameraFixture, TrackingTransformIsTrueAfterTransformIsTracked)
{
// Given
m_cameraViewportContextView->SetCameraTransform(AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 20.0f, 30.0f)));
// Given/When
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
// When
AZ::Transform referenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
bool trackingTransform = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
referenceFrame, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
trackingTransform, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
// Then
// reference frame is still the identity
EXPECT_THAT(referenceFrame, IsClose(AZ::Transform::CreateIdentity()));
EXPECT_THAT(trackingTransform, ::testing::IsTrue());
}
TEST_F(EditorCameraFixture, ExternalCameraTransformChangeWhenReferenceFrameIsSetUpdatesReferenceFrame)
TEST_F(EditorCameraFixture, TrackingTransformIsFalseAfterTransformIsStoppedBeingTracked)
{
// Given
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame);
const AZ::Transform nextTransform = AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 20.0f, 30.0f));
m_cameraViewportContextView->SetCameraTransform(nextTransform);
// When
AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
currentReferenceFrame, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
// Then
EXPECT_THAT(currentReferenceFrame, IsClose(nextTransform));
}
TEST_F(EditorCameraFixture, ReferenceFrameReturnedToIdentityAfterClear)
{
// Given
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame);
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
// When
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::ClearReferenceFrame);
AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
currentReferenceFrame, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StopTrackingTransform);
// Then
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
bool trackingTransform = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
trackingTransform, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsTrackingTransform);
EXPECT_THAT(trackingTransform, ::testing::IsFalse());
}
TEST_F(EditorCameraFixture, InterpolateToTransform)
@@ -169,8 +148,10 @@ namespace UnitTest
transformToInterpolateTo);
// simulate interpolation
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
const auto finalTransform = m_cameraViewportContextView->GetCameraTransform();
@@ -184,7 +165,7 @@ namespace UnitTest
const AZ::Transform referenceFrame = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationX(AZ::DegToRad(90.0f)), AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::Event(
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::SetReferenceFrame, referenceFrame);
TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::StartTrackingTransform, referenceFrame);
AZ::Transform transformToInterpolateTo = AZ::Transform::CreateFromQuaternionAndTranslation(
AZ::Quaternion::CreateRotationZ(AZ::DegToRad(90.0f)), AZ::Vector3(20.0f, 40.0f, 60.0f));
@@ -195,18 +176,85 @@ namespace UnitTest
transformToInterpolateTo);
// simulate interpolation
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(0.5f), AZ::ScriptTimePoint() });
AZ::Transform currentReferenceFrame = AZ::Transform::CreateTranslation(AZ::Vector3(1.0f, 2.0f, 3.0f));
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
currentReferenceFrame, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::GetReferenceFrame);
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
const auto finalTransform = m_cameraViewportContextView->GetCameraTransform();
// Then
EXPECT_THAT(finalTransform, IsClose(transformToInterpolateTo));
EXPECT_THAT(currentReferenceFrame, IsClose(AZ::Transform::CreateIdentity()));
}
TEST_F(EditorCameraFixture, BeginningCameraInterpolationReturnsTrue)
{
// Given/When
bool interpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
interpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
// Then
EXPECT_THAT(interpolationBegan, ::testing::IsTrue());
}
TEST_F(EditorCameraFixture, CameraInterpolationDoesNotBeginDuringAnExistingInterpolation)
{
// Given/When
bool initialInterpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
initialInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
m_controllerList->UpdateViewport(
{ TestViewportId, AzFramework::FloatSeconds(HalfInterpolateToTransformDuration), AZ::ScriptTimePoint() });
bool nextInterpolationBegan = true;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
nextInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
bool interpolating = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
interpolating, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsInterpolating);
// Then
EXPECT_THAT(initialInterpolationBegan, ::testing::IsTrue());
EXPECT_THAT(nextInterpolationBegan, ::testing::IsFalse());
EXPECT_THAT(interpolating, ::testing::IsTrue());
}
TEST_F(EditorCameraFixture, CameraInterpolationCanBeginAfterAnInterpolationCompletes)
{
// Given/When
bool initialInterpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
initialInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
m_controllerList->UpdateViewport(
{ TestViewportId,
AzFramework::FloatSeconds(AtomToolsFramework::ModularViewportCameraControllerRequests::InterpolateToTransformDuration + 0.5f),
AZ::ScriptTimePoint() });
bool interpolating = true;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
interpolating, TestViewportId, &AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::IsInterpolating);
bool nextInterpolationBegan = false;
AtomToolsFramework::ModularViewportCameraControllerRequestBus::EventResult(
nextInterpolationBegan, TestViewportId,
&AtomToolsFramework::ModularViewportCameraControllerRequestBus::Events::InterpolateToTransform,
AZ::Transform::CreateTranslation(AZ::Vector3(10.0f, 10.0f, 10.0f)));
// Then
EXPECT_THAT(initialInterpolationBegan, ::testing::IsTrue());
EXPECT_THAT(interpolating, ::testing::IsFalse());
EXPECT_THAT(nextInterpolationBegan, ::testing::IsTrue());
}
} // namespace UnitTest
@@ -74,7 +74,7 @@ namespace UnitTest
class ModularViewportCameraControllerFixture : public AllocatorsTestFixture
{
public:
static const AzFramework::ViewportId TestViewportId;
static inline constexpr AzFramework::ViewportId TestViewportId = 1234;
void SetUp() override
{
@@ -146,6 +146,17 @@ namespace UnitTest
controller->SetCameraPropsBuilderCallback(
[](AzFramework::CameraProps& cameraProps)
{
// note: rotateSmoothness is also used for roll (not related to camera input directly)
cameraProps.m_rotateSmoothnessFn = []
{
return 5.0f;
};
cameraProps.m_translateSmoothnessFn = []
{
return 5.0f;
};
cameraProps.m_rotateSmoothingEnabledFn = []
{
return false;
@@ -209,8 +220,6 @@ namespace UnitTest
AZStd::unique_ptr<SandboxEditor::EditorModularViewportCameraComposer> m_editorModularViewportCameraComposer;
};
const AzFramework::ViewportId ModularViewportCameraControllerFixture::TestViewportId = AzFramework::ViewportId(0);
TEST_F(ModularViewportCameraControllerFixture, MouseMovementDoesNotAccumulateExcessiveDriftInModularViewportCameraWithVaryingDeltaTime)
{
SandboxEditor::SetCameraCaptureCursorForLook(false);
@@ -380,6 +389,7 @@ namespace UnitTest
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() });
QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::RightButton, Qt::NoModifier, start + mouseDelta);
m_controllerList->UpdateViewport({ TestViewportId, AzFramework::FloatSeconds(deltaTime), AZ::ScriptTimePoint() });
// update the position of the widget
const auto offset = QPoint(500, 500);
@@ -12,6 +12,7 @@
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <Editor/ViewportManipulatorController.h>
#include <Mocks/MockWindowRequests.h>
namespace UnitTest
{
@@ -77,14 +78,15 @@ namespace UnitTest
class ViewportManipulatorControllerFixture : public AllocatorsTestFixture
{
public:
static const AzFramework::ViewportId TestViewportId;
static inline constexpr AzFramework::ViewportId TestViewportId = 1234;
static inline const QSize WidgetSize = QSize(1920, 1080);
void SetUp() override
{
AllocatorsTestFixture::SetUp();
m_rootWidget = AZStd::make_unique<QWidget>();
m_rootWidget->setFixedSize(QSize(100, 100));
m_rootWidget->setFixedSize(WidgetSize);
QApplication::setActiveWindow(m_rootWidget.get());
m_controllerList = AZStd::make_shared<AzFramework::ViewportControllerList>();
@@ -111,8 +113,6 @@ namespace UnitTest
AZStd::unique_ptr<AzToolsFramework::QtEventToAzInputMapper> m_inputChannelMapper;
};
const AzFramework::ViewportId ViewportManipulatorControllerFixture::TestViewportId = AzFramework::ViewportId(0);
TEST_F(ViewportManipulatorControllerFixture, AnEventIsNotPropagatedToTheViewportWhenAManipulatorHandlesItFirst)
{
// forward input events to our controller list
@@ -227,4 +227,74 @@ namespace UnitTest
// the key was released (cleared)
EXPECT_TRUE(endedEvent);
}
TEST_F(ViewportManipulatorControllerFixture, DoubleClickIsNotRegisteredIfMouseDeltaHasMovedMoreThanDeadzoneInClickInterval)
{
AzFramework::NativeWindowHandle nativeWindowHandle = nullptr;
// forward input events to our controller list
QObject::connect(
m_inputChannelMapper.get(), &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, m_rootWidget.get(),
[this, nativeWindowHandle](const AzFramework::InputChannel* inputChannel, [[maybe_unused]] QEvent* event)
{
m_controllerList->HandleInputChannelEvent(
AzFramework::ViewportControllerInputEvent{ TestViewportId, nativeWindowHandle, *inputChannel });
});
::testing::NiceMock<MockWindowRequests> mockWindowRequests;
mockWindowRequests.Connect(nativeWindowHandle);
using ::testing::Return;
// note: WindowRequests is used internally by ViewportManipulatorController
ON_CALL(mockWindowRequests, GetClientAreaSize())
.WillByDefault(Return(AzFramework::WindowSize(WidgetSize.width(), WidgetSize.height())));
EditorInteractionViewportSelectionFake editorInteractionViewportFake;
editorInteractionViewportFake.m_internalHandleMouseManipulatorInteraction = [](const MouseInteractionEvent&)
{
// report the event was not handled (manipulator was not interacted with)
return false;
};
bool doubleClickDetected = false;
editorInteractionViewportFake.m_internalHandleMouseViewportInteraction =
[&doubleClickDetected](const MouseInteractionEvent& mouseInteractionEvent)
{
// ensure no double click event is detected with the given inputs below
if (mouseInteractionEvent.m_mouseEvent == AzToolsFramework::ViewportInteraction::MouseEvent::DoubleClick)
{
doubleClickDetected = true;
}
return true;
};
editorInteractionViewportFake.Connect();
m_controllerList->Add(AZStd::make_shared<SandboxEditor::ViewportManipulatorController>());
// simulate a click, move, click
MouseMove(m_rootWidget.get(), QPoint(0, 0), QPoint(10, 10));
MousePressAndMove(m_rootWidget.get(), QPoint(10, 10), QPoint(0, 0), Qt::MouseButton::LeftButton);
QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier, QPoint(10, 10));
MouseMove(m_rootWidget.get(), QPoint(10, 10), QPoint(20, 20));
MousePressAndMove(m_rootWidget.get(), QPoint(20, 20), QPoint(0, 0), Qt::MouseButton::LeftButton);
QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier, QPoint(20, 20));
// ensure no double click was detected
EXPECT_FALSE(doubleClickDetected);
// simulate double click (sanity check it still is detected correctly with no movement)
MouseMove(m_rootWidget.get(), QPoint(0, 0), QPoint(10, 10));
MousePressAndMove(m_rootWidget.get(), QPoint(10, 10), QPoint(0, 0), Qt::MouseButton::LeftButton);
QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier, QPoint(10, 10));
MousePressAndMove(m_rootWidget.get(), QPoint(10, 10), QPoint(0, 0), Qt::MouseButton::LeftButton);
QTest::mouseRelease(m_rootWidget.get(), Qt::MouseButton::LeftButton, Qt::KeyboardModifier::NoModifier, QPoint(10, 10));
// ensure a double click was detected
EXPECT_TRUE(doubleClickDetected);
mockWindowRequests.Disconnect();
editorInteractionViewportFake.Disconnect();
}
} // namespace UnitTest
+6 -22
View File
@@ -519,7 +519,7 @@ MainWindow* MainWindow::instance()
void MainWindow::closeEvent(QCloseEvent* event)
{
gSettings.Save();
gSettings.Save(true);
AzFramework::SystemCursorState currentCursorState;
bool isInGameMode = false;
@@ -708,14 +708,10 @@ void MainWindow::InitActions()
.SetShortcut(QKeySequence::Undo)
.SetReserved()
.SetStatusTip(tr("Undo last operation"))
//.SetMenu(new QMenu("FIXME"))
.SetApplyHoverEffect()
.RegisterUpdateCallback(cryEdit, &CCryEditApp::OnUpdateUndo);
am->AddAction(ID_REDO, tr("&Redo"))
.SetShortcut(AzQtComponents::RedoKeySequence)
.SetReserved()
//.SetMenu(new QMenu("FIXME"))
.SetApplyHoverEffect()
.SetStatusTip(tr("Redo last undo operation"))
.RegisterUpdateCallback(cryEdit, &CCryEditApp::OnUpdateRedo);
@@ -731,7 +727,6 @@ void MainWindow::InitActions()
// Modify actions
am->AddAction(AzToolsFramework::EditModeMove, tr("Move"))
.SetIcon(Style::icon("Move"))
.SetApplyHoverEffect()
.SetShortcut(tr("1"))
.SetToolTip(tr("Move (1)"))
.SetCheckable(true)
@@ -757,7 +752,6 @@ void MainWindow::InitActions()
});
am->AddAction(AzToolsFramework::EditModeRotate, tr("Rotate"))
.SetIcon(Style::icon("Translate"))
.SetApplyHoverEffect()
.SetShortcut(tr("2"))
.SetToolTip(tr("Rotate (2)"))
.SetCheckable(true)
@@ -783,7 +777,6 @@ void MainWindow::InitActions()
});
am->AddAction(AzToolsFramework::EditModeScale, tr("Scale"))
.SetIcon(Style::icon("Scale"))
.SetApplyHoverEffect()
.SetShortcut(tr("3"))
.SetToolTip(tr("Scale (3)"))
.SetCheckable(true)
@@ -808,7 +801,6 @@ void MainWindow::InitActions()
am->AddAction(AzToolsFramework::SnapToGrid, tr("Snap to grid"))
.SetIcon(Style::icon("Grid"))
.SetApplyHoverEffect()
.SetShortcut(tr("G"))
.SetToolTip(tr("Snap to grid (G)"))
.SetStatusTip(tr("Toggles snap to grid"))
@@ -821,7 +813,6 @@ void MainWindow::InitActions()
am->AddAction(AzToolsFramework::SnapAngle, tr("Snap angle"))
.SetIcon(Style::icon("Angle"))
.SetApplyHoverEffect()
.SetStatusTip(tr("Snap angle"))
.SetCheckable(true)
.RegisterUpdateCallback([](QAction* action) {
@@ -961,7 +952,6 @@ void MainWindow::InitActions()
.SetShortcut(tr("Ctrl+P"))
.SetToolTip(tr("Simulate (Ctrl+P)"))
.SetStatusTip(tr("Enable processing of Physics and AI."))
.SetApplyHoverEffect()
.SetCheckable(true)
.RegisterUpdateCallback(cryEdit, &CCryEditApp::OnSwitchPhysicsUpdate);
am->AddAction(ID_GAME_SYNCPLAYER, tr("Move Player and Camera Separately")).SetCheckable(true)
@@ -1051,8 +1041,7 @@ void MainWindow::InitActions()
// Editors Toolbar actions
am->AddAction(ID_OPEN_ASSET_BROWSER, tr("Asset browser"))
.SetToolTip(tr("Open Asset Browser"))
.SetApplyHoverEffect();
.SetToolTip(tr("Open Asset Browser"));
AZ::EBusReduceResult<bool, AZStd::logical_or<bool>> emfxEnabled(false);
using AnimationRequestBus = AzToolsFramework::EditorAnimationSystemRequestsBus;
@@ -1062,8 +1051,7 @@ void MainWindow::InitActions()
{
QAction* action = am->AddAction(ID_OPEN_EMOTIONFX_EDITOR, tr("Animation Editor"))
.SetToolTip(tr("Open Animation Editor"))
.SetIcon(QIcon(":/EMotionFX/EMFX_icon_32x32.png"))
.SetApplyHoverEffect();
.SetIcon(QIcon(":/EMotionFX/EMFX_icon_32x32.png"));
QObject::connect(action, &QAction::triggered, this, []() {
QtViewPaneManager::instance()->OpenPane(LyViewPane::AnimationEditor);
});
@@ -1071,12 +1059,10 @@ void MainWindow::InitActions()
am->AddAction(ID_OPEN_AUDIO_CONTROLS_BROWSER, tr("Audio Controls Editor"))
.SetToolTip(tr("Open Audio Controls Editor"))
.SetIcon(Style::icon("Audio"))
.SetApplyHoverEffect();
.SetIcon(Style::icon("Audio"));
am->AddAction(ID_OPEN_UICANVASEDITOR, tr(LyViewPane::UiEditor))
.SetToolTip(tr("Open UI Editor"))
.SetApplyHoverEffect();
.SetToolTip(tr("Open UI Editor"));
// Edit Mode Toolbar Actions
am->AddAction(IDC_SELECTION_MASK, tr("Selected Object Types"));
@@ -1089,12 +1075,10 @@ void MainWindow::InitActions()
// Object Toolbar Actions
am->AddAction(ID_GOTO_SELECTED, tr("Go to selected object"))
.SetIcon(Style::icon("select_object"))
.SetApplyHoverEffect()
.Connect(&QAction::triggered, this, &MainWindow::OnGotoSelected);
// Misc Toolbar Actions
am->AddAction(ID_OPEN_SUBSTANCE_EDITOR, tr("Open Substance Editor"))
.SetApplyHoverEffect();
am->AddAction(ID_OPEN_SUBSTANCE_EDITOR, tr("Open Substance Editor"));
}
void MainWindow::InitToolActionHandlers()
@@ -10,7 +10,6 @@
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Component/Entity.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/Console/IConsole.h>
#include <AzCore/Debug/Profiler.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/RTTI/AttributeReader.h>
@@ -57,6 +56,7 @@
#include <AzToolsFramework/UI/PropertyEditor/EntityPropertyEditor.hxx>
#include <AzToolsFramework/UI/Layer/NameConflictWarning.hxx>
#include <AzToolsFramework/ViewportSelection/EditorHelpers.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <MathConversion.h>
#include <Atom/RPI.Public/ViewportContext.h>
@@ -1394,13 +1394,13 @@ void SandboxIntegrationManager::ContextMenu_NewEntity()
{
AZ::Vector3 worldPosition = AZ::Vector3::CreateZero();
CViewport* view = GetIEditor()->GetViewManager()->GetGameViewport();
// If we don't have a viewport active to aid in placement, the object
// will be created at the origin.
if (view)
if (CViewport* view = GetIEditor()->GetViewManager()->GetGameViewport())
{
const QPoint viewPoint(static_cast<int>(m_contextMenuViewPoint.GetX()), static_cast<int>(m_contextMenuViewPoint.GetY()));
worldPosition = view->GetHitLocation(viewPoint);
worldPosition = AzToolsFramework::FindClosestPickIntersection(
view->GetViewportId(), AzFramework::ScreenPointFromVector2(m_contextMenuViewPoint), AzToolsFramework::EditorPickRayLength,
GetDefaultEntityPlacementDistance());
}
CreateNewEntityAtPosition(worldPosition);
@@ -1675,6 +1675,12 @@ void SandboxIntegrationManager::GoToEntitiesInViewports(const AzToolsFramework::
if (auto viewportContext = viewportContextManager->GetViewportContextById(viewIndex))
{
const AZ::Transform cameraTransform = viewportContext->GetCameraTransform();
// do not attempt to interpolate to where we currently are
if (cameraTransform.GetTranslation().IsClose(center))
{
continue;
}
const AZ::Vector3 forward = (center - cameraTransform.GetTranslation()).GetNormalized();
// move camera 25% further back than required
+6 -4
View File
@@ -471,7 +471,7 @@ void SEditorSettings::LoadValue(const char* sSection, const char* sKey, ESystemC
}
//////////////////////////////////////////////////////////////////////////
void SEditorSettings::Save()
void SEditorSettings::Save(bool isEditorClosing)
{
QString strStringPlaceholder;
@@ -638,14 +638,16 @@ void SEditorSettings::Save()
// --- Settings Registry values
// Prefab System UI
AzFramework::ApplicationRequests::Bus::Broadcast(
&AzFramework::ApplicationRequests::SetPrefabSystemEnabled, prefabSystem);
AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::SetPrefabSystemEnabled, prefabSystem);
AzToolsFramework::Prefab::PrefabLoaderInterface* prefabLoaderInterface =
AZ::Interface<AzToolsFramework::Prefab::PrefabLoaderInterface>::Get();
prefabLoaderInterface->SetSaveAllPrefabsPreference(levelSaveSettings.saveAllPrefabsPreference);
SaveSettingsRegistryFile();
if (!isEditorClosing)
{
SaveSettingsRegistryFile();
}
}
//////////////////////////////////////////////////////////////////////////
+1 -1
View File
@@ -267,7 +267,7 @@ struct SANDBOX_API SEditorSettings
AZ_POP_DISABLE_DLL_EXPORT_BASECLASS_WARNING
SEditorSettings();
~SEditorSettings() = default;
void Save();
void Save(bool isEditorClosing = false);
void Load();
void LoadCloudSettings();
+26 -210
View File
@@ -14,14 +14,19 @@
// Qt
#include <QPainter>
// AzCore
#include <AzCore/Console/IConsole.h>
// AzQtComponents
#include <AzQtComponents/DragAndDrop/ViewportDragAndDrop.h>
#include <AzToolsFramework/API/ComponentEntitySelectionBus.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
// Editor
#include "Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h"
#include "ViewManager.h"
#include "Include/ITransformManipulator.h"
#include "Include/HitContext.h"
@@ -32,22 +37,35 @@
#include "GameEngine.h"
#include "Settings.h"
#ifdef LoadCursor
#undef LoadCursor
#endif
AZ_CVAR(
float,
ed_defaultEntityPlacementDistance,
10.0f,
nullptr,
AZ::ConsoleFunctorFlags::Null,
"The default distance to place an entity from the camera if no intersection is found");
float GetDefaultEntityPlacementDistance()
{
return ed_defaultEntityPlacementDistance;
}
//////////////////////////////////////////////////////////////////////
// Viewport drag and drop support
//////////////////////////////////////////////////////////////////////
void QtViewport::BuildDragDropContext(AzQtComponents::ViewportDragContext& context, const QPoint& pt)
void QtViewport::BuildDragDropContext(
AzQtComponents::ViewportDragContext& context, const AzFramework::ViewportId viewportId, const QPoint& point)
{
context.m_hitLocation = AZ::Vector3::CreateZero();
context.m_hitLocation = GetHitLocation(pt);
context.m_hitLocation = AzToolsFramework::FindClosestPickIntersection(
viewportId, AzToolsFramework::ViewportInteraction::ScreenPointFromQPoint(point), AzToolsFramework::EditorPickRayLength,
GetDefaultEntityPlacementDistance());
}
void QtViewport::dragEnterEvent(QDragEnterEvent* event)
{
if (!GetIEditor()->GetGameEngine()->IsLevelLoaded())
@@ -66,7 +84,7 @@ void QtViewport::dragEnterEvent(QDragEnterEvent* event)
// new bus-based way of doing it (install a listener!)
using namespace AzQtComponents;
ViewportDragContext context;
BuildDragDropContext(context, event->pos());
BuildDragDropContext(context, GetViewportId(), event->pos());
DragAndDropEventsBus::Event(DragAndDropContexts::EditorViewport, &DragAndDropEvents::DragEnter, event, context);
}
}
@@ -89,7 +107,7 @@ void QtViewport::dragMoveEvent(QDragMoveEvent* event)
// new bus-based way of doing it (install a listener!)
using namespace AzQtComponents;
ViewportDragContext context;
BuildDragDropContext(context, event->pos());
BuildDragDropContext(context, GetViewportId(), event->pos());
DragAndDropEventsBus::Event(DragAndDropContexts::EditorViewport, &DragAndDropEvents::DragMove, event, context);
}
}
@@ -112,7 +130,7 @@ void QtViewport::dropEvent(QDropEvent* event)
{
// new bus-based way of doing it (install a listener!)
ViewportDragContext context;
BuildDragDropContext(context, event->pos());
BuildDragDropContext(context, GetViewportId(), event->pos());
DragAndDropEventsBus::Event(DragAndDropContexts::EditorViewport, &DragAndDropEvents::Drop, event, context);
}
}
@@ -340,13 +358,6 @@ void QtViewport::resizeEvent(QResizeEvent* event)
Update();
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::leaveEvent(QEvent* event)
{
QWidget::leaveEvent(event);
MouseCallback(eMouseLeave, QPoint(), Qt::KeyboardModifiers(), Qt::MouseButtons());
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::paintEvent([[maybe_unused]] QPaintEvent* event)
{
@@ -581,63 +592,7 @@ void QtViewport::keyReleaseEvent(QKeyEvent* event)
OnKeyUp(nativeKey, 1, event->nativeModifiers());
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnLButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
// Save the mouse down position
m_cMouseDownPos = point;
if (MouseCallback(eMouseLDown, point, modifiers))
{
return;
}
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnLButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
// Check Edit Tool.
MouseCallback(eMouseLUp, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnRButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
MouseCallback(eMouseRDown, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnRButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
MouseCallback(eMouseRUp, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnMButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
// Check Edit Tool.
MouseCallback(eMouseMDown, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnMButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
// Move the viewer to the mouse location.
// Check Edit Tool.
MouseCallback(eMouseMUp, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnMButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
MouseCallback(eMouseMDblClick, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnMouseMove(Qt::KeyboardModifiers modifiers, Qt::MouseButtons buttons, const QPoint& point)
{
MouseCallback(eMouseMove, point, modifiers, buttons);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnSetCursor()
@@ -696,44 +651,6 @@ void QtViewport::OnDragSelectRectangle(const QRect& rect, bool bNormalizeRect)
GetIEditor()->SetStatusText(szNewStatusText);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnLButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
if (GetIEditor()->IsInGameMode())
{
// Ignore double clicks while in game.
return;
}
MouseCallback(eMouseLDblClick, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnRButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point)
{
MouseCallback(eMouseRDblClick, point, modifiers);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnKeyDown([[maybe_unused]] UINT nChar, [[maybe_unused]] UINT nRepCnt, [[maybe_unused]] UINT nFlags)
{
if (GetIEditor()->IsInGameMode())
{
// Ignore key downs while in game.
return;
}
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::OnKeyUp([[maybe_unused]] UINT nChar, [[maybe_unused]] UINT nRepCnt, [[maybe_unused]] UINT nFlags)
{
if (GetIEditor()->IsInGameMode())
{
// Ignore key downs while in game.
return;
}
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::SetCurrentCursor(const QCursor& hCursor, const QString& cursorString)
{
@@ -1119,29 +1036,6 @@ bool QtViewport::HitTest(const QPoint& point, HitContext& hitInfo)
return false;
}
AZ::Vector3 QtViewport::GetHitLocation(const QPoint& point)
{
Vec3 pos = Vec3(ZERO);
HitContext hit;
if (HitTest(point, hit))
{
pos = hit.raySrc + hit.rayDir * hit.dist;
pos = SnapToGrid(pos);
}
else
{
bool hitTerrain;
pos = ViewToWorld(point, &hitTerrain);
if (hitTerrain)
{
pos.z = GetIEditor()->GetTerrainElevation(pos.x, pos.y);
}
pos = SnapToGrid(pos);
}
return AZ::Vector3(pos.x, pos.y, pos.z);
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::SetZoomFactor(float fZoomFactor)
{
@@ -1315,84 +1209,6 @@ bool QtViewport::GetAdvancedSelectModeFlag()
return m_bAdvancedSelectMode;
}
//////////////////////////////////////////////////////////////////////////
bool QtViewport::MouseCallback(EMouseEvent event, const QPoint& point, Qt::KeyboardModifiers modifiers, Qt::MouseButtons buttons)
{
AZ_PROFILE_FUNCTION(Editor);
// Ignore any mouse events in game mode.
if (GetIEditor()->IsInGameMode())
{
return true;
}
// We must ignore mouse events when we are in the middle of an assert.
// Reason: If we have an assert called from an engine module under the editor, if we call this function,
// it may call the engine again and cause a deadlock.
// Concrete example: CryPhysics called from Trackview causing an assert, and moving the cursor over the viewport
// would cause the editor to freeze as it calls CryPhysics again for a raycast while it didn't release the lock.
if (gEnv->pSystem->IsAssertDialogVisible())
{
return true;
}
//////////////////////////////////////////////////////////////////////////
// Hit test gizmo objects.
//////////////////////////////////////////////////////////////////////////
bool bAltClick = (modifiers & Qt::AltModifier);
bool bCtrlClick = (modifiers & Qt::ControlModifier);
bool bShiftClick = (modifiers & Qt::ShiftModifier);
int flags = (bCtrlClick ? MK_CONTROL : 0) |
(bShiftClick ? MK_SHIFT : 0) |
((buttons& Qt::LeftButton) ? MK_LBUTTON : 0) |
((buttons& Qt::MiddleButton) ? MK_MBUTTON : 0) |
((buttons& Qt::RightButton) ? MK_RBUTTON : 0);
switch (event)
{
case eMouseMove:
if (m_nLastUpdateFrame == m_nLastMouseMoveFrame)
{
// If mouse move event generated in the same frame, ignore it.
return false;
}
m_nLastMouseMoveFrame = m_nLastUpdateFrame;
// Skip the marker position update if anything is selected, since it is only used
// by the info bar which doesn't show the marker when there is an active selection.
// This helps a performance issue when calling ViewToWorld (which calls RayWorldIntersection)
// on every mouse movement becomes very expensive in scenes with large amounts of entities.
CSelectionGroup* selection = GetIEditor()->GetSelection();
if (!(buttons & Qt::RightButton) /* && m_nLastUpdateFrame != m_nLastMouseMoveFrame*/ && (selection && selection->IsEmpty()))
{
//m_nLastMouseMoveFrame = m_nLastUpdateFrame;
Vec3 pos = ViewToWorld(point);
GetIEditor()->SetMarkerPosition(pos);
}
break;
}
QPoint tempPoint(point.x(), point.y());
//////////////////////////////////////////////////////////////////////////
// Handle viewport manipulators.
//////////////////////////////////////////////////////////////////////////
if (!bAltClick)
{
ITransformManipulator* pManipulator = GetIEditor()->GetTransformManipulator();
if (pManipulator)
{
if (pManipulator->MouseCallback(this, event, tempPoint, flags))
{
return true;
}
}
}
return false;
}
//////////////////////////////////////////////////////////////////////////
void QtViewport::ProcessRenderLisneters(DisplayContext& rstDisplayContext)
{
+20 -23
View File
@@ -6,13 +6,12 @@
*
*/
// Description : interface for the CViewport class.
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzFramework/Viewport/ViewportId.h>
#include <AzToolsFramework/Viewport/ViewportTypes.h>
#include <AzToolsFramework/ViewportUi/ViewportUiManager.h>
#include <Cry_Color.h>
@@ -88,6 +87,9 @@ enum EStdCursor
STD_CURSOR_LAST,
};
//! The default distance an entity is placed from the camera if there is no intersection
SANDBOX_API float GetDefaultEntityPlacementDistance();
AZ_PUSH_DISABLE_DLL_EXPORT_BASECLASS_WARNING
class SANDBOX_API CViewport
: public IDisplayViewport
@@ -201,7 +203,6 @@ public:
//! Performs hit testing of 2d point in view to find which object hit.
virtual bool HitTest(const QPoint& point, HitContext& hitInfo) = 0;
virtual AZ::Vector3 GetHitLocation(const QPoint& point) = 0;
virtual void MakeConstructionPlane(int axis) = 0;
@@ -432,7 +433,6 @@ public:
//! Performs hit testing of 2d point in view to find which object hit.
bool HitTest(const QPoint& point, HitContext& hitInfo) override;
AZ::Vector3 GetHitLocation(const QPoint& point) override;
//! Do 2D hit testing of line in world space.
// pToCameraDistance is an optional output parameter in which distance from the camera to the line is returned.
@@ -522,9 +522,6 @@ protected:
void setRenderOverlayVisible(bool);
bool isRenderOverlayVisible() const;
// called to process mouse callback inside the viewport.
virtual bool MouseCallback(EMouseEvent event, const QPoint& point, Qt::KeyboardModifiers modifiers, Qt::MouseButtons buttons = Qt::NoButton);
void ProcessRenderLisneters(DisplayContext& rstDisplayContext);
void mousePressEvent(QMouseEvent* event) override;
@@ -535,29 +532,29 @@ protected:
void keyPressEvent(QKeyEvent* event) override;
void keyReleaseEvent(QKeyEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
void leaveEvent(QEvent* event) override;
void paintEvent(QPaintEvent* event) override;
virtual void OnMouseMove(Qt::KeyboardModifiers modifiers, Qt::MouseButtons buttons, const QPoint& point);
virtual void OnMouseWheel(Qt::KeyboardModifiers modifiers, short zDelta, const QPoint& pt);
virtual void OnLButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnLButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnRButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnRButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnMButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnMButtonDown(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnMButtonUp(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnLButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnRButtonDblClk(Qt::KeyboardModifiers modifiers, const QPoint& point);
virtual void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
virtual void OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags);
virtual void OnMouseMove(Qt::KeyboardModifiers, Qt::MouseButtons, const QPoint&) {}
virtual void OnMouseWheel(Qt::KeyboardModifiers, short zDelta, const QPoint&);
virtual void OnLButtonDown(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnLButtonUp(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnRButtonDown(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnRButtonUp(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnMButtonDblClk(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnMButtonDown(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnMButtonUp(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnLButtonDblClk(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnRButtonDblClk(Qt::KeyboardModifiers, const QPoint&) {}
virtual void OnKeyDown([[maybe_unused]] UINT nChar, [[maybe_unused]] UINT nRepCnt, [[maybe_unused]] UINT nFlags) {}
virtual void OnKeyUp([[maybe_unused]] UINT nChar, [[maybe_unused]] UINT nRepCnt, [[maybe_unused]] UINT nFlags) {}
#if defined(AZ_PLATFORM_WINDOWS)
void OnRawInput(UINT wParam, HRAWINPUT lParam);
#endif
void OnSetCursor();
virtual void BuildDragDropContext(AzQtComponents::ViewportDragContext& context, const QPoint& pt);
virtual void BuildDragDropContext(
AzQtComponents::ViewportDragContext& context, AzFramework::ViewportId viewportId, const QPoint& point);
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
void dragLeaveEvent(QDragLeaveEvent* event) override;
+33 -23
View File
@@ -8,13 +8,15 @@
#include "ViewportManipulatorController.h"
#include <AzCore/Script/ScriptTimePoint.h>
#include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <AzFramework/Viewport/ViewportScreen.h>
#include <AzToolsFramework/Manipulators/ManipulatorManager.h>
#include <AzToolsFramework/ViewportSelection/EditorInteractionSystemViewportSelectionRequestBus.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
#include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <AzCore/Script/ScriptTimePoint.h>
#include <AzToolsFramework/ViewportSelection/EditorSelectionUtil.h>
#include <QApplication>
@@ -87,8 +89,14 @@ namespace SandboxEditor
}
using InteractionBus = AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus;
using namespace AzToolsFramework::ViewportInteraction;
using AzFramework::InputChannel;
using AzToolsFramework::ViewportInteraction::KeyboardModifier;
using AzToolsFramework::ViewportInteraction::MouseButton;
using AzToolsFramework::ViewportInteraction::MouseEvent;
using AzToolsFramework::ViewportInteraction::MouseInteraction;
using AzToolsFramework::ViewportInteraction::MouseInteractionEvent;
using AzToolsFramework::ViewportInteraction::ProjectedViewportRay;
using AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
bool interactionHandled = false;
float wheelDelta = 0.0f;
@@ -117,16 +125,13 @@ namespace SandboxEditor
aznumeric_cast<int>(position->m_normalizedPosition.GetX() * windowSize.m_width),
aznumeric_cast<int>(position->m_normalizedPosition.GetY() * windowSize.m_height));
m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint;
AZStd::optional<ProjectedViewportRay> ray;
ProjectedViewportRay ray{};
ViewportInteractionRequestBus::EventResult(
ray, GetViewportId(), &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint);
if (ray.has_value())
{
m_mouseInteraction.m_mousePick.m_rayOrigin = ray.value().origin;
m_mouseInteraction.m_mousePick.m_rayDirection = ray.value().direction;
}
m_mouseInteraction.m_mousePick.m_rayOrigin = ray.origin;
m_mouseInteraction.m_mousePick.m_rayDirection = ray.direction;
m_mouseInteraction.m_mousePick.m_screenCoordinates = screenPoint;
}
eventType = MouseEvent::Move;
@@ -152,7 +157,7 @@ namespace SandboxEditor
// Only insert the double click timing once we're done processing events, to avoid a false IsDoubleClick positive
if (finishedProcessingEvents)
{
m_pendingDoubleClicks[mouseButton] = m_curTime;
m_pendingDoubleClicks[mouseButton] = { m_currentTime, m_mouseInteraction.m_mousePick.m_screenCoordinates };
}
eventType = MouseEvent::Down;
}
@@ -160,8 +165,8 @@ namespace SandboxEditor
else if (state == InputChannel::State::Ended)
{
// If we've actually logged a mouse down event, forward a mouse up event.
// This prevents corner cases like the context menu thinking it should be opened even though no one clicked in this viewport,
// due to RenderViewportWidget ensuring all controllers get InputChannel::State::Ended events.
// This prevents corner cases like the context menu thinking it should be opened even though no one clicked in this
// viewport, due to RenderViewportWidget ensuring all controllers get InputChannel::State::Ended events.
if (m_mouseInteraction.m_mouseButtons.m_mouseButtons & mouseButtonValue)
{
// Erase the button from our state if we're done processing events.
@@ -246,17 +251,22 @@ namespace SandboxEditor
void ViewportManipulatorControllerInstance::UpdateViewport(const AzFramework::ViewportControllerUpdateEvent& event)
{
m_curTime = event.m_time;
m_currentTime = event.m_time;
}
bool ViewportManipulatorControllerInstance::IsDoubleClick(AzToolsFramework::ViewportInteraction::MouseButton button) const
{
auto clickIt = m_pendingDoubleClicks.find(button);
if (clickIt == m_pendingDoubleClicks.end())
if (auto clickIt = m_pendingDoubleClicks.find(button); clickIt != m_pendingDoubleClicks.end())
{
return false;
const double doubleClickThresholdMilliseconds = qApp->doubleClickInterval();
const bool insideTimeThreshold =
(m_currentTime.GetMilliseconds() - clickIt->second.m_time.GetMilliseconds()) < doubleClickThresholdMilliseconds;
const bool insideDistanceThreshold =
AzFramework::ScreenVectorLength(clickIt->second.m_position - m_mouseInteraction.m_mousePick.m_screenCoordinates) <
AzFramework::DefaultMouseMoveDeadZone;
return insideTimeThreshold && insideDistanceThreshold;
}
const double doubleClickThresholdMilliseconds = qApp->doubleClickInterval();
return (m_curTime.GetMilliseconds() - clickIt->second.GetMilliseconds()) < doubleClickThresholdMilliseconds;
return false;
}
} //namespace SandboxEditor
} // namespace SandboxEditor
+10 -2
View File
@@ -39,8 +39,16 @@ namespace SandboxEditor
static bool IsMouseMove(const AzFramework::InputChannel& inputChannel);
static AzToolsFramework::ViewportInteraction::KeyboardModifier GetKeyboardModifier(const AzFramework::InputChannel& inputChannel);
//! Represents the time and location of a click.
struct ClickEvent
{
AZ::ScriptTimePoint m_time;
AzFramework::ScreenPoint m_position;
};
AzToolsFramework::ViewportInteraction::MouseInteraction m_mouseInteraction;
AZStd::unordered_map<AzToolsFramework::ViewportInteraction::MouseButton, AZ::ScriptTimePoint> m_pendingDoubleClicks;
AZ::ScriptTimePoint m_curTime;
AZStd::unordered_map<AzToolsFramework::ViewportInteraction::MouseButton, ClickEvent> m_pendingDoubleClicks;
AZ::ScriptTimePoint m_currentTime;
};
} // namespace SandboxEditor
@@ -76,9 +76,12 @@ namespace AzFramework
virtual void DrawSolidCone(const AZ::Vector3& pos, const AZ::Vector3& dir, float radius, float height, bool drawShaded = true) { (void)pos; (void)dir; (void)radius; (void)height; (void)drawShaded; }
virtual void DrawWireCylinder(const AZ::Vector3& center, const AZ::Vector3& axis, float radius, float height) { (void)center; (void)axis; (void)radius; (void)height; }
virtual void DrawSolidCylinder(const AZ::Vector3& center, const AZ::Vector3& axis, float radius, float height, bool drawShaded = true) { (void)center; (void)axis; (void)radius; (void)height; (void)drawShaded; }
virtual void DrawWireCylinderNoEnds(const AZ::Vector3& center, const AZ::Vector3& axis, float radius, float height) { (void)center; (void)axis; (void)radius; (void)height; }
virtual void DrawSolidCylinderNoEnds(const AZ::Vector3& center, const AZ::Vector3& axis, float radius, float height, bool drawShaded = true) { (void)center; (void)axis; (void)radius; (void)height; (void)drawShaded; }
virtual void DrawWireCapsule(const AZ::Vector3& center, const AZ::Vector3& axis, float radius, float heightStraightSection) { (void)center; (void)axis; (void)radius; (void)heightStraightSection; }
virtual void DrawWireSphere(const AZ::Vector3& pos, float radius) { (void)pos; (void)radius; }
virtual void DrawWireSphere(const AZ::Vector3& pos, const AZ::Vector3 radius) { (void)pos; (void)radius; }
virtual void DrawWireHemisphere(const AZ::Vector3& pos, const AZ::Vector3& axis, float radius) { (void)pos; (void)axis; (void)radius; }
virtual void DrawWireDisk(const AZ::Vector3& pos, const AZ::Vector3& dir, float radius) { (void)pos; (void)dir; (void)radius; }
virtual void DrawBall(const AZ::Vector3& pos, float radius, bool drawShaded = true) { (void)pos; (void)radius; (void)drawShaded; }
virtual void DrawDisk(const AZ::Vector3& pos, const AZ::Vector3& dir, float radius) { (void)pos; (void)dir; (void)radius; }
@@ -94,27 +94,27 @@ namespace AzFramework
float y;
float z;
// 2.4 Factor as RzRyRx
if (orientation.GetElement(2, 0) < 1.0f)
// 2.5 Factor as RzRxRy
if (orientation.GetElement(2, 1) < 1.0f)
{
if (orientation.GetElement(2, 0) > -1.0f)
if (orientation.GetElement(2, 1) > -1.0f)
{
x = AZStd::atan2(orientation.GetElement(2, 1), orientation.GetElement(2, 2));
y = AZStd::asin(-orientation.GetElement(2, 0));
z = AZStd::atan2(orientation.GetElement(1, 0), orientation.GetElement(0, 0));
x = AZStd::asin(orientation.GetElement(2, 1));
y = AZStd::atan2(-orientation.GetElement(2, 0), orientation.GetElement(2, 2));
z = AZStd::atan2(-orientation.GetElement(0, 1), orientation.GetElement(1, 1));
}
else
{
x = 0.0f;
y = AZ::Constants::Pi * 0.5f;
z = -AZStd::atan2(-orientation.GetElement(2, 1), orientation.GetElement(1, 1));
x = -AZ::Constants::Pi * 0.5f;
y = 0.0f;
z = -AZStd::atan2(orientation.GetElement(0, 2), orientation.GetElement(0, 0));
}
}
else
{
x = 0.0f;
y = -AZ::Constants::Pi * 0.5f;
z = AZStd::atan2(-orientation.GetElement(1, 2), orientation.GetElement(1, 1));
x = AZ::Constants::Pi * 0.5f;
y = 0.0f;
z = AZStd::atan2(orientation.GetElement(0, 2), orientation.GetElement(0, 0));
}
return { x, y, z };
@@ -122,14 +122,36 @@ namespace AzFramework
void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform)
{
const auto eulerAngles = AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(transform));
UpdateCameraFromTranslationAndRotation(
camera, transform.GetTranslation(), AzFramework::EulerAngles(AZ::Matrix3x3::CreateFromTransform(transform)));
}
void UpdateCameraFromTranslationAndRotation(Camera& camera, const AZ::Vector3& translation, const AZ::Vector3& eulerAngles)
{
camera.m_pitch = eulerAngles.GetX();
camera.m_yaw = eulerAngles.GetZ();
camera.m_pivot = transform.GetTranslation();
camera.m_pivot = translation;
camera.m_offset = AZ::Vector3::CreateZero();
}
float SmoothValueTime(const float smoothness, float deltaTime)
{
// note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent
// article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
const float rate = AZStd::exp2(smoothness);
return AZStd::exp2(-rate * deltaTime);
}
float SmoothValue(const float target, const float current, const float time)
{
return AZ::Lerp(target, current, time);
}
float SmoothValue(const float target, const float current, const float smoothness, const float deltaTime)
{
return SmoothValue(target, current, SmoothValueTime(smoothness, deltaTime));
}
bool CameraSystem::HandleEvents(const InputEvent& event)
{
if (const auto& cursor = AZStd::get_if<CursorEvent>(&event))
@@ -291,6 +313,11 @@ namespace AzFramework
{
return false;
};
m_constrainPitch = []() constexpr
{
return true;
};
}
bool RotateCameraInput::HandleEvents(const InputEvent& event, const ScreenVector& cursorDelta, [[maybe_unused]] const float scrollDelta)
@@ -312,7 +339,10 @@ namespace AzFramework
nextCamera.m_yaw -= float(cursorDelta.m_x) * rotateSpeed * Invert(m_invertYawFn());
nextCamera.m_yaw = WrapYawRotation(nextCamera.m_yaw);
nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch);
if (m_constrainPitch())
{
nextCamera.m_pitch = ClampPitchRotation(nextCamera.m_pitch);
}
return nextCamera;
}
@@ -726,14 +756,14 @@ namespace AzFramework
Camera SmoothCamera(const Camera& currentCamera, const Camera& targetCamera, const CameraProps& cameraProps, const float deltaTime)
{
const auto clamp_rotation = [](const float angle)
const auto clampRotation = [](const float angle)
{
return AZStd::fmod(angle + AZ::Constants::TwoPi, AZ::Constants::TwoPi);
};
// keep yaw in 0 - 360 range
float targetYaw = clamp_rotation(targetCamera.m_yaw);
const float currentYaw = clamp_rotation(currentCamera.m_yaw);
float targetYaw = clampRotation(targetCamera.m_yaw);
const float currentYaw = clampRotation(currentCamera.m_yaw);
// return the sign of the float input (-1, 0, 1)
const auto sign = [](const float value)
@@ -742,21 +772,17 @@ namespace AzFramework
};
// ensure smooth transition when moving across 0 - 360 boundary
const float yawDelta = targetYaw - currentYaw;
if (AZStd::abs(yawDelta) >= AZ::Constants::Pi)
if (const float yawDelta = targetYaw - currentYaw; AZStd::abs(yawDelta) >= AZ::Constants::Pi)
{
targetYaw -= AZ::Constants::TwoPi * sign(yawDelta);
}
Camera camera;
// note: the math for the lerp smoothing implementation for camera rotation and translation was inspired by this excellent
// article by Scott Lembcke: https://www.gamasutra.com/blogs/ScottLembcke/20180404/316046/Improved_Lerp_Smoothing.php
if (cameraProps.m_rotateSmoothingEnabledFn())
{
const float lookRate = AZStd::exp2(cameraProps.m_rotateSmoothnessFn());
const float lookTime = AZStd::exp2(-lookRate * deltaTime);
camera.m_pitch = AZ::Lerp(targetCamera.m_pitch, currentCamera.m_pitch, lookTime);
camera.m_yaw = AZ::Lerp(targetYaw, currentYaw, lookTime);
const float lookTime = SmoothValueTime(cameraProps.m_rotateSmoothnessFn(), deltaTime);
camera.m_pitch = SmoothValue(targetCamera.m_pitch, currentCamera.m_pitch, lookTime);
camera.m_yaw = SmoothValue(targetYaw, currentYaw, lookTime);
}
else
{
@@ -766,8 +792,7 @@ namespace AzFramework
if (cameraProps.m_translateSmoothingEnabledFn())
{
const float moveRate = AZStd::exp2(cameraProps.m_translateSmoothnessFn());
const float moveTime = AZStd::exp2(-moveRate * deltaTime);
const float moveTime = SmoothValueTime(cameraProps.m_rotateSmoothnessFn(), deltaTime);
camera.m_pivot = targetCamera.m_pivot.Lerp(currentCamera.m_pivot, moveTime);
camera.m_offset = targetCamera.m_offset.Lerp(currentCamera.m_offset, moveTime);
}
@@ -85,6 +85,19 @@ namespace AzFramework
//! Extracts Euler angles (orientation) and translation from the transform and writes the values to the camera.
void UpdateCameraFromTransform(Camera& camera, const AZ::Transform& transform);
//! Writes the translation value and Euler angles to the camera.
void UpdateCameraFromTranslationAndRotation(Camera& camera, const AZ::Vector3& translation, const AZ::Vector3& eulerAngles);
//! Returns the time ('t') input value to use with SmoothValue.
//! Useful if it is to be reused for multiple calls to SmoothValue.
float SmoothValueTime(float smoothness, float deltaTime);
// Smoothly interpolate a value from current to target according to a smoothing parameter.
float SmoothValue(float target, float current, float smoothness, float deltaTime);
// Overload of SmoothValue that takes time ('t') value directly.
float SmoothValue(float target, float current, float time);
//! Generic motion type.
template<typename MotionTag>
struct MotionEvent
@@ -334,6 +347,7 @@ namespace AzFramework
AZStd::function<float()> m_rotateSpeedFn;
AZStd::function<bool()> m_invertPitchFn;
AZStd::function<bool()> m_invertYawFn;
AZStd::function<bool()> m_constrainPitch;
private:
InputChannelId m_rotateChannelId; //!< Input channel to begin the rotate camera input.
@@ -8,18 +8,18 @@
#include "CameraState.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Math/Matrix3x4.h>
#include <AzCore/Math/Transform.h>
#include <AzCore/Serialization/SerializeContext.h>
namespace AzFramework
{
void SetCameraClippingVolume(
AzFramework::CameraState& cameraState, const float nearPlane, const float farPlane, const float fovRad)
AzFramework::CameraState& cameraState, const float nearPlane, const float farPlane, const float verticalFovRad)
{
cameraState.m_nearClip = nearPlane;
cameraState.m_farClip = farPlane;
cameraState.m_fovOrZoom = fovRad;
cameraState.m_fovOrZoom = verticalFovRad;
}
void SetCameraTransform(CameraState& cameraState, const AZ::Transform& transform)
@@ -35,20 +35,34 @@ namespace AzFramework
SetCameraClippingVolume(cameraState, 0.1f, 1000.0f, AZ::DegToRad(60.0f));
}
AzFramework::CameraState CreateDefaultCamera(
const AZ::Transform& transform, const AZ::Vector2& viewportSize)
CameraState CreateCamera(
const AZ::Transform& transform,
const float nearPlane,
const float farPlane,
const float verticalFovRad,
const AZ::Vector2& viewportSize)
{
AzFramework::CameraState cameraState;
SetDefaultCameraClippingVolume(cameraState);
SetCameraTransform(cameraState, transform);
SetCameraClippingVolume(cameraState, nearPlane, farPlane, verticalFovRad);
cameraState.m_viewportSize = viewportSize;
return cameraState;
}
AzFramework::CameraState CreateIdentityDefaultCamera(
const AZ::Vector3& position, const AZ::Vector2& viewportSize)
AzFramework::CameraState CreateDefaultCamera(const AZ::Transform& transform, const AZ::Vector2& viewportSize)
{
AzFramework::CameraState cameraState;
SetCameraTransform(cameraState, transform);
SetDefaultCameraClippingVolume(cameraState);
cameraState.m_viewportSize = viewportSize;
return cameraState;
}
AzFramework::CameraState CreateIdentityDefaultCamera(const AZ::Vector3& position, const AZ::Vector2& viewportSize)
{
return CreateDefaultCamera(AZ::Transform::CreateTranslation(position), viewportSize);
}
@@ -89,15 +103,15 @@ namespace AzFramework
void CameraState::Reflect(AZ::SerializeContext& serializeContext)
{
serializeContext.Class<CameraState>()->
Field("Position", &CameraState::m_position)->
Field("Forward", &CameraState::m_forward)->
Field("Side", &CameraState::m_side)->
Field("Up", &CameraState::m_up)->
Field("ViewportSize", &CameraState::m_viewportSize)->
Field("NearClip", &CameraState::m_nearClip)->
Field("FarClip", &CameraState::m_farClip)->
Field("FovZoom", &CameraState::m_fovOrZoom)->
Field("Ortho", &CameraState::m_orthographic);
serializeContext.Class<CameraState>()
->Field("Position", &CameraState::m_position)
->Field("Forward", &CameraState::m_forward)
->Field("Side", &CameraState::m_side)
->Field("Up", &CameraState::m_up)
->Field("ViewportSize", &CameraState::m_viewportSize)
->Field("NearClip", &CameraState::m_nearClip)
->Field("FarClip", &CameraState::m_farClip)
->Field("FovZoom", &CameraState::m_fovOrZoom)
->Field("Ortho", &CameraState::m_orthographic);
}
} // namespace AzFramework
@@ -40,10 +40,14 @@ namespace AzFramework
AZ::Vector2 m_viewportSize = AZ::Vector2::CreateZero(); //!< Dimensions of the viewport.
float m_nearClip = 0.01f; //!< Near clip plane of the camera.
float m_farClip = 100.0f; //!< Far clip plane of the camera.
float m_fovOrZoom = 0.0f; //!< Fov or zoom of camera depending on if it is using orthographic projection or not.
float m_fovOrZoom = 0.0f; //!< Vertical fov or zoom of camera depending on if it is using orthographic projection or not.
bool m_orthographic = false; //!< Is the camera using orthographic projection or not.
};
//! Create a camera at the given transform, specifying the near and far clip planes as well as the fov with a specific viewport size.
CameraState CreateCamera(
const AZ::Transform& transform, float nearPlane, float farPlane, float verticalFovRad, const AZ::Vector2& viewportSize);
//! Create a camera at the given transform with a specific viewport size.
//! @note The near/far clip planes and fov are sensible default values - please
//! use SetCameraClippingVolume to override them.
@@ -60,7 +64,7 @@ namespace AzFramework
CameraState CreateCameraFromWorldFromViewMatrix(const AZ::Matrix4x4& worldFromView, const AZ::Vector2& viewportSize);
//! Override the default near/far clipping planes and fov of the camera.
void SetCameraClippingVolume(CameraState& cameraState, float nearPlane, float farPlane, float fovRad);
void SetCameraClippingVolume(CameraState& cameraState, float nearPlane, float farPlane, float verticalFovRad);
//! Override the default near/far clipping planes and fov of the camera by inferring them the specified right handed transform into clip space.
void SetCameraClippingVolumeFromPerspectiveFovMatrixRH(CameraState& cameraState, const AZ::Matrix4x4& clipFromView);
@@ -24,11 +24,12 @@ namespace AzFramework
ClickDetector::ClickOutcome ClickDetector::DetectClick(const ClickEvent clickEvent, const ScreenVector& cursorDelta)
{
m_moveAccumulator += ScreenVectorLength(cursorDelta);
const auto previousDetectionState = m_detectionState;
if (previousDetectionState == DetectionState::WaitingForMove)
{
// only allow the action to begin if the mouse has been moved a small amount
m_moveAccumulator += ScreenVectorLength(cursorDelta);
if (m_moveAccumulator > m_deadZone)
{
m_detectionState = DetectionState::Moved;
@@ -43,7 +44,7 @@ namespace AzFramework
using FloatingPointSeconds = AZStd::chrono::duration<float, AZStd::chrono::seconds::period>;
const auto diff = now - m_tryBeginTime.value();
if (FloatingPointSeconds(diff).count() < m_doubleClickInterval)
if (FloatingPointSeconds(diff).count() < m_doubleClickInterval && m_moveAccumulator < m_deadZone)
{
return ClickOutcome::Nil;
}
@@ -15,6 +15,10 @@
namespace AzFramework
{
//! Default value to use for detecting if the mouse has moved far enough after a mouse down to no longer
//! register a click when a mouse up occurs.
inline constexpr float DefaultMouseMoveDeadZone = 2.0f;
struct ScreenVector;
//! Utility class to help detect different types of mouse click (mouse down and up with
@@ -66,7 +70,7 @@ namespace AzFramework
};
float m_moveAccumulator = 0.0f; //!< How far the mouse has moved after mouse down.
float m_deadZone = 2.0f; //!< How far to move before a click is cancelled (when Move will fire).
float m_deadZone = DefaultMouseMoveDeadZone; //!< How far to move before a click is cancelled (when Move will fire).
float m_doubleClickInterval = 0.4f; //!< Default double click interval, can be overridden.
DetectionState m_detectionState; //!< Internal state of ClickDetector.
//! Mouse down time (happens each mouse down, helps with double click handling).
@@ -24,6 +24,10 @@ namespace AzFramework
serializeContext->Class<ScreenVector>()->
Field("X", &ScreenVector::m_x)->
Field("Y", &ScreenVector::m_y);
serializeContext->Class<ScreenSize>()->
Field("Width", &ScreenSize::m_width)->
Field("Height", &ScreenSize::m_height);
}
}
} // namespace AzFramework
@@ -26,7 +26,7 @@ namespace AzFramework
AZ_TYPE_INFO(ScreenPoint, "{8472B6C2-527F-44FC-87F8-C226B1A57A97}");
ScreenPoint() = default;
ScreenPoint(int x, int y)
constexpr ScreenPoint(int x, int y)
: m_x(x)
, m_y(y)
{
@@ -45,7 +45,7 @@ namespace AzFramework
AZ_TYPE_INFO(ScreenVector, "{1EAA2C62-8FDB-4A28-9FE3-1FA4F1418894}");
ScreenVector() = default;
ScreenVector(int x, int y)
constexpr ScreenVector(int x, int y)
: m_x(x)
, m_y(y)
{
@@ -55,6 +55,22 @@ namespace AzFramework
int m_y; //!< Y screen delta.
};
//! A wrapper around a screen width and height.
struct ScreenSize
{
AZ_TYPE_INFO(ScreenSize, "{26D28916-6E8E-44B8-83F9-C44BCDA370E2}");
ScreenSize() = default;
constexpr ScreenSize(int width, int height)
: m_width(width)
, m_height(height)
{
}
int m_width; //!< Screen size width.
int m_height; //!< Screen size height.
};
void ScreenGeometryReflect(AZ::ReflectContext* context);
inline const ScreenVector operator-(const ScreenPoint& lhs, const ScreenPoint& rhs)
@@ -138,6 +154,16 @@ namespace AzFramework
return !operator==(lhs, rhs);
}
inline const bool operator==(const ScreenSize& lhs, const ScreenSize& rhs)
{
return lhs.m_width == rhs.m_width && lhs.m_height == rhs.m_height;
}
inline const bool operator!=(const ScreenSize& lhs, const ScreenSize& rhs)
{
return !operator==(lhs, rhs);
}
inline ScreenVector& operator*=(ScreenVector& lhs, const float rhs)
{
lhs.m_x = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_x) * rhs));
@@ -152,6 +178,20 @@ namespace AzFramework
return result;
}
inline ScreenSize& operator*=(ScreenSize& lhs, const float rhs)
{
lhs.m_width = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_width) * rhs));
lhs.m_height = aznumeric_cast<int>(AZStd::lround(aznumeric_cast<float>(lhs.m_height) * rhs));
return lhs;
}
inline const ScreenSize operator*(const ScreenSize& lhs, const float rhs)
{
ScreenSize result{ lhs };
result *= rhs;
return result;
}
inline float ScreenVectorLength(const ScreenVector& screenVector)
{
return aznumeric_cast<float>(AZStd::sqrt(screenVector.m_x * screenVector.m_x + screenVector.m_y * screenVector.m_y));
@@ -168,4 +208,28 @@ namespace AzFramework
{
return AZ::Vector2(aznumeric_cast<float>(screenVector.m_x), aznumeric_cast<float>(screenVector.m_y));
}
//! Return an AZ::Vector2 from a ScreenSize.
inline AZ::Vector2 Vector2FromScreenSize(const ScreenSize& screenSize)
{
return AZ::Vector2(aznumeric_cast<float>(screenSize.m_width), aznumeric_cast<float>(screenSize.m_height));
}
//! Return a ScreenPoint from an AZ::Vector2.
inline ScreenPoint ScreenPointFromVector2(const AZ::Vector2& vector2)
{
return ScreenPoint(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
}
//! Return a ScreenVector from an AZ::Vector2.
inline ScreenVector ScreenVectorFromVector2(const AZ::Vector2& vector2)
{
return ScreenVector(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
}
//! Return a ScreenSize from an AZ::Vector2.
inline ScreenSize ScreenSizeFromVector2(const AZ::Vector2& vector2)
{
return ScreenSize(aznumeric_cast<int>(AZStd::lround(vector2.GetX())), aznumeric_cast<int>(AZStd::lround(vector2.GetY())));
}
} // namespace AzFramework
@@ -10,6 +10,7 @@
#include <AzCore/Math/Frustum.h>
#include <AzCore/Math/Matrix4x4.h>
#include <AzCore/Math/MatrixUtils.h>
#include <AzCore/Math/Vector4.h>
#include <AzCore/Math/VectorConversions.h>
#include <AzFramework/Entity/EntityDebugDisplayBus.h>
@@ -112,9 +113,8 @@ namespace AzFramework
const AZ::Matrix4x4& cameraProjection,
const AZ::Vector2& viewportSize)
{
const auto ndcNormalizedPosition = WorldToScreenNdc(worldPosition, cameraView, cameraProjection);
// scale ndc position by screen dimensions to return screen position
return ScreenPointFromNdc(AZ::Vector3ToVector2(ndcNormalizedPosition), viewportSize);
return ScreenPointFromNdc(AZ::Vector3ToVector2(WorldToScreenNdc(worldPosition, cameraView, cameraProjection)), viewportSize);
}
ScreenPoint WorldToScreen(const AZ::Vector3& worldPosition, const CameraState& cameraState)
@@ -144,9 +144,7 @@ namespace AzFramework
const AZ::Matrix4x4& inverseCameraProjection,
const AZ::Vector2& viewportSize)
{
const auto normalizedScreenPosition = NdcFromScreenPoint(screenPosition, viewportSize);
return ScreenNdcToWorld(normalizedScreenPosition, inverseCameraView, inverseCameraProjection);
return ScreenNdcToWorld(NdcFromScreenPoint(screenPosition, viewportSize), inverseCameraView, inverseCameraProjection);
}
AZ::Vector3 ScreenToWorld(const ScreenPoint& screenPosition, const CameraState& cameraState)
@@ -104,9 +104,10 @@ namespace UnitTest
AZStd::shared_ptr<AzFramework::OrbitCameraInput> m_orbitCamera;
AZ::Vector3 m_pivot = AZ::Vector3::CreateZero();
//! This is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
//! on vertical or horizontal motion) as the rotate speed function is set to be 1/1000.
inline static const int PixelMotionDelta = 1570;
// this is approximately Pi/2 * 1000 - this can be used to rotate the camera 90 degrees (pitch or yaw based
// on vertical or horizontal motion) as the rotate speed function is set to be 1/1000.
inline static const int PixelMotionDelta90Degrees = 1570;
inline static const int PixelMotionDelta135Degrees = 2356;
};
TEST_F(CameraInputFixture, BeginAndEndOrbitCameraInputConsumesCorrectEvents)
@@ -292,7 +293,7 @@ namespace UnitTest
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ PixelMotionDelta90Degrees });
const float expectedYaw = AzFramework::WrapYawRotation(-AZ::Constants::HalfPi);
@@ -310,7 +311,7 @@ namespace UnitTest
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta90Degrees });
const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
@@ -331,7 +332,7 @@ namespace UnitTest
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta });
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ PixelMotionDelta90Degrees });
const auto expectedCameraEndingPosition = AZ::Vector3(0.0f, -10.0f, 10.0f);
const float expectedPitch = AzFramework::ClampPitchRotation(-AZ::Constants::HalfPi);
@@ -354,7 +355,7 @@ namespace UnitTest
HandleEventAndUpdate(AzFramework::DiscreteInputEvent{ m_orbitChannelId, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Left, AzFramework::InputChannel::State::Began });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta });
HandleEventAndUpdate(AzFramework::HorizontalMotionEvent{ -PixelMotionDelta90Degrees });
const auto expectedCameraEndingPosition = AZ::Vector3(20.0f, -5.0f, 0.0f);
const float expectedYaw = AzFramework::WrapYawRotation(AZ::Constants::HalfPi);
@@ -366,4 +367,42 @@ namespace UnitTest
EXPECT_THAT(m_camera.m_offset, IsClose(AZ::Vector3(5.0f, -10.0f, 0.0f)));
EXPECT_THAT(m_camera.Translation(), IsCloseTolerance(expectedCameraEndingPosition, 0.01f));
}
TEST_F(CameraInputFixture, CameraPitchCanNotBeMovedPastNinetyDegreesWhenConstrained)
{
const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
// pitch by 135.0 degrees
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ -PixelMotionDelta135Degrees });
// clamped to 90.0 degrees
const float expectedPitch = AZ::DegToRad(90.0f);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
}
TEST_F(CameraInputFixture, CameraPitchCanBeMovedPastNinetyDegreesWhenUnconstrained)
{
m_firstPersonRotateCamera->m_constrainPitch = []
{
return false;
};
const auto cameraStartingPosition = AZ::Vector3(15.0f, -20.0f, 0.0f);
m_targetCamera.m_pivot = cameraStartingPosition;
HandleEventAndUpdate(
AzFramework::DiscreteInputEvent{ AzFramework::InputDeviceMouse::Button::Right, AzFramework::InputChannel::State::Began });
// pitch by 135.0 degrees
HandleEventAndUpdate(AzFramework::VerticalMotionEvent{ -PixelMotionDelta135Degrees });
const float expectedPitch = AZ::DegToRad(135.0f);
using ::testing::FloatNear;
EXPECT_THAT(m_camera.m_pitch, FloatNear(expectedPitch, 0.001f));
}
} // namespace UnitTest
@@ -11,6 +11,7 @@
#include <AzFramework/Viewport/CameraState.h>
#include <AZTestShared/Math/MathTestHelpers.h>
#include <AzCore/Math/SimdMath.h>
#include <AzCore/Math/MatrixUtils.h>
#include <AzCore/Math/Matrix4x4.h>
namespace UnitTest
@@ -51,22 +52,6 @@ namespace UnitTest
{
};
// Taken from Atom::MatrixUtils for testing purposes, this can be removed if MakePerspectiveFovMatrixRH makes it into AZ
static AZ::Matrix4x4 MakePerspectiveMatrixRH(float fovY, float aspectRatio, float nearClip, float farClip)
{
float sinFov, cosFov;
AZ::SinCos(0.5f * fovY, sinFov, cosFov);
float yScale = cosFov / sinFov; //cot(fovY/2)
float xScale = yScale / aspectRatio;
AZ::Matrix4x4 out;
out.SetRow(0, xScale, 0.f, 0.f, 0.f );
out.SetRow(1, 0.f, yScale, 0.f, 0.f );
out.SetRow(2, 0.f, 0.f, farClip / (nearClip - farClip), nearClip*farClip / (nearClip - farClip) );
out.SetRow(3, 0.f, 0.f, -1.f, 0.f );
return out;
}
TEST_P(Translation, Permutation)
{
// Given a position
@@ -176,7 +161,8 @@ namespace UnitTest
{
auto [fovY, aspectRatio, nearClip, farClip] = GetParam();
AZ::Matrix4x4 clipFromView = MakePerspectiveMatrixRH(fovY, aspectRatio, nearClip, farClip);
AZ::Matrix4x4 clipFromView;
MakePerspectiveFovMatrixRH(clipFromView, fovY, aspectRatio, nearClip, farClip);
AzFramework::SetCameraClippingVolumeFromPerspectiveFovMatrixRH(m_cameraState, clipFromView);
@@ -144,12 +144,45 @@ namespace UnitTest
{
using ::testing::Eq;
const ClickDetector::ClickOutcome downOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome upOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(50, 50));
const ClickDetector::ClickOutcome downOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome upOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(50, 50));
EXPECT_THAT(downOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(upOutcome, Eq(ClickDetector::ClickOutcome::Release));
}
//! note: ClickDetector does not explicitly return double clicks but if one occurs the ClickOutcome will be Nil
TEST_F(ClickDetectorFixture, DoubleClickIsRegisteredIfMouseDeltaHasMovedLessThanDeadzoneInClickInterval)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome firstDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome firstUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
EXPECT_THAT(firstDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(firstUpOutcome, Eq(ClickDetector::ClickOutcome::Click));
EXPECT_THAT(secondDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(secondUpOutcome, Eq(ClickDetector::ClickOutcome::Nil));
}
TEST_F(ClickDetectorFixture, DoubleClickIsNotRegisteredIfMouseDeltaHasMovedMoreThanDeadzoneInClickInterval)
{
using ::testing::Eq;
const ClickDetector::ClickOutcome firstDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(0, 0));
const ClickDetector::ClickOutcome firstUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
const ClickDetector::ClickOutcome secondDownOutcome =
m_clickDetector.DetectClick(ClickDetector::ClickEvent::Down, ScreenVector(10, 10));
const ClickDetector::ClickOutcome secondUpOutcome = m_clickDetector.DetectClick(ClickDetector::ClickEvent::Up, ScreenVector(0, 0));
EXPECT_THAT(firstDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(firstUpOutcome, Eq(ClickDetector::ClickOutcome::Click));
EXPECT_THAT(secondDownOutcome, Eq(ClickDetector::ClickOutcome::Nil));
EXPECT_THAT(secondUpOutcome, Eq(ClickDetector::ClickOutcome::Click));
}
} // namespace UnitTest
@@ -8,12 +8,13 @@
#include <AzCore/UnitTest/TestTypes.h>
#include <AzFramework/Viewport/CursorState.h>
#include <Tests/Utils/Printers.h>
namespace UnitTest
{
using AzFramework::CursorState;
using AzFramework::ScreenVector;
using AzFramework::ScreenPoint;
using AzFramework::ScreenVector;
class CursorStateFixture : public ::testing::Test
{
@@ -0,0 +1,32 @@
/*
* 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 "Printers.h"
#include <AzFramework/Viewport/ScreenGeometry.h>
#include <ostream>
#include <string>
namespace AzFramework
{
void PrintTo(const ScreenPoint& screenPoint, std::ostream* os)
{
*os << "(x: " << screenPoint.m_x << ", y: " << screenPoint.m_y << ")";
}
void PrintTo(const ScreenVector& screenVector, std::ostream* os)
{
*os << "(x: " << screenVector.m_x << ", y: " << screenVector.m_y << ")";
}
void PrintTo(const ScreenSize& screenSize, std::ostream* os)
{
*os << "(width: " << screenSize.m_width << ", height: " << screenSize.m_height << ")";
}
} // namespace AzFramework
@@ -0,0 +1,20 @@
/*
* 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 <iosfwd>
namespace AzFramework
{
struct ScreenPoint;
struct ScreenVector;
struct ScreenSize;
void PrintTo(const ScreenPoint& screenPoint, std::ostream* os);
void PrintTo(const ScreenVector& screenVector, std::ostream* os);
void PrintTo(const ScreenSize& screenSize, std::ostream* os);
} // namespace AzFramework
@@ -11,5 +11,7 @@ set(FILES
Mocks/MockWindowRequests.h
Utils/Utils.h
Utils/Utils.cpp
Utils/Printers.h
Utils/Printers.cpp
FrameworkApplicationFixture.h
)
@@ -41,8 +41,8 @@ namespace AzManipulatorTestFramework
// ViewportInteractionRequestBus overrides ...
AzFramework::CameraState GetCameraState() override;
AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) override;
AZStd::optional<AZ::Vector3> ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition, float depth) override;
AZStd::optional<AzToolsFramework::ViewportInteraction::ProjectedViewportRay> ViewportScreenToWorldRay(
AZ::Vector3 ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition) override;
AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportScreenToWorldRay(
const AzFramework::ScreenPoint& screenPosition) override;
float DeviceScalingFactor() override;
@@ -95,7 +95,7 @@ namespace AzManipulatorTestFramework
AzToolsFramework::ViewportInteraction::MousePick mousePick;
mousePick.m_screenCoordinates = screenPoint;
mousePick.m_rayOrigin = cameraState.m_position;
mousePick.m_rayOrigin = nearPlaneWorldPosition;
mousePick.m_rayDirection = (nearPlaneWorldPosition - cameraState.m_position).GetNormalized();
return mousePick;
@@ -69,8 +69,6 @@ namespace AzManipulatorTestFramework
void ImmediateModeActionDispatcher::CameraStateImpl(const AzFramework::CameraState& cameraState)
{
m_viewportManipulatorInteraction.GetViewportInteraction().SetCameraState(cameraState);
GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick.m_rayOrigin = cameraState.m_position;
GetMouseInteractionEvent()->m_mouseInteraction.m_mousePick.m_rayDirection = cameraState.m_forward;
}
void ImmediateModeActionDispatcher::MouseLButtonDownImpl()
@@ -20,7 +20,8 @@ namespace AzManipulatorTestFramework
{
public:
IndirectCallManipulatorManager(ViewportInteractionInterface& viewportInteraction);
// ManipulatorManagerInterface ...
// ManipulatorManagerInterface overrides ...
void ConsumeMouseInteractionEvent(const MouseInteractionEvent& event) override;
AzToolsFramework::ManipulatorManagerId GetId() const override;
bool ManipulatorBeingInteracted() const override;
@@ -140,13 +140,12 @@ namespace AzManipulatorTestFramework
return m_viewportId;
}
AZStd::optional<AZ::Vector3> ViewportInteraction::ViewportScreenToWorld(
[[maybe_unused]] const AzFramework::ScreenPoint& screenPosition, [[maybe_unused]] float depth)
AZ::Vector3 ViewportInteraction::ViewportScreenToWorld([[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
{
return {};
return AZ::Vector3::CreateZero();
}
AZStd::optional<AzToolsFramework::ViewportInteraction::ProjectedViewportRay> ViewportInteraction::ViewportScreenToWorldRay(
AzToolsFramework::ViewportInteraction::ProjectedViewportRay ViewportInteraction::ViewportScreenToWorldRay(
[[maybe_unused]] const AzFramework::ScreenPoint& screenPosition)
{
return {};
@@ -140,8 +140,8 @@ namespace UnitTest
// given a left mouse down ray in world space
// consume the mouse move event
state.m_actionDispatcher->CameraState(m_cameraState)
->MouseLButtonDown()
->MousePosition(AzManipulatorTestFramework::GetCameraStateViewportCenter(m_cameraState))
->MouseLButtonDown()
->ExpectTrue(state.m_linearManipulator->PerformingAction())
->ExpectManipulatorBeingInteracted()
->MouseLButtonUp()
@@ -1198,14 +1198,25 @@ namespace AzToolsFramework
AZ::EntityId ToolsApplication::GetCurrentLevelEntityId()
{
AzFramework::EntityContextId editorEntityContextId = AzFramework::EntityContextId::CreateNull();
AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId);
AZ::SliceComponent* rootSliceComponent = nullptr;
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(rootSliceComponent, editorEntityContextId,
&AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
if (rootSliceComponent && rootSliceComponent->GetMetadataEntity())
if (IsPrefabSystemEnabled())
{
return rootSliceComponent->GetMetadataEntity()->GetId();
if (auto prefabPublicInterface = AZ::Interface<Prefab::PrefabPublicInterface>::Get())
{
return prefabPublicInterface->GetLevelInstanceContainerEntityId();
}
}
else
{
AzFramework::EntityContextId editorEntityContextId = AzFramework::EntityContextId::CreateNull();
AzToolsFramework::EditorEntityContextRequestBus::BroadcastResult(
editorEntityContextId, &AzToolsFramework::EditorEntityContextRequestBus::Events::GetEditorEntityContextId);
AZ::SliceComponent* rootSliceComponent = nullptr;
AzFramework::SliceEntityOwnershipServiceRequestBus::EventResult(
rootSliceComponent, editorEntityContextId, &AzFramework::SliceEntityOwnershipServiceRequestBus::Events::GetRootSlice);
if (rootSliceComponent && rootSliceComponent->GetMetadataEntity())
{
return rootSliceComponent->GetMetadataEntity()->GetId();
}
}
return AZ::EntityId();
@@ -448,7 +448,11 @@ namespace AzToolsFramework
ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder,
componentMode.m_componentMode->GetComponentModeName().c_str());
componentMode.m_componentMode->GetComponentModeName().c_str(),
[]
{
ComponentModeSystemRequestBus::Broadcast(&ComponentModeSystemRequests::EndComponentMode);
});
}
RefreshActions();
@@ -55,8 +55,11 @@ namespace AzToolsFramework
GetEntityComponentIdPair(), elementIdsToDisplay);
// create the component mode border with the specific name for this component mode
ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder,
GetComponentModeName());
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, GetComponentModeName(),
[]
{
ComponentModeSystemRequestBus::Broadcast(&ComponentModeSystemRequests::EndComponentMode);
});
// set the EntityComponentId for this ComponentMode to active in the ComponentModeViewportUi system
ComponentModeViewportUiRequestBus::Event(
GetComponentType(), &ComponentModeViewportUiRequestBus::Events::SetViewportUiActiveEntityComponentId,
@@ -98,7 +98,7 @@ namespace AzToolsFramework::Prefab
}
// Retrieve parent of currently focused prefab.
InstanceOptionalReference parentInstance = m_instanceFocusHierarchy[hierarchySize - 2];
InstanceOptionalReference parentInstance = GetReferenceFromContainerEntityId(m_instanceFocusHierarchy[hierarchySize - 2]);
// Use container entity of parent Instance for focus operations.
AZ::EntityId entityId = parentInstance->get().GetContainerEntityId();
@@ -132,7 +132,7 @@ namespace AzToolsFramework::Prefab
return AZ::Failure(AZStd::string("Prefab Focus Handler: Invalid index on FocusOnPathIndex."));
}
InstanceOptionalReference focusedInstance = m_instanceFocusHierarchy[index];
InstanceOptionalReference focusedInstance = GetReferenceFromContainerEntityId(m_instanceFocusHierarchy[index]);
return FocusOnOwningPrefab(focusedInstance->get().GetContainerEntityId());
}
@@ -172,7 +172,8 @@ namespace AzToolsFramework::Prefab
// Close all container entities in the old path.
CloseInstanceContainers(m_instanceFocusHierarchy);
m_focusedInstance = focusedInstance;
// Do not store the container for the root instance, use an invalid EntityId instead.
m_focusedInstanceContainerEntityId = focusedInstance->get().GetParentInstance().has_value() ? focusedInstance->get().GetContainerEntityId() : AZ::EntityId();
m_focusedTemplateId = focusedInstance->get().GetTemplateId();
// Focus on the descendants of the container entity in the Editor, if the interface is initialized.
@@ -206,56 +207,55 @@ namespace AzToolsFramework::Prefab
InstanceOptionalReference PrefabFocusHandler::GetFocusedPrefabInstance(
[[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
return m_focusedInstance;
return GetReferenceFromContainerEntityId(m_focusedInstanceContainerEntityId);
}
AZ::EntityId PrefabFocusHandler::GetFocusedPrefabContainerEntityId([[maybe_unused]] AzFramework::EntityContextId entityContextId) const
{
if (!m_focusedInstance.has_value())
{
// PrefabFocusHandler has not been initialized yet.
return AZ::EntityId();
}
return m_focusedInstance->get().GetContainerEntityId();
return m_focusedInstanceContainerEntityId;
}
bool PrefabFocusHandler::IsOwningPrefabBeingFocused(AZ::EntityId entityId) const
{
if (!m_focusedInstance.has_value())
{
// PrefabFocusHandler has not been initialized yet.
return false;
}
if (!entityId.IsValid())
{
return false;
}
InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
if (!instance.has_value())
{
return false;
}
return instance.has_value() && (&instance->get() == &m_focusedInstance->get());
// If this is owned by the root instance, that corresponds to an invalid m_focusedInstanceContainerEntityId.
if (!instance->get().GetParentInstance().has_value())
{
return !m_focusedInstanceContainerEntityId.IsValid();
}
return (instance->get().GetContainerEntityId() == m_focusedInstanceContainerEntityId);
}
bool PrefabFocusHandler::IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const
{
if (!m_focusedInstance.has_value())
{
// PrefabFocusHandler has not been initialized yet.
return false;
}
if (!entityId.IsValid())
{
return false;
}
// If the focus is on the root, m_focusedInstanceContainerEntityId will be the invalid id.
// In those case all entities are in the focus hierarchy and should return true.
if (!m_focusedInstanceContainerEntityId.IsValid())
{
return true;
}
InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId);
while (instance.has_value())
{
if (&instance->get() == &m_focusedInstance->get())
if (instance->get().GetContainerEntityId() == m_focusedInstanceContainerEntityId)
{
return true;
}
@@ -290,8 +290,9 @@ namespace AzToolsFramework::Prefab
// Determine if the entityId is the container for any of the instances in the vector.
auto result = AZStd::find_if(
m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(),
[entityId](const InstanceOptionalReference& instance)
[&, entityId](const AZ::EntityId& containerEntityId)
{
InstanceOptionalReference instance = GetReferenceFromContainerEntityId(containerEntityId);
return (instance->get().GetContainerEntityId() == entityId);
}
);
@@ -316,8 +317,9 @@ namespace AzToolsFramework::Prefab
// Determine if the templateId matches any of the instances in the vector.
auto result = AZStd::find_if(
m_instanceFocusHierarchy.begin(), m_instanceFocusHierarchy.end(),
[templateId](const InstanceOptionalReference& instance)
[&, templateId](const AZ::EntityId& containerEntityId)
{
InstanceOptionalReference instance = GetReferenceFromContainerEntityId(containerEntityId);
return (instance->get().GetTemplateId() == templateId);
}
);
@@ -336,10 +338,17 @@ namespace AzToolsFramework::Prefab
AZStd::list<InstanceOptionalReference> instanceFocusList;
InstanceOptionalReference currentInstance = m_focusedInstance;
InstanceOptionalReference currentInstance = GetReferenceFromContainerEntityId(m_focusedInstanceContainerEntityId);
while (currentInstance.has_value())
{
m_instanceFocusHierarchy.emplace_back(currentInstance);
if (currentInstance->get().GetParentInstance().has_value())
{
m_instanceFocusHierarchy.emplace_back(currentInstance->get().GetContainerEntityId());
}
else
{
m_instanceFocusHierarchy.emplace_back(AZ::EntityId());
}
currentInstance = currentInstance->get().GetParentInstance();
}
@@ -357,42 +366,48 @@ namespace AzToolsFramework::Prefab
size_t index = 0;
size_t maxIndex = m_instanceFocusHierarchy.size() - 1;
for (const InstanceOptionalReference& instance : m_instanceFocusHierarchy)
for (const AZ::EntityId containerEntityId : m_instanceFocusHierarchy)
{
AZStd::string prefabName;
if (index < maxIndex)
InstanceOptionalReference instance = GetReferenceFromContainerEntityId(containerEntityId);
if (instance.has_value())
{
// Get the filename without the extension (stem).
prefabName = instance->get().GetTemplateSourcePath().Stem().Native();
}
else
{
// Get the full filename.
prefabName = instance->get().GetTemplateSourcePath().Filename().Native();
}
AZStd::string prefabName;
if (prefabSystemComponentInterface->IsTemplateDirty(instance->get().GetTemplateId()))
{
prefabName += "*";
}
if (index < maxIndex)
{
// Get the filename without the extension (stem).
prefabName = instance->get().GetTemplateSourcePath().Stem().Native();
}
else
{
// Get the full filename.
prefabName = instance->get().GetTemplateSourcePath().Filename().Native();
}
m_instanceFocusPath.Append(prefabName);
if (prefabSystemComponentInterface->IsTemplateDirty(instance->get().GetTemplateId()))
{
prefabName += "*";
}
m_instanceFocusPath.Append(prefabName);
}
++index;
}
}
void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
void PrefabFocusHandler::OpenInstanceContainers(const AZStd::vector<AZ::EntityId>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
{
return;
}
for (const InstanceOptionalReference& instance : instances)
for (const AZ::EntityId containerEntityId : instances)
{
InstanceOptionalReference instance = GetReferenceFromContainerEntityId(containerEntityId);
if (instance.has_value())
{
m_containerEntityInterface->SetContainerOpen(instance->get().GetContainerEntityId(), true);
@@ -400,7 +415,7 @@ namespace AzToolsFramework::Prefab
}
}
void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const
void PrefabFocusHandler::CloseInstanceContainers(const AZStd::vector<AZ::EntityId>& instances) const
{
// If this is called outside the Editor, this interface won't be initialized.
if (!m_containerEntityInterface)
@@ -408,8 +423,10 @@ namespace AzToolsFramework::Prefab
return;
}
for (const InstanceOptionalReference& instance : instances)
for (const AZ::EntityId containerEntityId : instances)
{
InstanceOptionalReference instance = GetReferenceFromContainerEntityId(containerEntityId);
if (instance.has_value())
{
m_containerEntityInterface->SetContainerOpen(instance->get().GetContainerEntityId(), false);
@@ -417,4 +434,22 @@ namespace AzToolsFramework::Prefab
}
}
InstanceOptionalReference PrefabFocusHandler::GetReferenceFromContainerEntityId(AZ::EntityId containerEntityId) const
{
if (!containerEntityId.IsValid())
{
PrefabEditorEntityOwnershipInterface* prefabEditorEntityOwnershipInterface =
AZ::Interface<PrefabEditorEntityOwnershipInterface>::Get();
if (!prefabEditorEntityOwnershipInterface)
{
return AZStd::nullopt;
}
return prefabEditorEntityOwnershipInterface->GetRootPrefabInstance();
}
return m_instanceEntityMapperInterface->FindOwningInstance(containerEntityId);
}
} // namespace AzToolsFramework::Prefab
@@ -73,16 +73,19 @@ namespace AzToolsFramework::Prefab
void RefreshInstanceFocusList();
void RefreshInstanceFocusPath();
void OpenInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
void CloseInstanceContainers(const AZStd::vector<InstanceOptionalReference>& instances) const;
void OpenInstanceContainers(const AZStd::vector<AZ::EntityId>& instances) const;
void CloseInstanceContainers(const AZStd::vector<AZ::EntityId>& instances) const;
//! The instance the editor is currently focusing on.
InstanceOptionalReference m_focusedInstance;
InstanceOptionalReference GetReferenceFromContainerEntityId(AZ::EntityId containerEntityId) const;
//! The EntityId of the prefab container entity for the instance the editor is currently focusing on.
AZ::EntityId m_focusedInstanceContainerEntityId = AZ::EntityId();
//! The templateId of the focused instance.
TemplateId m_focusedTemplateId;
//! The list of instances going from the root (index 0) to the focused instance.
AZStd::vector<InstanceOptionalReference> m_instanceFocusHierarchy;
//! A path containing the names of the containers in the instance focus hierarchy, separated with a /.
//! The list of instances going from the root (index 0) to the focused instance,
//! referenced by their prefab container's EntityId.
AZStd::vector<AZ::EntityId> m_instanceFocusHierarchy;
//! A path containing the filenames of the instances in the focus hierarchy, separated with a /.
AZ::IO::Path m_instanceFocusPath;
ContainerEntityInterface* m_containerEntityInterface = nullptr;
@@ -527,8 +527,8 @@ namespace AzToolsFramework
m_errorButton = nullptr;
}
}
void PropertyAssetCtrl::UpdateErrorButton(const AZStd::string& errorLog)
void PropertyAssetCtrl::UpdateErrorButton()
{
if (m_errorButton)
{
@@ -543,12 +543,17 @@ namespace AzToolsFramework
m_errorButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
m_errorButton->setFixedSize(QSize(16, 16));
m_errorButton->setMouseTracking(true);
m_errorButton->setIcon(QIcon("Icons/PropertyEditor/error_icon.png"));
m_errorButton->setIcon(QIcon(":/PropertyEditor/Resources/error_icon.png"));
m_errorButton->setToolTip("Show Errors");
// Insert the error button after the asset label
qobject_cast<QHBoxLayout*>(layout())->insertWidget(1, m_errorButton);
}
}
void PropertyAssetCtrl::UpdateErrorButtonWithLog(const AZStd::string& errorLog)
{
UpdateErrorButton();
// Connect pressed to opening the error dialog
// Must capture this for call to QObject::connect
@@ -587,6 +592,21 @@ namespace AzToolsFramework
logDialog->show();
});
}
void PropertyAssetCtrl::UpdateErrorButtonWithMessage(const AZStd::string& message)
{
UpdateErrorButton();
connect(m_errorButton, &QPushButton::clicked, this, [this, message]() {
QMessageBox::critical(nullptr, "Error", message.c_str());
// Without this, the error button would maintain focus after clicking, which left the red error icon in a blue-highlighted state
if (parentWidget())
{
parentWidget()->setFocus();
}
});
}
void PropertyAssetCtrl::ClearAssetInternal()
{
@@ -960,7 +980,6 @@ namespace AzToolsFramework
else
{
const AZ::Data::AssetId assetID = GetCurrentAssetID();
m_currentAssetHint = "";
AZ::Outcome<AssetSystem::JobInfoContainer> jobOutcome = AZ::Failure();
AssetSystemJobRequestBus::BroadcastResult(jobOutcome, &AssetSystemJobRequestBus::Events::GetAssetJobsInfoByAssetID, assetID, false, false);
@@ -1018,7 +1037,7 @@ namespace AzToolsFramework
// In case of failure, render failure icon
case AssetSystem::JobStatus::Failed:
{
UpdateErrorButton(errorLog);
UpdateErrorButtonWithLog(errorLog);
}
break;
@@ -1043,6 +1062,10 @@ namespace AzToolsFramework
m_currentAssetHint = assetPath;
}
}
else
{
UpdateErrorButtonWithMessage(AZStd::string::format("Asset is missing.\n\nID: %s\nHint:%s", assetID.ToString<AZStd::string>().c_str(), GetCurrentAssetHint().c_str()));
}
}
// Get the asset file name
@@ -168,7 +168,9 @@ namespace AzToolsFramework
bool IsCorrectMimeData(const QMimeData* pData, AZ::Data::AssetId* pAssetId = nullptr, AZ::Data::AssetType* pAssetType = nullptr) const;
void ClearErrorButton();
void UpdateErrorButton(const AZStd::string& errorLog);
void UpdateErrorButton();
void UpdateErrorButtonWithLog(const AZStd::string& errorLog);
void UpdateErrorButtonWithMessage(const AZStd::string& message);
virtual const AZStd::string GetFolderSelection() const { return AZStd::string(); }
virtual void SetFolderSelection(const AZStd::string& /* folderPath */) {}
virtual void ClearAssetInternal();
@@ -158,7 +158,10 @@ namespace UnitTest
{
// Create & Start a new ToolsApplication if there's no existing one
m_app = CreateTestApplication();
m_app->Start(AzFramework::Application::Descriptor());
AZ::ComponentApplication::StartupParameters startupParameters;
startupParameters.m_loadAssetCatalog = false;
m_app->Start(AzFramework::Application::Descriptor(), startupParameters);
}
// without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
@@ -6,6 +6,7 @@
*
*/
#include <AzFramework/Render/IntersectorInterface.h>
#include <AzToolsFramework/Viewport/ViewportMessages.h>
namespace AzToolsFramework
@@ -62,4 +63,33 @@ namespace AzToolsFramework
return circleBoundWidth;
}
AZ::Vector3 FindClosestPickIntersection(
AzFramework::ViewportId viewportId, const AzFramework::ScreenPoint& screenPoint, const float rayLength, const float defaultDistance)
{
using AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus;
AzToolsFramework::ViewportInteraction::ProjectedViewportRay viewportRay{};
ViewportInteractionRequestBus::EventResult(
viewportRay, viewportId, &ViewportInteractionRequestBus::Events::ViewportScreenToWorldRay, screenPoint);
AzFramework::RenderGeometry::RayRequest ray;
ray.m_startWorldPosition = viewportRay.origin;
ray.m_endWorldPosition = viewportRay.origin + viewportRay.direction * rayLength;
ray.m_onlyVisible = true;
AzFramework::RenderGeometry::RayResult renderGeometryIntersectionResult;
AzFramework::RenderGeometry::IntersectorBus::EventResult(
renderGeometryIntersectionResult, AzToolsFramework::GetEntityContextId(),
&AzFramework::RenderGeometry::IntersectorBus::Events::RayIntersect, ray);
// attempt a ray intersection with any visible mesh and return the intersection position if successful
if (renderGeometryIntersectionResult)
{
return renderGeometryIntersectionResult.m_worldPosition;
}
else
{
return viewportRay.origin + viewportRay.direction * defaultDistance;
}
}
} // namespace AzToolsFramework
@@ -162,12 +162,11 @@ namespace AzToolsFramework
//! Multiply by DeviceScalingFactor to get the position in viewport pixel space.
virtual AzFramework::ScreenPoint ViewportWorldToScreen(const AZ::Vector3& worldPosition) = 0;
//! Transforms a point from Qt widget screen space to world space based on the given clip space depth.
//! Depth specifies a relative camera depth to project in the range of [0.f, 1.f].
//! Returns the world space position if successful.
virtual AZStd::optional<AZ::Vector3> ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition, float depth) = 0;
virtual AZ::Vector3 ViewportScreenToWorld(const AzFramework::ScreenPoint& screenPosition) = 0;
//! Casts a point in screen space to a ray in world space originating from the viewport camera frustum's near plane.
//! Returns a ray containing the ray's origin and a direction normal, if successful.
virtual AZStd::optional<ProjectedViewportRay> ViewportScreenToWorldRay(const AzFramework::ScreenPoint& screenPosition) = 0;
virtual ProjectedViewportRay ViewportScreenToWorldRay(const AzFramework::ScreenPoint& screenPosition) = 0;
//! Gets the DPI scaling factor that translates Qt widget space into viewport pixel space.
virtual float DeviceScalingFactor() = 0;
@@ -229,9 +228,6 @@ namespace AzToolsFramework
class MainEditorViewportInteractionRequests
{
public:
//! Given a point in screen space, return the picked entity (if any).
//! Picked EntityId will be returned, InvalidEntityId will be returned on failure.
virtual AZ::EntityId PickEntity(const AzFramework::ScreenPoint& point) = 0;
//! Given a point in screen space, return the terrain position in world space.
virtual AZ::Vector3 PickTerrain(const AzFramework::ScreenPoint& point) = 0;
//! Return the terrain height given a world position in 2d (xy plane).
@@ -266,7 +262,6 @@ namespace AzToolsFramework
{
public:
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
//! Returns the current state of the keyboard modifier keys.
virtual KeyboardModifiers QueryKeyboardModifiers() = 0;
@@ -290,7 +285,6 @@ namespace AzToolsFramework
{
public:
static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single;
static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
//! Returns the current time in seconds.
//! This interface can be overridden for the purposes of testing to simplify viewport input requests.
@@ -340,6 +334,12 @@ namespace AzToolsFramework
return entityContextId;
}
//! Performs an intersection test against meshes in the scene, if there is a hit (the ray intersects
//! a mesh), that position is returned, otherwise a point projected defaultDistance from the
//! origin of the ray will be returned.
AZ::Vector3 FindClosestPickIntersection(
AzFramework::ViewportId viewportId, const AzFramework::ScreenPoint& screenPoint, float rayLength, float defaultDistance);
//! Maps a mouse interaction event to a ClickDetector event.
//! @note Function only cares about up or down events, all other events are mapped to Nil (ignored).
AzFramework::ClickDetector::ClickEvent ClickDetectorEventFromViewportInteraction(
@@ -148,7 +148,6 @@ namespace AzToolsFramework
return false;
}
EditorHelpers::EditorHelpers(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache)
{
@@ -190,7 +189,10 @@ namespace AzToolsFramework
if (helpersVisible)
{
// some components choose to hide their icons (e.g. meshes)
if (!m_entityDataCache->IsVisibleEntityIconHidden(entityCacheIndex))
// we also do not want to test against icons that may not be showing as they're inside a 'closed' entity container
// (these icons only become visible when it is opened for editing)
if (!m_entityDataCache->IsVisibleEntityIconHidden(entityCacheIndex) &&
m_entityDataCache->IsVisibleEntityIndividuallySelectableInViewport(entityCacheIndex))
{
const AZ::Vector3& entityPosition = m_entityDataCache->GetVisibleEntityPosition(entityCacheIndex);
@@ -235,7 +237,7 @@ namespace AzToolsFramework
viewportId, &ViewportInteraction::ViewportMouseCursorRequestBus::Events::SetOverrideCursor,
ViewportInteraction::CursorStyleOverride::Forbidden);
}
if (mouseInteraction.m_mouseInteraction.m_mouseButtons.Left() &&
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::Down ||
mouseInteraction.m_mouseEvent == ViewportInteraction::MouseEvent::DoubleClick)
@@ -18,9 +18,6 @@
namespace AzToolsFramework
{
// default ray length for picking in the viewport
static const float EditorPickRayLength = 1000.0f;
AZ::Vector3 CalculateCenterOffset(const AZ::EntityId entityId, const EditorTransformComponentSelectionRequests::Pivot pivot)
{
if (Centered(pivot))
@@ -26,6 +26,9 @@ namespace AzFramework
namespace AzToolsFramework
{
//! Default ray length for picking in the viewport.
inline constexpr float EditorPickRayLength = 1000.0f;
//! Is the pivot at the center of the object (middle of extents) or at the
//! exported authored object root position.
inline bool Centered(const EditorTransformComponentSelectionRequests::Pivot pivot)
@@ -27,6 +27,7 @@
#include <AzToolsFramework/Manipulators/ScaleManipulators.h>
#include <AzToolsFramework/Manipulators/TranslationManipulators.h>
#include <AzToolsFramework/Maths/TransformUtils.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusInterface.h>
#include <AzToolsFramework/Prefab/PrefabFocusPublicInterface.h>
#include <AzToolsFramework/ToolsComponents/EditorLockComponentBus.h>
@@ -409,7 +410,7 @@ namespace AzToolsFramework
const AzFramework::CameraState cameraState = GetCameraState(viewportId);
for (size_t entityCacheIndex = 0; entityCacheIndex < entityDataCache.VisibleEntityDataCount(); ++entityCacheIndex)
{
if (!entityDataCache.IsVisibleEntitySelectableInViewport(entityCacheIndex))
if (!entityDataCache.IsVisibleEntityIndividuallySelectableInViewport(entityCacheIndex))
{
continue;
}
@@ -983,7 +984,7 @@ namespace AzToolsFramework
{
if (auto entityIndex = entityDataCache.GetVisibleEntityIndexFromId(entityId))
{
if (entityDataCache.IsVisibleEntitySelectableInViewport(*entityIndex))
if (entityDataCache.IsVisibleEntityIndividuallySelectableInViewport(*entityIndex))
{
return *entityIndex;
}
@@ -1014,6 +1015,15 @@ namespace AzToolsFramework
ToolsApplicationNotificationBus::Broadcast(&ToolsApplicationNotificationBus::Events::InvalidatePropertyDisplay, Refresh_Values);
}
// leaves focus mode by focusing on the parent of the current perfab in the entity outliner
static void LeaveFocusMode()
{
if (auto prefabFocusPublicInterface = AZ::Interface<Prefab::PrefabFocusPublicInterface>::Get())
{
prefabFocusPublicInterface->FocusOnParentOfFocusedPrefab(GetEntityContextId());
}
}
EditorTransformComponentSelection::EditorTransformComponentSelection(const EditorVisibleEntityDataCache* entityDataCache)
: m_entityDataCache(entityDataCache)
{
@@ -3674,7 +3684,8 @@ namespace AzToolsFramework
case ViewportEditorMode::Focus:
{
ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode");
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode",
LeaveFocusMode);
}
break;
case ViewportEditorMode::Default:
@@ -3703,12 +3714,14 @@ namespace AzToolsFramework
if (editorModeState.IsModeActive(ViewportEditorMode::Focus))
{
ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode");
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::CreateViewportBorder, "Focus Mode",
LeaveFocusMode);
}
}
break;
case ViewportEditorMode::Focus:
{
ViewportUi::ViewportUiRequestBus::Event(
ViewportUi::DefaultViewportId, &ViewportUi::ViewportUiRequestBus::Events::RemoveViewportBorder);
}
@@ -293,12 +293,10 @@ namespace AzToolsFramework
return m_impl->m_visibleEntityDatas[index].m_iconHidden;
}
bool EditorVisibleEntityDataCache::IsVisibleEntitySelectableInViewport(size_t index) const
bool EditorVisibleEntityDataCache::IsVisibleEntityIndividuallySelectableInViewport(const size_t index) const
{
return m_impl->m_visibleEntityDatas[index].m_visible
&& !m_impl->m_visibleEntityDatas[index].m_locked
&& m_impl->m_visibleEntityDatas[index].m_inFocus
&& !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
return m_impl->m_visibleEntityDatas[index].m_visible && !m_impl->m_visibleEntityDatas[index].m_locked &&
m_impl->m_visibleEntityDatas[index].m_inFocus && !m_impl->m_visibleEntityDatas[index].m_descendantOfClosedContainer;
}
AZStd::optional<size_t> EditorVisibleEntityDataCache::GetVisibleEntityIndexFromId(const AZ::EntityId entityId) const
@@ -55,7 +55,10 @@ namespace AzToolsFramework
bool IsVisibleEntityVisible(size_t index) const;
bool IsVisibleEntitySelected(size_t index) const;
bool IsVisibleEntityIconHidden(size_t index) const;
bool IsVisibleEntitySelectableInViewport(size_t index) const;
//! Returns true if the entity is individually selectable (none of its ancestors are a closed container entity).
//! @note It may still be desirable to be able to 'click' an entity that is a descendant of a closed container
//! to select the container itself, not the individual entity.
bool IsVisibleEntityIndividuallySelectableInViewport(size_t index) const;
AZStd::optional<size_t> GetVisibleEntityIndexFromId(AZ::EntityId entityId) const;
@@ -62,9 +62,6 @@ namespace AzToolsFramework::ViewportUi::Internal
return;
}
// set hover to true by default
action->setProperty("IconHasHoverEffect", true);
// add the action
addAction(action);
@@ -20,7 +20,9 @@
namespace AzToolsFramework::ViewportUi::Internal
{
const static int HighlightBorderSize = 5;
const static char* HighlightBorderColor = "#4A90E2";
const static char* const HighlightBorderColor = "#4A90E2";
const static int HighlightBorderBackButtonIconSize = 20;
const static char* const HighlightBorderBackButtonIconFile = "X_axis.svg";
static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup)
{
@@ -62,6 +64,7 @@ namespace AzToolsFramework::ViewportUi::Internal
, m_fullScreenLayout(&m_uiOverlay)
, m_uiOverlayLayout()
, m_viewportBorderText(&m_uiOverlay)
, m_viewportBorderBackButton(&m_uiOverlay)
{
}
@@ -254,7 +257,7 @@ namespace AzToolsFramework::ViewportUi::Internal
auto viewportUiMapElement = m_viewportUiElements.find(elementId);
if (viewportUiMapElement != m_viewportUiElements.end())
{
viewportUiMapElement->second.m_widget->setVisible(false);
viewportUiMapElement->second.m_widget->hide();
viewportUiMapElement->second.m_widget->setParent(nullptr);
m_viewportUiElements.erase(viewportUiMapElement);
}
@@ -269,7 +272,7 @@ namespace AzToolsFramework::ViewportUi::Internal
{
if (ViewportUiElementInfo element = GetViewportUiElementInfo(elementId); element.m_widget)
{
element.m_widget->setVisible(true);
element.m_widget->show();
}
}
@@ -277,7 +280,7 @@ namespace AzToolsFramework::ViewportUi::Internal
{
if (ViewportUiElementInfo element = GetViewportUiElementInfo(elementId); element.m_widget)
{
element.m_widget->setVisible(false);
element.m_widget->hide();
}
}
@@ -291,27 +294,34 @@ namespace AzToolsFramework::ViewportUi::Internal
return false;
}
void ViewportUiDisplay::CreateViewportBorder(const AZStd::string& borderTitle)
void ViewportUiDisplay::CreateViewportBorder(
const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback)
{
const AZStd::string styleSheet = AZStd::string::format(
"border: %dpx solid %s; border-top: %dpx solid %s;", HighlightBorderSize, HighlightBorderColor, ViewportUiTopBorderSize,
HighlightBorderColor);
m_uiOverlay.setStyleSheet(styleSheet.c_str());
m_uiOverlay.setStyleSheet(QString("border: %1px solid %2; border-top: %3px solid %4;")
.arg(
QString::number(HighlightBorderSize), HighlightBorderColor,
QString::number(ViewportUiTopBorderSize), HighlightBorderColor));
m_uiOverlayLayout.setContentsMargins(
HighlightBorderSize + ViewportUiOverlayMargin, ViewportUiTopBorderSize + ViewportUiOverlayMargin,
HighlightBorderSize + ViewportUiOverlayMargin, HighlightBorderSize + ViewportUiOverlayMargin);
m_viewportBorderText.setVisible(true);
m_viewportBorderText.show();
m_viewportBorderText.setText(borderTitle.c_str());
UpdateUiOverlayGeometry();
// only display the back button if a callback was provided
m_viewportBorderBackButtonCallback = backButtonCallback;
m_viewportBorderBackButton.setVisible(m_viewportBorderBackButtonCallback.has_value());
}
void ViewportUiDisplay::RemoveViewportBorder()
{
m_viewportBorderText.setVisible(false);
m_viewportBorderText.hide();
m_uiOverlay.setStyleSheet("border: none;");
m_uiOverlayLayout.setContentsMargins(
ViewportUiOverlayMargin, ViewportUiOverlayMargin + ViewportUiOverlayTopMarginPadding, ViewportUiOverlayMargin,
ViewportUiOverlayMargin);
m_viewportBorderBackButtonCallback.reset();
m_viewportBorderBackButton.hide();
}
void ViewportUiDisplay::PositionViewportUiElementFromWorldSpace(ViewportUiElementId elementId, const AZ::Vector3& pos)
@@ -350,23 +360,46 @@ namespace AzToolsFramework::ViewportUi::Internal
{
m_uiMainWindow.setObjectName(QString("ViewportUiWindow"));
ConfigureWindowForViewportUi(&m_uiMainWindow);
m_uiMainWindow.setVisible(false);
m_uiMainWindow.hide();
m_uiOverlay.setObjectName(QString("ViewportUiOverlay"));
m_uiMainWindow.setCentralWidget(&m_uiOverlay);
m_uiOverlay.setVisible(false);
m_uiOverlay.hide();
// remove any spacing and margins from the UI Overlay Layout
m_fullScreenLayout.setSpacing(0);
m_fullScreenLayout.setContentsMargins(0, 0, 0, 0);
m_fullScreenLayout.addLayout(&m_uiOverlayLayout, 0, 0, 1, 1);
// format the label which will appear on top of the highlight border
AZStd::string styleSheet = AZStd::string::format("background-color: %s; border: none;", HighlightBorderColor);
m_viewportBorderText.setStyleSheet(styleSheet.c_str());
// style the label which will appear on top of the highlight border
m_viewportBorderText.setStyleSheet(QString("background-color: %1; border: none").arg(HighlightBorderColor));
m_viewportBorderText.setFixedHeight(ViewportUiTopBorderSize);
m_viewportBorderText.setVisible(false);
m_viewportBorderText.hide();
m_fullScreenLayout.addWidget(&m_viewportBorderText, 0, 0, Qt::AlignTop | Qt::AlignHCenter);
m_viewportBorderBackButton.setAutoRaise(true); // hover highlight
m_viewportBorderBackButton.hide();
QIcon backButtonIcon(QString(":/stylesheet/img/UI20/toolbar/%1").arg(HighlightBorderBackButtonIconFile));
m_viewportBorderBackButton.setIcon(backButtonIcon);
m_viewportBorderBackButton.setIconSize(QSize(HighlightBorderBackButtonIconSize, HighlightBorderBackButtonIconSize));
// setup the handler for the back button to call the user provided callback (if any)
QObject::connect(
&m_viewportBorderBackButton, &QToolButton::clicked,
[this]
{
if (m_viewportBorderBackButtonCallback.has_value())
{
// we need to swap out the existing back button callback because it will be reset in RemoveViewportBorder()
// so preserve the lifetime with this temporary callback until after the call to RemoveViewportBorder()
AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback;
m_viewportBorderBackButtonCallback.swap(backButtonCallback);
RemoveViewportBorder();
(*backButtonCallback)();
}
});
m_fullScreenLayout.addWidget(&m_viewportBorderBackButton, 0, 0, Qt::AlignTop | Qt::AlignRight);
}
void ViewportUiDisplay::PrepareWidgetForViewportUi(QPointer<QWidget> widget)
@@ -414,16 +447,9 @@ namespace AzToolsFramework::ViewportUi::Internal
region += m_uiOverlay.childrenRegion();
// set viewport ui visibility depending on if elements are present
if (region.isEmpty() || !UiDisplayEnabled())
{
m_uiMainWindow.setVisible(false);
m_uiOverlay.setVisible(false);
}
else
{
m_uiMainWindow.setVisible(true);
m_uiOverlay.setVisible(true);
}
const bool visible = !region.isEmpty() && UiDisplayEnabled();
m_uiMainWindow.setVisible(visible);
m_uiOverlay.setVisible(visible);
m_uiMainWindow.setMask(region);
}
@@ -17,6 +17,7 @@
#include <QLabel>
#include <QMainWindow>
#include <QPointer>
#include <QToolButton>
AZ_PUSH_DISABLE_WARNING(4251, "-Wunknown-warning-option")
#include <QGridLayout>
@@ -89,7 +90,7 @@ namespace AzToolsFramework::ViewportUi::Internal
AZStd::shared_ptr<QWidget> GetViewportUiElement(ViewportUiElementId elementId);
bool IsViewportUiElementVisible(ViewportUiElementId elementId);
void CreateViewportBorder(const AZStd::string& borderTitle);
void CreateViewportBorder(const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback);
void RemoveViewportBorder();
private:
@@ -113,7 +114,10 @@ namespace AzToolsFramework::ViewportUi::Internal
QWidget m_uiOverlay; //!< The UI Overlay which displays Viewport UI Elements.
QGridLayout m_fullScreenLayout; //!< The layout which extends across the full screen.
ViewportUiDisplayLayout m_uiOverlayLayout; //!< The layout used for optionally anchoring Viewport UI Elements.
QLabel m_viewportBorderText; //!< The text used for the viewport border.
QLabel m_viewportBorderText; //!< The text used for the viewport highlight border.
QToolButton m_viewportBorderBackButton; //!< The button to return from the viewport highlight border (only displayed if callback provided).
//! The optional callback for when the viewport highlight border back button is pressed.
AZStd::optional<ViewportUiBackButtonCallback> m_viewportBorderBackButtonCallback;
QWidget* m_renderOverlay;
QPointer<QWidget> m_fullScreenWidget; //!< Reference to the widget attached to m_fullScreenLayout if any.
@@ -240,9 +240,10 @@ namespace AzToolsFramework::ViewportUi
}
}
void ViewportUiManager::CreateViewportBorder(const AZStd::string& borderTitle)
void ViewportUiManager::CreateViewportBorder(
const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback)
{
m_viewportUi->CreateViewportBorder(borderTitle);
m_viewportUi->CreateViewportBorder(borderTitle, backButtonCallback);
}
void ViewportUiManager::RemoveViewportBorder()
@@ -50,7 +50,8 @@ namespace AzToolsFramework::ViewportUi
void RegisterTextFieldCallback(TextFieldId textFieldId, AZ::Event<AZStd::string>::Handler& handler) override;
void RemoveTextField(TextFieldId textFieldId) override;
void SetTextFieldVisible(TextFieldId textFieldId, bool visible) override;
void CreateViewportBorder(const AZStd::string& borderTitle) override;
void CreateViewportBorder(
const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback) override;
void RemoveViewportBorder() override;
void PressButton(ClusterId clusterId, ButtonId buttonId) override;
void PressButton(SwitcherId switcherId, ButtonId buttonId) override;
@@ -22,6 +22,9 @@ namespace AzToolsFramework::ViewportUi
using SwitcherId = IdType<struct SwitcherIdType>;
using TextFieldId = IdType<struct TextFieldIdType>;
//! Callback function for viewport UI back button.
using ViewportUiBackButtonCallback = AZStd::function<void()>;
inline const ViewportUiElementId InvalidViewportUiElementId = ViewportUiElementId(0);
inline const ButtonId InvalidButtonId = ButtonId(0);
inline const ClusterId InvalidClusterId = ClusterId(0);
@@ -95,9 +98,9 @@ namespace AzToolsFramework::ViewportUi
virtual void RemoveTextField(TextFieldId textFieldId) = 0;
//! Sets the visibility of the text field.
virtual void SetTextFieldVisible(TextFieldId textFieldId, bool visible) = 0;
//! Create the highlight border for Component Mode.
virtual void CreateViewportBorder(const AZStd::string& borderTitle) = 0;
//! Remove the highlight border for Component Mode.
//! Create the highlight border with optional back button to exit the given editor mode.
virtual void CreateViewportBorder(const AZStd::string& borderTitle, AZStd::optional<ViewportUiBackButtonCallback> backButtonCallback) = 0;
//! Remove the highlight border.
virtual void RemoveViewportBorder() = 0;
//! Invoke a button press on a cluster.
virtual void PressButton(ClusterId clusterId, ButtonId buttonId) = 0;
@@ -22,8 +22,6 @@ namespace AzToolsFramework::ViewportUi::Internal
// Add am empty active button (is set in the call to SetActiveMode)
m_activeButton = new QToolButton();
// No hover effect for the main button as it's not clickable
m_activeButton->setProperty("IconHasHoverEffect", false);
m_activeButton->setCheckable(false);
m_activeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
addWidget(m_activeButton);
@@ -56,9 +54,6 @@ namespace AzToolsFramework::ViewportUi::Internal
return;
}
// set hover to true by default
action->setProperty("IconHasHoverEffect", true);
// add the action
addAction(action);
@@ -40,6 +40,9 @@ namespace UnitTest
{
AzFramework::BoundsRequestBus::Handler::BusConnect(GetEntityId());
AzToolsFramework::EditorComponentSelectionRequestsBus::Handler::BusConnect(GetEntityId());
// default local bounds to unit cube
m_localBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
}
void BoundsTestComponent::Deactivate()
@@ -57,7 +60,6 @@ namespace UnitTest
AZ::Aabb BoundsTestComponent::GetLocalBounds()
{
return AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f), AZ::Vector3(0.5f));
return m_localBounds;
}
} // namespace UnitTest
@@ -41,5 +41,7 @@ namespace UnitTest
// BoundsRequestBus overrides ...
AZ::Aabb GetWorldBounds() override;
AZ::Aabb GetLocalBounds() override;
AZ::Aabb m_localBounds; //!< Local bounds that can be modified for certain tests (defaults to unit cube).
};
} // namespace UnitTest
@@ -38,7 +38,7 @@
#include <AzToolsFramework/ViewportSelection/EditorVisibleEntityDataCache.h>
#include <AzToolsFramework/ViewportUi/ViewportUiManager.h>
#include<Tests/BoundsTestComponent.h>
#include <Tests/BoundsTestComponent.h>
namespace AZ
{
@@ -493,12 +493,8 @@ namespace UnitTest
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
AzToolsFramework::EntityIdList selectedEntities;
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
selectedEntities, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities);
AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 };
const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
const AzToolsFramework::EntityIdList expectedSelectedEntities = { entity4, entity5, entity6 };
EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
@@ -527,12 +523,8 @@ namespace UnitTest
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Then
AzToolsFramework::EntityIdList selectedEntities;
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
selectedEntities, &AzToolsFramework::ToolsApplicationRequestBus::Events::GetSelectedEntities);
AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId1, entity2, entity3, entity4 };
const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
const AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId1, entity2, entity3, entity4 };
EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
@@ -946,6 +938,42 @@ namespace UnitTest
EXPECT_THAT(selectedEntitiesAfter, UnorderedElementsAre(m_entityId1));
}
TEST_F(
EditorTransformComponentSelectionViewportPickingManipulatorTestFixture, BoundsBetweenCameraAndNearClipPlaneDoesNotIntersectMouseRay)
{
// move camera to 10 units along the y-axis
AzFramework::SetCameraTransform(m_cameraState, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.0f)));
// send a very narrow bounds for entity1
AZ::Entity* entity1 = AzToolsFramework::GetEntityById(m_entityId1);
auto* boundTestComponent = entity1->FindComponent<BoundsTestComponent>();
boundTestComponent->m_localBounds =
AZ::Aabb::CreateFromMinMax(AZ::Vector3(-0.5f, -0.0025f, -0.5f), AZ::Vector3(0.5f, 0.0025f, 0.5f));
// move entity1 in front of the camera between it and the near clip plane
AZ::TransformBus::Event(
m_entityId1, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(10.05f)));
// move entity2 behind entity1
AZ::TransformBus::Event(
m_entityId2, &AZ::TransformBus::Events::SetWorldTM, AZ::Transform::CreateTranslation(AZ::Vector3::CreateAxisY(15.0f)));
const auto entity2ScreenPosition = AzFramework::WorldToScreen(AzToolsFramework::GetWorldTranslation(m_entityId2), m_cameraState);
// click the entity in the viewport
m_actionDispatcher->SetStickySelect(true)
->CameraState(m_cameraState)
->MousePosition(entity2ScreenPosition)
->CameraState(m_cameraState)
->MouseLButtonDown()
->MouseLButtonUp();
// ensure entity1 is not selected as it is before the near clip plane
using ::testing::UnorderedElementsAreArray;
const AzToolsFramework::EntityIdList selectedEntities = SelectedEntities();
const AzToolsFramework::EntityIdList expectedSelectedEntities = { m_entityId2 };
EXPECT_THAT(selectedEntities, UnorderedElementsAreArray(expectedSelectedEntities));
}
class EditorTransformComponentSelectionViewportPickingManipulatorTestFixtureParam
: public EditorTransformComponentSelectionViewportPickingManipulatorTestFixture
, public ::testing::WithParamInterface<bool>
@@ -23,6 +23,7 @@
#include <AzManipulatorTestFramework/AzManipulatorTestFrameworkTestHelpers.h>
#include <AzManipulatorTestFramework/IndirectManipulatorViewportInteraction.h>
#include <AzManipulatorTestFramework/ImmediateModeActionDispatcher.h>
#include <Tests/Utils/Printers.h>
using namespace AzToolsFramework;
@@ -106,7 +106,9 @@ namespace UnitTest
inline static const char* Passenger2EntityName = "Passenger2";
};
TEST_F(PrefabFocusTests, PrefabFocus_FocusOnOwningPrefab_RootContainer)
// Test was disabled because the implementation of GetFocusedPrefabInstance now relies on the Prefab EOS,
// which is not used by our test environment. This can be restored once Instance handles are implemented.
TEST_F(PrefabFocusTests, DISABLED_PrefabFocus_FocusOnOwningPrefab_RootContainer)
{
// Verify FocusOnOwningPrefab works when passing the container entity of the root prefab.
{
@@ -121,7 +123,9 @@ namespace UnitTest
}
}
TEST_F(PrefabFocusTests, PrefabFocus_FocusOnOwningPrefab_RootEntity)
// Test was disabled because the implementation of GetFocusedPrefabInstance now relies on the Prefab EOS,
// which is not used by our test environment. This can be restored once Instance handles are implemented.
TEST_F(PrefabFocusTests, DISABLED_PrefabFocus_FocusOnOwningPrefab_RootEntity)
{
// Verify FocusOnOwningPrefab works when passing a nested entity of the root prefab.
{
@@ -17,6 +17,7 @@
#include <AzTest/AzTest.h>
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
#include <AzToolsFramework/Viewport/ViewportTypes.h>
#include <Tests/Utils/Printers.h>
namespace UnitTest
{
@@ -35,6 +36,7 @@ namespace UnitTest
const auto worldResult = AzFramework::ScreenToWorld(screenPoint, cameraState);
return AzFramework::WorldToScreen(worldResult, cameraState);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// ScreenPoint tests
TEST(ViewportScreen, WorldToScreenAndScreenToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
@@ -102,8 +104,8 @@ namespace UnitTest
}
////////////////////////////////////////////////////////////////////////////////////////////////////////
// NDC tests
TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
// Ndc tests
TEST(ViewportScreen, WorldToScreenNdcAndScreenNdcToWorldReturnsTheSameValueIdentityCameraOffsetFromOrigin)
{
using NdcPoint = AZ::Vector2;
@@ -136,7 +138,7 @@ namespace UnitTest
}
}
TEST(ViewportScreen, WorldToScreenNDCAndScreenNDCToWorldReturnsTheSameValueOrientatedCamera)
TEST(ViewportScreen, WorldToScreenNdcAndScreenNdcToWorldReturnsTheSameValueOrientatedCamera)
{
using NdcPoint = AZ::Vector2;
@@ -153,7 +155,7 @@ namespace UnitTest
// note: nearClip is 0.1 - the world space value returned will be aligned to the near clip
// plane of the camera so use that to confirm the mapping to/from is correct
TEST(ViewportScreen, ScreenNDCToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
TEST(ViewportScreen, ScreenNdcToWorldReturnsPositionOnNearClipPlaneInWorldSpace)
{
using NdcPoint = AZ::Vector2;
@@ -23,6 +23,8 @@
#include <AzCore/RTTI/BehaviorContext.h>
//////////////////////////////////////////////////////////////////////////
#include <xxhash/xxhash.h>
namespace AssetBuilderSDK
{
const char* const ErrorWindow = "Error"; //Use this window name to log error messages.
@@ -1599,4 +1601,70 @@ namespace AssetBuilderSDK
{
return m_errorsOccurred;
}
AZ::u64 GetHashFromIOStream(AZ::IO::GenericStream& readStream, AZ::IO::SizeType* bytesReadOut, int hashMsDelay)
{
constexpr AZ::u64 HashBufferSize = 1024 * 64;
char buffer[HashBufferSize];
if(readStream.IsOpen() && readStream.CanRead())
{
AZ::IO::SizeType bytesRead;
auto* state = XXH64_createState();
if(state == nullptr)
{
AZ_Assert(false, "Failed to create hash state");
return 0;
}
if (XXH64_reset(state, 0) == XXH_ERROR)
{
AZ_Assert(false, "Failed to reset hash state");
return 0;
}
do
{
// In edge cases where another process is writing to this file while this hashing is occuring and that file wasn't locked,
// the following read check can fail because it performs an end of file check, and asserts and shuts down if the read size
// was smaller than the buffer and the read is not at the end of the file. The logic used to check end of file internal to read
// will be out of date in the edge cases where another process is actively writing to this file while this hash is running.
// The stream's length ends up more accurate in this case, preventing this assert and shut down.
// One area this occurs is the navigation mesh file (mnmnavmission0.bai) that's temporarily created when exporting a level,
// the navigation system can still be writing to this file when hashing begins, causing the EoF marker to change.
AZ::IO::SizeType remainingToRead = AZStd::min(readStream.GetLength() - readStream.GetCurPos(), aznumeric_cast<AZ::IO::SizeType>(AZ_ARRAY_SIZE(buffer)));
bytesRead = readStream.Read(remainingToRead, buffer);
if(bytesReadOut)
{
*bytesReadOut += bytesRead;
}
XXH64_update(state, buffer, bytesRead);
// Used by unit tests to force the race condition mentioned above, to verify the crash fix.
if(hashMsDelay > 0)
{
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(hashMsDelay));
}
} while (bytesRead > 0);
auto hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
return 0;
}
AZ::u64 GetFileHash(const char* filePath, AZ::IO::SizeType* bytesReadOut, int hashMsDelay)
{
constexpr bool ErrorOnReadFailure = true;
AZ::IO::FileIOStream readStream(filePath, AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, ErrorOnReadFailure);
return GetHashFromIOStream(readStream, bytesReadOut, hashMsDelay);
}
}
@@ -911,6 +911,19 @@ namespace AssetBuilderSDK
//! There can be multiple builders running at once, so we need to filter out ones coming from other builders
AZStd::thread_id m_jobThreadId;
};
//! Get hash for a whole file
//! @filePath the path for the file
//! @bytesReadOut output the read file size in bytes
//! @hashMsDelay [Do not use except for unit test] add a delay in ms for between each block reading.
AZ::u64 GetFileHash(const char* filePath, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
//! Get hash for a generic IO stream
//! @readStream the input readable stream
//! @bytesReadOut output the read size in bytes
//! @hashMsDelay [Do not use except for unit test] add a delay in ms for between each block reading.
AZ::u64 GetHashFromIOStream(AZ::IO::GenericStream& readStream, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
} // namespace AssetBuilderSDK
namespace AZ
@@ -32,6 +32,7 @@ ly_add_target(
PUBLIC
AZ::AzFramework
AZ::AzToolsFramework
3rdParty::xxhash
)
ly_add_source_properties(
SOURCES AssetBuilderSDK/AssetBuilderSDK.cpp
@@ -32,7 +32,8 @@ struct FolderRootWatch::PlatformImplementation
{
if (m_iNotifyHandle < 0)
{
m_iNotifyHandle = inotify_init();
// The CLOEXEC flag prevents the inotify watchers from copying on fork/exec
m_iNotifyHandle = inotify_init1(IN_CLOEXEC);
}
return (m_iNotifyHandle >= 0);
}
@@ -1161,7 +1161,7 @@ namespace AssetUtilities
{
#ifndef AZ_TESTS_ENABLED
// Only used for unit tests, speed is critical for GetFileHash.
AZ_UNUSED(hashMsDelay);
hashMsDelay = 0;
#endif
bool useFileHashing = ShouldUseFileHashing();
@@ -1170,10 +1170,10 @@ namespace AssetUtilities
return 0;
}
AZ::u64 hash = 0;
if(!force)
{
auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get();
AZ::u64 hash = 0;
if (fileStateInterface && fileStateInterface->GetHash(filePath, &hash))
{
@@ -1181,64 +1181,8 @@ namespace AssetUtilities
}
}
char buffer[FileHashBufferSize];
constexpr bool ErrorOnReadFailure = true;
AZ::IO::FileIOStream readStream(filePath, AZ::IO::OpenMode::ModeRead | AZ::IO::OpenMode::ModeBinary, ErrorOnReadFailure);
if(readStream.IsOpen() && readStream.CanRead())
{
AZ::IO::SizeType bytesRead;
auto* state = XXH64_createState();
if(state == nullptr)
{
AZ_Assert(false, "Failed to create hash state");
return 0;
}
if (XXH64_reset(state, 0) == XXH_ERROR)
{
AZ_Assert(false, "Failed to reset hash state");
return 0;
}
do
{
// In edge cases where another process is writing to this file while this hashing is occuring and that file wasn't locked,
// the following read check can fail because it performs an end of file check, and asserts and shuts down if the read size
// was smaller than the buffer and the read is not at the end of the file. The logic used to check end of file internal to read
// will be out of date in the edge cases where another process is actively writing to this file while this hash is running.
// The stream's length ends up more accurate in this case, preventing this assert and shut down.
// One area this occurs is the navigation mesh file (mnmnavmission0.bai) that's temporarily created when exporting a level,
// the navigation system can still be writing to this file when hashing begins, causing the EoF marker to change.
AZ::IO::SizeType remainingToRead = AZStd::min(readStream.GetLength() - readStream.GetCurPos(), aznumeric_cast<AZ::IO::SizeType>(AZ_ARRAY_SIZE(buffer)));
bytesRead = readStream.Read(remainingToRead, buffer);
if(bytesReadOut)
{
*bytesReadOut += bytesRead;
}
XXH64_update(state, buffer, bytesRead);
#ifdef AZ_TESTS_ENABLED
// Used by unit tests to force the race condition mentioned above, to verify the crash fix.
if(hashMsDelay > 0)
{
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(hashMsDelay));
}
#endif
} while (bytesRead > 0);
auto hash = XXH64_digest(state);
XXH64_freeState(state);
return hash;
}
return 0;
hash = AssetBuilderSDK::GetFileHash(filePath, bytesReadOut, hashMsDelay);
return hash;
}
AZ::u64 AdjustTimestamp(QDateTime timestamp)
@@ -238,7 +238,6 @@ namespace AssetUtilities
// hashMsDelay is only for automated tests to test that writing to a file while it's hashing does not cause a crash.
// hashMsDelay is not used in non-unit test builds.
AZ::u64 GetFileHash(const char* filePath, bool force = false, AZ::IO::SizeType* bytesReadOut = nullptr, int hashMsDelay = 0);
inline constexpr AZ::u64 FileHashBufferSize = 1024 * 64;
//! Adjusts a timestamp to fix timezone settings and account for any precision adjustment needed
AZ::u64 AdjustTimestamp(QDateTime timestamp);
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:342c3eaccf68a178dfd8c2b1792a93a8c9197c8184dca11bf90706d7481df087
size 1611268
oid sha256:e9ad0383f3b917fa7f4efa307a8e109a70bb5f66deb197189d013f60eb8dc32c
size 1010250
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:797794816e4b1702f1ae1f32b408c95c79eb1f8a95aba43cfad9cccc181b0bda
size 1135182
oid sha256:84aab95ec8a5e3ba6ecb3aff1a814afc3171a937aa658decd18c2740623bd172
size 984146
@@ -15,6 +15,7 @@
#include <GemCatalog/GemCatalogScreen.h>
#include <GemRepo/GemRepoScreen.h>
#include <ProjectUtils.h>
#include <DownloadController.h>
#include <QDialogButtonBox>
#include <QHBoxLayout>
@@ -73,13 +73,25 @@ namespace O3DE::ProjectManager
emit GemDownloadProgress(m_gemNames.front(), bytesDownloaded, totalBytes);
}
void DownloadController::HandleResults(const QString& result)
void DownloadController::HandleResults(const QString& result, const QString& detailedError)
{
bool succeeded = true;
if (!result.isEmpty())
{
QMessageBox::critical(nullptr, tr("Gem download"), result);
if (!detailedError.isEmpty())
{
QMessageBox gemDownloadError;
gemDownloadError.setIcon(QMessageBox::Critical);
gemDownloadError.setWindowTitle(tr("Gem download"));
gemDownloadError.setText(result);
gemDownloadError.setDetailedText(detailedError);
gemDownloadError.exec();
}
else
{
QMessageBox::critical(nullptr, tr("Gem download"), result);
}
succeeded = false;
}
@@ -54,7 +54,7 @@ namespace O3DE::ProjectManager
}
public slots:
void UpdateUIProgress(int bytesDownloaded, int totalBytes);
void HandleResults(const QString& result);
void HandleResults(const QString& result, const QString& detailedError);
signals:
void StartGemDownload(const QString& gemName);
@@ -24,16 +24,16 @@ namespace O3DE::ProjectManager
{
emit UpdateProgress(bytesDownloaded, totalBytes);
};
AZ::Outcome<void, AZStd::string> gemInfoResult =
AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> gemInfoResult =
PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress, /*force*/true);
if (gemInfoResult.IsSuccess())
{
emit Done("");
emit Done("", "");
}
else
{
emit Done(tr("Gem download failed"));
emit Done(gemInfoResult.GetError().first.c_str(), gemInfoResult.GetError().second.c_str());
}
}
@@ -32,7 +32,7 @@ namespace O3DE::ProjectManager
signals:
void UpdateProgress(int bytesDownloaded, int totalBytes);
void Done(QString result = "");
void Done(QString result = "", QString detailedResult = "");
private:
@@ -72,12 +72,7 @@ namespace O3DE::ProjectManager
bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen)
{
if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum())
{
return true;
}
return false;
return screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum();
}
void EngineScreenCtrl::NotifyCurrentScreen()
@@ -7,25 +7,32 @@
*/
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <TagWidget.h>
#include <AzCore/std/functional.h>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QLabel>
#include <QPushButton>
#include <QProgressBar>
#include <TagWidget.h>
#include <QMenu>
#include <QLocale>
#include <QMovie>
#include <QPainter>
#include <QPainterPath>
namespace O3DE::ProjectManager
{
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
: QWidget(parent)
GemCartWidget::GemCartWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
: QScrollArea(parent)
, m_gemModel(gemModel)
, m_downloadController(downloadController)
{
setObjectName("GemCatalogCart");
setWidgetResizable(true);
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_layout = new QVBoxLayout();
m_layout->setSpacing(0);
@@ -118,17 +125,15 @@ namespace O3DE::ProjectManager
}
return dependencies;
});
setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
}
CartOverlayWidget::~CartOverlayWidget()
GemCartWidget::~GemCartWidget()
{
// disconnect from all download controller signals
disconnect(m_downloadController, nullptr, this, nullptr);
}
void CartOverlayWidget::CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices)
void GemCartWidget::CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices)
{
QWidget* widget = new QWidget();
widget->setFixedWidth(s_width);
@@ -164,12 +169,12 @@ namespace O3DE::ProjectManager
update();
}
void CartOverlayWidget::OnCancelDownloadActivated(const QString& gemName)
void GemCartWidget::OnCancelDownloadActivated(const QString& gemName)
{
m_downloadController->CancelGemDownload(gemName);
}
void CartOverlayWidget::CreateDownloadSection()
void GemCartWidget::CreateDownloadSection()
{
m_downloadSectionWidget = new QWidget();
m_downloadSectionWidget->setFixedWidth(s_width);
@@ -223,12 +228,12 @@ namespace O3DE::ProjectManager
}
// connect to download controller data changed
connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &CartOverlayWidget::GemDownloadAdded);
connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &CartOverlayWidget::GemDownloadRemoved);
connect(m_downloadController, &DownloadController::GemDownloadProgress, this, &CartOverlayWidget::GemDownloadProgress);
connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &GemCartWidget::GemDownloadAdded);
connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &GemCartWidget::GemDownloadRemoved);
connect(m_downloadController, &DownloadController::GemDownloadProgress, this, &GemCartWidget::GemDownloadProgress);
}
void CartOverlayWidget::GemDownloadAdded(const QString& gemName)
void GemCartWidget::GemDownloadAdded(const QString& gemName)
{
// Containing widget for the current download item
QWidget* newGemDownloadWidget = new QWidget();
@@ -246,7 +251,7 @@ namespace O3DE::ProjectManager
nameProgressLayout->addStretch();
QLabel* cancelText = new QLabel(tr("<a href=\"%1\">Cancel</a>").arg(gemName), newGemDownloadWidget);
cancelText->setTextInteractionFlags(Qt::LinksAccessibleByMouse);
connect(cancelText, &QLabel::linkActivated, this, &CartOverlayWidget::OnCancelDownloadActivated);
connect(cancelText, &QLabel::linkActivated, this, &GemCartWidget::OnCancelDownloadActivated);
nameProgressLayout->addWidget(cancelText);
downloadingGemLayout->addLayout(nameProgressLayout);
@@ -267,7 +272,7 @@ namespace O3DE::ProjectManager
m_downloadingListWidget->show();
}
void CartOverlayWidget::GemDownloadRemoved(const QString& gemName)
void GemCartWidget::GemDownloadRemoved(const QString& gemName)
{
QWidget* gemToRemove = m_downloadingListWidget->findChild<QWidget*>(gemName);
if (gemToRemove)
@@ -289,7 +294,7 @@ namespace O3DE::ProjectManager
}
}
void CartOverlayWidget::GemDownloadProgress(const QString& gemName, int bytesDownloaded, int totalBytes)
void GemCartWidget::GemDownloadProgress(const QString& gemName, int bytesDownloaded, int totalBytes)
{
QWidget* gemToUpdate = m_downloadingListWidget->findChild<QWidget*>(gemName);
if (gemToUpdate)
@@ -324,7 +329,7 @@ namespace O3DE::ProjectManager
}
}
QVector<Tag> CartOverlayWidget::GetTagsFromModelIndices(const QVector<QModelIndex>& gems) const
QVector<Tag> GemCartWidget::GetTagsFromModelIndices(const QVector<QModelIndex>& gems) const
{
QVector<Tag> tags;
tags.reserve(gems.size());
@@ -349,7 +354,7 @@ namespace O3DE::ProjectManager
iconButton->setFocusPolicy(Qt::NoFocus);
iconButton->setIcon(QIcon(":/Summary.svg"));
iconButton->setFixedSize(s_iconSize, s_iconSize);
connect(iconButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
connect(iconButton, &QPushButton::clicked, this, &CartButton::ShowGemCart);
m_layout->addWidget(iconButton);
m_countLabel = new QLabel();
@@ -362,7 +367,7 @@ namespace O3DE::ProjectManager
m_dropDownButton->setFocusPolicy(Qt::NoFocus);
m_dropDownButton->setIcon(QIcon(":/CarrotArrowDown.svg"));
m_dropDownButton->setFixedSize(s_arrowDownIconSize, s_arrowDownIconSize);
connect(m_dropDownButton, &QPushButton::clicked, this, &CartButton::ShowOverlay);
connect(m_dropDownButton, &QPushButton::clicked, this, &CartButton::ShowGemCart);
m_layout->addWidget(m_dropDownButton);
// Adjust the label text whenever the model gets updated.
@@ -377,28 +382,28 @@ namespace O3DE::ProjectManager
m_dropDownButton->setVisible(!toBeAdded.isEmpty() || !toBeRemoved.isEmpty());
// Automatically close the overlay window in case there are no gems to be activated or deactivated anymore.
if (m_cartOverlay && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
if (m_gemCart && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
{
m_cartOverlay->deleteLater();
m_cartOverlay = nullptr;
m_gemCart->deleteLater();
m_gemCart = nullptr;
}
});
}
void CartButton::mousePressEvent([[maybe_unused]] QMouseEvent* event)
{
ShowOverlay();
ShowGemCart();
}
void CartButton::hideEvent(QHideEvent*)
{
if (m_cartOverlay)
if (m_gemCart)
{
m_cartOverlay->hide();
m_gemCart->hide();
}
}
void CartButton::ShowOverlay()
void CartButton::ShowGemCart()
{
const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/true);
const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/true);
@@ -407,37 +412,33 @@ namespace O3DE::ProjectManager
return;
}
if (m_cartOverlay)
if (m_gemCart)
{
// Directly delete the former overlay before creating the new one.
// Don't use deleteLater() here. This might overwrite the new overlay pointer
// depending on the event queue.
delete m_cartOverlay;
delete m_gemCart;
}
m_cartOverlay = new CartOverlayWidget(m_gemModel, m_downloadController, this);
connect(m_cartOverlay, &QWidget::destroyed, this, [=]
m_gemCart = new GemCartWidget(m_gemModel, m_downloadController, this);
connect(m_gemCart, &QWidget::destroyed, this, [=]
{
// Reset the overlay pointer on destruction to prevent dangling pointers.
m_cartOverlay = nullptr;
m_gemCart = nullptr;
// Tell header gem cart is no longer open
UpdateGemCart(nullptr);
});
m_cartOverlay->show();
m_gemCart->show();
const QPoint parentPos = m_dropDownButton->mapToParent(m_dropDownButton->pos());
const QPoint globalPos = m_dropDownButton->mapToGlobal(m_dropDownButton->pos());
const QPoint offset(-4, 10);
m_cartOverlay->setGeometry(globalPos.x() - parentPos.x() - m_cartOverlay->width() + width() + offset.x(),
globalPos.y() + offset.y(),
m_cartOverlay->width(),
m_cartOverlay->height());
emit UpdateGemCart(m_gemCart);
}
CartButton::~CartButton()
{
// Make sure the overlay window is automatically closed in case the gem catalog is destroyed.
if (m_cartOverlay)
if (m_gemCart)
{
m_cartOverlay->deleteLater();
m_gemCart->deleteLater();
}
}
@@ -514,6 +515,17 @@ namespace O3DE::ProjectManager
connect(m_downloadController, &DownloadController::GemDownloadAdded, this, &GemCatalogHeaderWidget::GemDownloadAdded);
connect(m_downloadController, &DownloadController::GemDownloadRemoved, this, &GemCatalogHeaderWidget::GemDownloadRemoved);
connect(
m_cartButton, &CartButton::UpdateGemCart, this,
[this](QWidget* gemCart)
{
GemCartShown(gemCart);
if (gemCart)
{
emit UpdateGemCart(gemCart);
}
});
}
void GemCatalogHeaderWidget::GemDownloadAdded(const QString& /*gemName*/)
@@ -521,7 +533,7 @@ namespace O3DE::ProjectManager
m_downloadSpinner->show();
m_downloadLabel->show();
m_downloadSpinnerMovie->start();
m_cartButton->ShowOverlay();
m_cartButton->ShowGemCart();
}
void GemCatalogHeaderWidget::GemDownloadRemoved(const QString& /*gemName*/)
@@ -534,8 +546,44 @@ namespace O3DE::ProjectManager
}
}
void GemCatalogHeaderWidget::GemCartShown(bool state)
{
m_showGemCart = state;
repaint();
}
void GemCatalogHeaderWidget::ReinitForProject()
{
m_filterLineEdit->setText({});
}
void GemCatalogHeaderWidget::paintEvent([[maybe_unused]] QPaintEvent* event)
{
// Only show triangle when cart is shown
if (!m_showGemCart)
{
return;
}
const QPoint buttonPos = m_cartButton->pos();
const QSize buttonSize = m_cartButton->size();
// Draw isosceles triangle with top point touching bottom of cartButton
// Bottom aligned with header bottom and top of right panel
const QPoint topPoint(buttonPos.x() + buttonSize.width() / 2, buttonPos.y() + buttonSize.height());
const QPoint bottomLeftPoint(topPoint.x() - 20, height());
const QPoint bottomRightPoint(topPoint.x() + 20, height());
QPainterPath trianglePath;
trianglePath.moveTo(topPoint);
trianglePath.lineTo(bottomLeftPoint);
trianglePath.lineTo(bottomRightPoint);
trianglePath.lineTo(topPoint);
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
painter.fillPath(trianglePath, QBrush(QColor("#555555")));
}
} // namespace O3DE::ProjectManager
@@ -14,8 +14,10 @@
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <TagWidget.h>
#include <QFrame>
#include <DownloadController.h>
#include <QFrame>
#include <QScrollArea>
#endif
QT_FORWARD_DECLARE_CLASS(QPushButton)
@@ -28,14 +30,14 @@ QT_FORWARD_DECLARE_CLASS(QMovie)
namespace O3DE::ProjectManager
{
class CartOverlayWidget
: public QWidget
class GemCartWidget
: public QScrollArea
{
Q_OBJECT // AUTOMOC
public:
CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
~CartOverlayWidget();
GemCartWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
~GemCartWidget();
public slots:
void GemDownloadAdded(const QString& gemName);
@@ -68,7 +70,10 @@ namespace O3DE::ProjectManager
public:
CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
~CartButton();
void ShowOverlay();
void ShowGemCart();
signals:
void UpdateGemCart(QWidget* gemCart);
private:
void mousePressEvent(QMouseEvent* event) override;
@@ -78,7 +83,7 @@ namespace O3DE::ProjectManager
QHBoxLayout* m_layout = nullptr;
QLabel* m_countLabel = nullptr;
QPushButton* m_dropDownButton = nullptr;
CartOverlayWidget* m_cartOverlay = nullptr;
GemCartWidget* m_gemCart = nullptr;
DownloadController* m_downloadController = nullptr;
inline constexpr static int s_iconSize = 24;
@@ -99,11 +104,16 @@ namespace O3DE::ProjectManager
public slots:
void GemDownloadAdded(const QString& gemName);
void GemDownloadRemoved(const QString& gemName);
void GemCartShown(bool state = false);
signals:
void AddGem();
void OpenGemsRepo();
void RefreshGems();
void UpdateGemCart(QWidget* gemCart);
protected slots:
void paintEvent(QPaintEvent* event) override;
private:
AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr;
@@ -113,5 +123,6 @@ namespace O3DE::ProjectManager
QLabel* m_downloadLabel = nullptr;
QMovie* m_downloadSpinnerMovie = nullptr;
CartButton* m_cartButton = nullptr;
bool m_showGemCart = false;
};
} // namespace O3DE::ProjectManager
@@ -8,6 +8,11 @@
#include <GemCatalog/GemCatalogScreen.h>
#include <PythonBindingsInterface.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemListHeaderWidget.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <GemCatalog/GemRequirementDialog.h>
@@ -28,6 +33,7 @@
#include <QFileDialog>
#include <QMessageBox>
#include <QHash>
#include <QStackedWidget>
namespace O3DE::ProjectManager
{
@@ -51,9 +57,12 @@ namespace O3DE::ProjectManager
vLayout->addWidget(m_headerWidget);
connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged);
connect(m_gemModel, &GemModel::dependencyGemStatusChanged, this, &GemCatalogScreen::OnDependencyGemStatusChanged);
connect(m_gemModel->GetSelectionModel(), &QItemSelectionModel::selectionChanged, this, [this]{ ShowInspector(); });
connect(m_headerWidget, &GemCatalogHeaderWidget::RefreshGems, this, &GemCatalogScreen::Refresh);
connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo);
connect(m_headerWidget, &GemCatalogHeaderWidget::AddGem, this, &GemCatalogScreen::OnAddGemClicked);
connect(m_headerWidget, &GemCatalogHeaderWidget::UpdateGemCart, this, &GemCatalogScreen::UpdateAndShowGemCart);
connect(m_downloadController, &DownloadController::Done, this, &GemCatalogScreen::OnGemDownloadResult);
QHBoxLayout* hLayout = new QHBoxLayout();
@@ -61,8 +70,11 @@ namespace O3DE::ProjectManager
vLayout->addLayout(hLayout);
m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), this);
m_rightPanelStack = new QStackedWidget(this);
m_rightPanelStack->setFixedWidth(240);
m_gemInspector = new GemInspector(m_gemModel, this);
m_gemInspector->setFixedWidth(240);
connect(m_gemInspector, &GemInspector::TagClicked, [=](const Tag& tag) { SelectGem(tag.id); });
connect(m_gemInspector, &GemInspector::UpdateGem, this, &GemCatalogScreen::UpdateGem);
@@ -85,7 +97,9 @@ namespace O3DE::ProjectManager
hLayout->addWidget(filterWidget);
hLayout->addLayout(middleVLayout);
hLayout->addWidget(m_gemInspector);
hLayout->addWidget(m_rightPanelStack);
m_rightPanelStack->addWidget(m_gemInspector);
m_notificationsView = AZStd::make_unique<AzToolsFramework::ToastNotificationsView>(this, AZ_CRC("GemCatalogNotificationsView"));
m_notificationsView->SetOffset(QPoint(10, 70));
@@ -188,7 +202,7 @@ namespace O3DE::ProjectManager
}
// add all the gem repos into the hash
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoGemsInfos();
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
if (allRepoGemInfosResult.IsSuccess())
{
const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@@ -266,7 +280,8 @@ namespace O3DE::ProjectManager
{
notification += tr(" and ");
}
if (added && GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded)
if (added && (GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded) ||
(GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::DownloadFailed))
{
m_downloadController->AddGemDownload(GemModel::GetName(modelIndex));
GemModel::SetDownloadStatus(*m_gemModel, modelIndex, GemInfo::DownloadStatus::Downloading);
@@ -291,6 +306,18 @@ namespace O3DE::ProjectManager
}
}
void GemCatalogScreen::OnDependencyGemStatusChanged(const QString& gemName)
{
QModelIndex modelIndex = m_gemModel->FindIndexByNameString(gemName);
bool added = GemModel::IsAddedDependency(modelIndex);
if (added && (GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded) ||
(GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::DownloadFailed))
{
m_downloadController->AddGemDownload(GemModel::GetName(modelIndex));
GemModel::SetDownloadStatus(*m_gemModel, modelIndex, GemInfo::DownloadStatus::Downloading);
}
}
void GemCatalogScreen::SelectGem(const QString& gemName)
{
QModelIndex modelIndex = m_gemModel->FindIndexByNameString(gemName);
@@ -303,6 +330,8 @@ namespace O3DE::ProjectManager
QModelIndex proxyIndex = m_proxyModel->mapFromSource(modelIndex);
m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
m_gemListView->scrollTo(proxyIndex);
ShowInspector();
}
void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex)
@@ -363,8 +392,12 @@ namespace O3DE::ProjectManager
{
const QString selectedGemPath = m_gemModel->GetPath(modelIndex);
// Remove gem from gems to be added
const bool wasAdded = GemModel::WasPreviouslyAdded(modelIndex);
const bool wasAddedDependency = GemModel::WasPreviouslyAddedDependency(modelIndex);
// Remove gem from gems to be added to update any dependencies
GemModel::SetIsAdded(*m_gemModel, modelIndex, false);
GemModel::DeactivateDependentGems(*m_gemModel, modelIndex);
// Unregister the gem
auto unregisterResult = PythonBindingsInterface::Get()->UnregisterGem(selectedGemPath);
@@ -391,6 +424,8 @@ namespace O3DE::ProjectManager
// Select remote gem
QModelIndex remoteGemIndex = m_gemModel->FindIndexByNameString(selectedGemName);
GemModel::SetWasPreviouslyAdded(*m_gemModel, remoteGemIndex, wasAdded);
GemModel::SetWasPreviouslyAddedDependency(*m_gemModel, remoteGemIndex, wasAddedDependency);
QModelIndex proxyIndex = m_proxyModel->mapFromSource(remoteGemIndex);
m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
}
@@ -435,7 +470,7 @@ namespace O3DE::ProjectManager
m_gemModel->AddGem(gemInfo);
}
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoGemsInfos();
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
if (allRepoGemInfosResult.IsSuccess())
{
const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
@@ -491,6 +526,12 @@ namespace O3DE::ProjectManager
}
}
void GemCatalogScreen::ShowInspector()
{
m_rightPanelStack->setCurrentIndex(RightPanelWidgetOrder::Inspector);
m_headerWidget->GemCartShown();
}
GemCatalogScreen::EnableDisableGemsResult GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
{
IPythonBindings* pythonBindings = PythonBindingsInterface::Get();
@@ -523,7 +564,9 @@ namespace O3DE::ProjectManager
const QString& gemPath = GemModel::GetPath(modelIndex);
// make sure any remote gems we added were downloaded successfully
if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote && GemModel::GetDownloadStatus(modelIndex) != GemInfo::Downloaded)
const GemInfo::DownloadStatus status = GemModel::GetDownloadStatus(modelIndex);
if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote &&
!(status == GemInfo::Downloaded || status == GemInfo::DownloadSuccessful))
{
QMessageBox::critical(
nullptr, "Cannot add gem that isn't downloaded",
@@ -570,6 +613,18 @@ namespace O3DE::ProjectManager
emit ChangeScreenRequest(ProjectManagerScreen::GemRepos);
}
void GemCatalogScreen::UpdateAndShowGemCart(QWidget* cartWidget)
{
QWidget* previousCart = m_rightPanelStack->widget(RightPanelWidgetOrder::Cart);
if (previousCart)
{
m_rightPanelStack->removeWidget(previousCart);
}
m_rightPanelStack->insertWidget(RightPanelWidgetOrder::Cart, cartWidget);
m_rightPanelStack->setCurrentIndex(RightPanelWidgetOrder::Cart);
}
void GemCatalogScreen::OnGemDownloadResult(const QString& gemName, bool succeeded)
{
if (succeeded)
@@ -620,6 +675,8 @@ namespace O3DE::ProjectManager
QModelIndex index = m_gemModel->FindIndexByNameString(gemName);
if (index.isValid())
{
GemModel::SetIsAdded(*m_gemModel, index, false);
GemModel::DeactivateDependentGems(*m_gemModel, index);
GemModel::SetDownloadStatus(*m_gemModel, index, GemInfo::DownloadFailed);
}
}
@@ -12,18 +12,24 @@
#include <ScreenWidget.h>
#include <AzCore/std/smart_ptr/unique_ptr.h>
#include <AzToolsFramework/UI/Notifications/ToastNotificationsView.h>
#include <GemCatalog/GemCatalogHeaderWidget.h>
#include <GemCatalog/GemFilterWidget.h>
#include <GemCatalog/GemListView.h>
#include <GemCatalog/GemInspector.h>
#include <GemCatalog/GemModel.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <QSet>
#include <QString>
#endif
QT_FORWARD_DECLARE_CLASS(QVBoxLayout)
QT_FORWARD_DECLARE_CLASS(QStackedWidget)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(GemCatalogHeaderWidget)
QT_FORWARD_DECLARE_CLASS(GemFilterWidget)
QT_FORWARD_DECLARE_CLASS(GemListView)
QT_FORWARD_DECLARE_CLASS(GemInspector)
QT_FORWARD_DECLARE_CLASS(GemModel)
QT_FORWARD_DECLARE_CLASS(GemSortFilterProxyModel)
QT_FORWARD_DECLARE_CLASS(DownloadController)
class GemCatalogScreen
: public ScreenWidget
{
@@ -47,6 +53,7 @@ namespace O3DE::ProjectManager
public slots:
void OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies);
void OnDependencyGemStatusChanged(const QString& gemName);
void OnAddGemClicked();
void SelectGem(const QString& gemName);
void OnGemDownloadResult(const QString& gemName, bool succeeded = true);
@@ -62,14 +69,22 @@ namespace O3DE::ProjectManager
private slots:
void HandleOpenGemRepo();
void UpdateAndShowGemCart(QWidget* cartWidget);
void ShowInspector();
private:
enum RightPanelWidgetOrder
{
Inspector = 0,
Cart
};
void FillModel(const QString& projectPath);
AZStd::unique_ptr<AzToolsFramework::ToastNotificationsView> m_notificationsView;
GemListView* m_gemListView = nullptr;
QStackedWidget* m_rightPanelStack = nullptr;
GemInspector* m_gemInspector = nullptr;
GemModel* m_gemModel = nullptr;
GemCatalogHeaderWidget* m_headerWidget = nullptr;
@@ -53,10 +53,13 @@ namespace O3DE::ProjectManager
Update(selectedIndices[0]);
}
void SetLabelElidedText(QLabel* label, QString text)
void SetLabelElidedText(QLabel* label, QString text, int labelWidth = 0)
{
QFontMetrics nameFontMetrics(label->font());
int labelWidth = label->width();
if (!labelWidth)
{
labelWidth = label->width();
}
// Don't elide if the widgets are sized too small (sometimes occurs when loading gem catalog)
if (labelWidth > 100)
@@ -84,7 +87,8 @@ namespace O3DE::ProjectManager
m_summaryLabel->setText(m_model->GetSummary(modelIndex));
m_summaryLabel->adjustSize();
m_licenseLinkLabel->setText(m_model->GetLicenseText(modelIndex));
// Manually define remaining space to elide text because spacer would like to take all of the space
SetLabelElidedText(m_licenseLinkLabel, m_model->GetLicenseText(modelIndex), width() - m_licenseLabel->width() - 35);
m_licenseLinkLabel->SetUrl(m_model->GetLicenseLink(modelIndex));
m_directoryLinkLabel->SetUrl(m_model->GetDirectoryLink(modelIndex));
@@ -175,8 +179,8 @@ namespace O3DE::ProjectManager
licenseHLayout->setAlignment(Qt::AlignLeft);
m_mainLayout->addLayout(licenseHLayout);
QLabel* licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor);
licenseLabel->setText(tr("License: "));
m_licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor);
m_licenseLabel->setText(tr("License: "));
m_licenseLinkLabel = new LinkLabel("", QUrl(), s_baseFontSize);
licenseHLayout->addWidget(m_licenseLinkLabel);
@@ -64,6 +64,7 @@ namespace O3DE::ProjectManager
QLabel* m_nameLabel = nullptr;
QLabel* m_creatorLabel = nullptr;
QLabel* m_summaryLabel = nullptr;
QLabel* m_licenseLabel = nullptr;
LinkLabel* m_licenseLinkLabel = nullptr;
LinkLabel* m_directoryLinkLabel = nullptr;
LinkLabel* m_documentationLinkLabel = nullptr;
@@ -357,6 +357,8 @@ namespace O3DE::ProjectManager
if (!IsAdded(dependency))
{
numChangedDependencies++;
const QString dependencyName = gemModel->GetName(dependency);
gemModel->emit dependencyGemStatusChanged(dependencyName);
}
}
}
@@ -381,6 +383,8 @@ namespace O3DE::ProjectManager
if (!IsAdded(dependency))
{
numChangedDependencies++;
const QString dependencyName = gemModel->GetName(dependency);
gemModel->emit dependencyGemStatusChanged(dependencyName);
}
}
}
@@ -479,6 +483,23 @@ namespace O3DE::ProjectManager
return previouslyAdded && !added;
}
void GemModel::DeactivateDependentGems(QAbstractItemModel& model, const QModelIndex& modelIndex)
{
GemModel* gemModel = GetSourceModel(&model);
AZ_Assert(gemModel, "Failed to obtain GemModel");
QVector<QModelIndex> dependentGems = gemModel->GatherDependentGems(modelIndex);
if (!dependentGems.isEmpty())
{
// we need to deactivate all gems that depend on this one
for (auto dependentModelIndex : dependentGems)
{
SetIsAdded(model, dependentModelIndex, false);
}
}
}
void GemModel::SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status)
{
model.setData(modelIndex, status, RoleDownloadStatus);
@@ -99,6 +99,7 @@ namespace O3DE::ProjectManager
static bool NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies = false);
static bool HasRequirement(const QModelIndex& modelIndex);
static void UpdateDependencies(QAbstractItemModel& model, const QString& gemName, bool isAdded);
static void DeactivateDependentGems(QAbstractItemModel& model, const QModelIndex& modelIndex);
static void SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status);
bool DoGemsToBeAddedHaveRequirements() const;
@@ -113,6 +114,7 @@ namespace O3DE::ProjectManager
signals:
void gemStatusChanged(const QString& gemName, uint32_t numChangedDependencies);
void dependencyGemStatusChanged(const QString& gemName);
protected slots:
void OnRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last);
@@ -37,7 +37,7 @@ namespace O3DE::ProjectManager
QString m_additionalInfo = "";
QString m_directoryLink = "";
QString m_repoUri = "";
QStringList m_includedGemPaths = {};
QStringList m_includedGemUris = {};
QDateTime m_lastUpdated;
};
} // namespace O3DE::ProjectManager
@@ -8,6 +8,7 @@
#include <GemRepo/GemRepoInspector.h>
#include <GemRepo/GemRepoItemDelegate.h>
#include <PythonBindingsInterface.h>
#include <QFrame>
#include <QLabel>
@@ -60,8 +61,10 @@ namespace O3DE::ProjectManager
// Repo name and url link
m_nameLabel->setText(m_model->GetName(modelIndex));
m_repoLinkLabel->setText(m_model->GetRepoUri(modelIndex));
m_repoLinkLabel->SetUrl(m_model->GetRepoUri(modelIndex));
const QString repoUri = m_model->GetRepoUri(modelIndex);
m_repoLinkLabel->setText(repoUri);
m_repoLinkLabel->SetUrl(repoUri);
// Repo summary
m_summaryLabel->setText(m_model->GetSummary(modelIndex));
@@ -41,7 +41,7 @@ namespace O3DE::ProjectManager
item->setData(gemRepoInfo.m_lastUpdated, RoleLastUpdated);
item->setData(gemRepoInfo.m_path, RolePath);
item->setData(gemRepoInfo.m_additionalInfo, RoleAdditionalInfo);
item->setData(gemRepoInfo.m_includedGemPaths, RoleIncludedGems);
item->setData(gemRepoInfo.m_includedGemUris, RoleIncludedGems);
appendRow(item);
@@ -98,7 +98,7 @@ namespace O3DE::ProjectManager
return modelIndex.data(RolePath).toString();
}
QStringList GemRepoModel::GetIncludedGemPaths(const QModelIndex& modelIndex)
QStringList GemRepoModel::GetIncludedGemUris(const QModelIndex& modelIndex)
{
return modelIndex.data(RoleIncludedGems).toStringList();
}
@@ -118,23 +118,19 @@ namespace O3DE::ProjectManager
QVector<GemInfo> GemRepoModel::GetIncludedGemInfos(const QModelIndex& modelIndex)
{
QVector<GemInfo> allGemInfos;
QStringList repoGemPaths = GetIncludedGemPaths(modelIndex);
QString repoUri = GetRepoUri(modelIndex);
for (const QString& gemPath : repoGemPaths)
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& gemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForRepo(repoUri);
if (gemInfosResult.IsSuccess())
{
AZ::Outcome<GemInfo> gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(gemPath);
if (gemInfoResult.IsSuccess())
{
allGemInfos.append(gemInfoResult.GetValue());
}
else
{
QMessageBox::critical(nullptr, tr("Gem Not Found"), tr("Cannot find info for gem %1.").arg(gemPath));
}
return gemInfosResult.GetValue();
}
else
{
QMessageBox::critical(nullptr, tr("Gems not found"), tr("Cannot find info for gems from repo %1").arg(GetName(modelIndex)));
}
return allGemInfos;
return QVector<GemInfo>();
}
bool GemRepoModel::IsEnabled(const QModelIndex& modelIndex)
@@ -39,7 +39,7 @@ namespace O3DE::ProjectManager
static QDateTime GetLastUpdated(const QModelIndex& modelIndex);
static QString GetPath(const QModelIndex& modelIndex);
static QStringList GetIncludedGemPaths(const QModelIndex& modelIndex);
static QStringList GetIncludedGemUris(const QModelIndex& modelIndex);
static QVector<Tag> GetIncludedGemTags(const QModelIndex& modelIndex);
static QVector<GemInfo> GetIncludedGemInfos(const QModelIndex& modelIndex);
@@ -92,8 +92,9 @@ namespace O3DE::ProjectManager
return;
}
bool addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUri);
if (addGemRepoResult)
AZ::Outcome < void,
AZStd::pair<AZStd::string, AZStd::string>> addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUri);
if (addGemRepoResult.IsSuccess())
{
Reinit();
emit OnRefresh();
@@ -101,8 +102,21 @@ namespace O3DE::ProjectManager
else
{
QString failureMessage = tr("Failed to add gem repo: %1.").arg(repoUri);
QMessageBox::critical(this, tr("Operation failed"), failureMessage);
AZ_Error("Project Manger", false, failureMessage.toUtf8());
if (!addGemRepoResult.GetError().second.empty())
{
QMessageBox addRepoError;
addRepoError.setIcon(QMessageBox::Critical);
addRepoError.setWindowTitle(failureMessage);
addRepoError.setText(addGemRepoResult.GetError().first.c_str());
addRepoError.setDetailedText(addGemRepoResult.GetError().second.c_str());
addRepoError.exec();
}
else
{
QMessageBox::critical(this, failureMessage, addGemRepoResult.GetError().first.c_str());
}
AZ_Error("Project Manager", false, failureMessage.toUtf8());
}
}
}
@@ -9,12 +9,14 @@
#include <ProjectBuilderController.h>
#include <ProjectBuilderWorker.h>
#include <ProjectButtonWidget.h>
#include <ProjectManagerSettings.h>
#include <AzCore/Settings/SettingsRegistry.h>
#include <QMessageBox>
#include <QDesktopServices>
#include <QUrl>
namespace O3DE::ProjectManager
{
ProjectBuilderController::ProjectBuilderController(const ProjectInfo& projectInfo, ProjectButton* projectButton, QWidget* parent)
@@ -27,6 +29,15 @@ namespace O3DE::ProjectManager
m_worker = new ProjectBuilderWorker(m_projectInfo);
m_worker->moveToThread(&m_workerThread);
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
// Remove key here in case Project Manager crashing while building that causes HandleResults to not be called
QString settingsKey = GetProjectBuiltSuccessfullyKey(m_projectInfo.m_projectName);
settingsRegistry->Remove(settingsKey.toStdString().c_str());
SaveProjectManagerSettings();
}
connect(&m_workerThread, &QThread::finished, m_worker, &ProjectBuilderWorker::deleteLater);
connect(&m_workerThread, &QThread::started, m_worker, &ProjectBuilderWorker::BuildProject);
connect(m_worker, &ProjectBuilderWorker::Done, this, &ProjectBuilderController::HandleResults);
@@ -80,6 +91,8 @@ namespace O3DE::ProjectManager
void ProjectBuilderController::HandleResults(const QString& result)
{
QString settingsKey = GetProjectBuiltSuccessfullyKey(m_projectInfo.m_projectName);
if (!result.isEmpty())
{
if (result.contains(tr("log")))
@@ -109,12 +122,26 @@ namespace O3DE::ProjectManager
emit NotifyBuildProject(m_projectInfo);
}
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
settingsRegistry->Remove(settingsKey.toStdString().c_str());
SaveProjectManagerSettings();
}
emit Done(false);
return;
}
else
{
m_projectInfo.m_buildFailed = false;
auto settingsRegistry = AZ::SettingsRegistry::Get();
if (settingsRegistry)
{
settingsRegistry->Set(settingsKey.toStdString().c_str(), true);
SaveProjectManagerSettings();
}
}
emit Done(true);
@@ -203,6 +203,7 @@ namespace O3DE::ProjectManager
QMenu* menu = new QMenu(this);
menu->addAction(tr("Edit Project Settings..."), this, [this]() { emit EditProject(m_projectInfo.m_path); });
menu->addAction(tr("Configure Gems..."), this, [this]() { emit EditProjectGems(m_projectInfo.m_path); });
menu->addAction(tr("Build"), this, [this]() { emit BuildProject(m_projectInfo); });
menu->addAction(tr("Open CMake GUI..."), this, [this]() { emit OpenCMakeGUI(m_projectInfo); });
menu->addSeparator();

Some files were not shown because too many files have changed in this diff Show More