// Copyright 2017 ~ 2022 Critical Failure Studio Ltd. All rights reserved. #include "PaperZDToolViewport.h" #include "Editors/Util/PaperZDEditorStyle.h" #include "Framework/Application/SlateApplication.h" #include "Math/ColorList.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SOverlay.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Text/STextBlock.h" const int32 DefaultZoomLevel = 7; const int32 NumZoomLevels = 17; struct FZoomLevelEntry { FZoomLevelEntry(float InZoomAmount, const FText& InDisplayText) : DisplayText(FText::Format(NSLOCTEXT("PaperZD", "Zoom", "Zoom {0}"), InDisplayText)) , ZoomAmount(InZoomAmount) { } FText DisplayText; float ZoomAmount; }; static const FZoomLevelEntry& GetZoomLevel(int Index) { static const FZoomLevelEntry ZoomLevels[NumZoomLevels] = { FZoomLevelEntry(0.03125f, NSLOCTEXT("PaperZD", "ZoomLevel_1_32", "1:32")), FZoomLevelEntry(0.0625f, NSLOCTEXT("PaperZD", "ZoomLevel_1_16", "1:16")), FZoomLevelEntry(0.125f, NSLOCTEXT("PaperZD", "ZoomLevel_1_8", "1:8")), FZoomLevelEntry(0.250f, NSLOCTEXT("PaperZD", "ZoomLevel_1_4", "1:4")), FZoomLevelEntry(0.500f, NSLOCTEXT("PaperZD", "ZoomLevel_1_2", "1:2")), FZoomLevelEntry(0.750f, NSLOCTEXT("PaperZD", "ZoomLevel_3_4", "3:4")), FZoomLevelEntry(0.875f, NSLOCTEXT("PaperZD", "ZoomLevel_7_8", "7:8")), FZoomLevelEntry(1.000f, NSLOCTEXT("PaperZD", "ZoomLevel_1_1", "1:1")), FZoomLevelEntry(2.000f, NSLOCTEXT("PaperZD", "ZoomLevel_2x", "2x")), FZoomLevelEntry(3.000f, NSLOCTEXT("PaperZD", "ZoomLevel_3x", "3x")), FZoomLevelEntry(4.500f, NSLOCTEXT("PaperZD", "ZoomLevel_4x", "4x")), FZoomLevelEntry(5.000f, NSLOCTEXT("PaperZD", "ZoomLevel_5x", "5x")), FZoomLevelEntry(6.000f, NSLOCTEXT("PaperZD", "ZoomLevel_6x", "6x")), FZoomLevelEntry(7.000f, NSLOCTEXT("PaperZD", "ZoomLevel_7x", "7x")), FZoomLevelEntry(8.000f, NSLOCTEXT("PaperZD", "ZoomLevel_8x", "8x")), FZoomLevelEntry(16.000f, NSLOCTEXT("PaperZD", "ZoomLevel_16x", "16x")), FZoomLevelEntry(32.000f, NSLOCTEXT("PaperZD", "ZoomLevel_32x", "32x")) }; return ZoomLevels[Index]; } ////////////////////////////////////////////////////////////////////////// // SPaperZDToolViewport SPaperZDToolViewport::~SPaperZDToolViewport() { ViewportClient.Reset(); } TSharedRef SPaperZDToolViewport::MakeEditorViewportClient() { return ViewportClient.ToSharedRef(); } void SPaperZDToolViewport::Construct(const FArguments& InArgs, TSharedRef InViewportClient) { OnSelectionChanged = InArgs._OnSelectionChanged; ViewportClient = InViewportClient; ViewportClient->SetRealtime(false); SEditorViewport::Construct(SEditorViewport::FArguments()); // Indicator of current zoom level ViewportOverlay->AddSlot() .Padding(5) .VAlign(VAlign_Top) [ SNew(STextBlock) .TextStyle(FPaperZDEditorStyle::Get(), "PaperZDEditor.Tools.ViewportZoomTextStyle") .Text(this, &SPaperZDToolViewport::GetZoomText) .ColorAndOpacity(this, &SPaperZDToolViewport::GetZoomTextColorAndOpacity) ]; // Title for the viewport ViewportOverlay->AddSlot() .VAlign(VAlign_Top) [ SNew(SBorder) .BorderImage(FPaperZDEditorStyle::Get().GetBrush("PaperZDEditor.Tools.ViewportTitleBackground")) .HAlign(HAlign_Fill) .Visibility(EVisibility::HitTestInvisible) [ SNew(SVerticalBox) // Title text/icon + SVerticalBox::Slot() .AutoHeight() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .HAlign(HAlign_Center) .FillWidth(1.f) [ SNew(STextBlock) .TextStyle(FPaperZDEditorStyle::Get(), "PaperZDEditor.Tools.ViewportTitleTextStyle") .Text(this, &SPaperZDToolViewport::GetTitleText) ] ] ] ]; ZoomLevel = DefaultZoomLevel; PreviousZoomLevel = DefaultZoomLevel; ViewOffset = FVector2D::ZeroVector; TotalMouseDelta = 0; bIsPanning = false; ZoomLevelFade = FCurveSequence(0.0f, 0.75f); ZoomLevelFade.Play(this->AsShared()); DeferredPanPosition = FVector2D::ZeroVector; bRequestDeferredPan = false; } void SPaperZDToolViewport::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { // Handle any deferred panning if (bRequestDeferredPan) { bRequestDeferredPan = false; UpdateViewOffset(AllottedGeometry, DeferredPanPosition); } if (!HasMouseCapture()) { bShowSoftwareCursor = false; bIsPanning = false; } ViewportClient->SetZoomPos(ViewOffset, GetZoomAmount()); ViewportClient->bNeedsRedraw = true; bool bSelectionModified = false; if (Marquee.IsValid()) { bSelectionModified = true; OnSelectionChanged.ExecuteIfBound(Marquee, true); } if (bSelectionModified || bIsPanning || FSlateThrottleManager::Get().IsAllowingExpensiveTasks()) { // Setup the selection set for the viewport ViewportClient->SelectionRectangles.Empty(); if (Marquee.IsValid()) { FViewportSelectionRectangle& Rect = *(new (ViewportClient->SelectionRectangles) FViewportSelectionRectangle); Rect.Color = FColorList::Yellow; Rect.Color.A = 0.45f; Rect.TopLeft = Marquee.Rect.GetUpperLeft(); Rect.Dimensions = Marquee.Rect.GetSize(); } // Tick and render the viewport ViewportClient->Tick(InDeltaTime); GEditor->UpdateSingleViewportClient(ViewportClient.Get(), /*bInAllowNonRealtimeViewportToDraw=*/ true, /*bLinkedOrthoMovement=*/ false); } } FReply SPaperZDToolViewport::OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { TotalMouseDelta = 0; if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { // RIGHT BUTTON is for dragging and Context Menu. FReply ReplyState = FReply::Handled(); ReplyState.CaptureMouse(SharedThis(this)); ReplyState.UseHighPrecisionMouseMovement(SharedThis(this)); SoftwareCursorPosition = PanelCoordToGraphCoord(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()) * MyGeometry.Scale); // clear any interpolation when you manually pan //DeferredMovementTargetObject = nullptr; return ReplyState; } else if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { // START MARQUEE SELECTION. const FVector2D GraphMousePos = PanelCoordToGraphCoord(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()) * MyGeometry.Scale); Marquee.Start(GraphMousePos, FPaperZDToolMarqueeOperation::OperationTypeFromMouseEvent(MouseEvent)); // Trigger a selection update now so that single-clicks without a drag still select something OnSelectionChanged.ExecuteIfBound(Marquee, true); ViewportClient->Invalidate(); return FReply::Handled().CaptureMouse(SharedThis(this)); } else { return FReply::Unhandled(); } } FReply SPaperZDToolViewport::OnMouseButtonUp(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { // Did the user move the cursor sufficiently far, or is it in a dead zone? // In Dead zone - implies actions like summoning context menus and general clicking. // Out of Dead Zone - implies dragging actions like moving nodes and marquee selection. const bool bCursorInDeadZone = TotalMouseDelta <= FSlateApplication::Get().GetDragTriggerDistance(); if (MouseEvent.GetEffectingButton() == EKeys::RightMouseButton) { FReply ReplyState = FReply::Handled(); if (HasMouseCapture()) { FSlateRect ThisPanelScreenSpaceRect = MyGeometry.GetLayoutBoundingRect(); const FVector2D ScreenSpaceCursorPos = MyGeometry.LocalToAbsolute(GraphCoordToPanelCoord(SoftwareCursorPosition) / MyGeometry.Scale); FIntPoint BestPositionInViewport( FMath::RoundToInt(FMath::Clamp(ScreenSpaceCursorPos.X, ThisPanelScreenSpaceRect.Left, ThisPanelScreenSpaceRect.Right)), FMath::RoundToInt(FMath::Clamp(ScreenSpaceCursorPos.Y, ThisPanelScreenSpaceRect.Top, ThisPanelScreenSpaceRect.Bottom)) ); if (!bCursorInDeadZone) { ReplyState.SetMousePos(BestPositionInViewport); } } TSharedPtr WidgetToFocus; if (bCursorInDeadZone) { //WidgetToFocus = OnSummonContextMenu(MyGeometry, MouseEvent); } bShowSoftwareCursor = false; bIsPanning = false; return (WidgetToFocus.IsValid()) ? ReplyState.ReleaseMouseCapture().SetUserFocus(WidgetToFocus.ToSharedRef(), EFocusCause::SetDirectly) : ReplyState.ReleaseMouseCapture(); } else if (MouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { if (OnHandleLeftMouseRelease(MyGeometry, MouseEvent)) { } else if (bCursorInDeadZone) { //@TODO: Move to selection manager // if ( NodeUnderMousePtr.IsValid() ) // { // // We clicked on a node! // TSharedRef NodeWidgetUnderMouse = NodeUnderMousePtr.Pin().ToSharedRef(); // // SelectionManager.ClickedOnNode(NodeWidgetUnderMouse->GetObjectBeingDisplayed(), MouseEvent); // // // We're done interacting with this node. // NodeUnderMousePtr.Reset(); // } // else if (HasMouseCapture()) { // We clicked on the panel background //@TODO: There isn't a marquee operation for clear, need to signal to remove existing sets too! //Marquee.End(); //OnSelectionChanged.ExecuteIfBound(Marquee, false); } } else if (Marquee.IsValid()) { //ApplyMarqueeSelection(Marquee, SelectionManager.SelectedNodes, SelectionManager.SelectedNodes); //SelectionManager.OnSelectionChanged.ExecuteIfBound(SelectionManager.SelectedNodes); OnSelectionChanged.ExecuteIfBound(Marquee, true); } // The existing marquee operation ended; reset it. Marquee = FPaperZDToolMarqueeOperation(); if (bIsPanning) { // We have released the left mouse button. But we're still panning // (i.e. RIGHT MOUSE is down), so we want to hold on to capturing mouse input. return FReply::Handled(); } else { // We aren't panning, so we can release the mouse capture. return FReply::Handled().ReleaseMouseCapture(); } } return FReply::Unhandled(); } FReply SPaperZDToolViewport::OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { const bool bIsRightMouseButtonDown = MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton); const bool bIsLeftMouseButtonDown = MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton); if (HasMouseCapture()) { // Track how much the mouse moved since the mouse down. const FVector2D CursorDelta = MouseEvent.GetCursorDelta(); TotalMouseDelta += CursorDelta.Size(); if (bIsRightMouseButtonDown) { FReply ReplyState = FReply::Handled(); if (!CursorDelta.IsZero()) { bShowSoftwareCursor = true; } bIsPanning = true; ViewOffset -= CursorDelta / GetZoomAmount(); return ReplyState; } else if (bIsLeftMouseButtonDown) { // TSharedPtr NodeBeingDragged = NodeUnderMousePtr.Pin(); // Update the amount to pan panel UpdateViewOffset(MyGeometry, MouseEvent.GetScreenSpacePosition()); const bool bCursorInDeadZone = TotalMouseDelta <= FSlateApplication::Get().GetDragTriggerDistance(); { // We are marquee selecting const FVector2D GraphMousePos = PanelCoordToGraphCoord(MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()) * MyGeometry.Scale); Marquee.Rect.UpdateEndPoint(GraphMousePos); return FReply::Handled(); } } } return FReply::Unhandled(); } FReply SPaperZDToolViewport::OnMouseWheel(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { // We want to zoom into this point; i.e. keep it the same fraction offset into the panel const FVector2D WidgetSpaceCursorPos = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()) * MyGeometry.Scale; FVector2D PointToMaintainGraphSpace = PanelCoordToGraphCoord(WidgetSpaceCursorPos); const int32 ZoomLevelDelta = FMath::FloorToInt(MouseEvent.GetWheelDelta()); const bool bAllowFullZoomRange = true; const float OldZoomLevel = ZoomLevel; if (bAllowFullZoomRange) { ZoomLevel = FMath::Clamp(ZoomLevel + ZoomLevelDelta, 0, NumZoomLevels - 1); } else { // Without control, we do not allow zooming out past 1:1. ZoomLevel = FMath::Clamp(ZoomLevel + ZoomLevelDelta, 0, DefaultZoomLevel); } ZoomLevelFade.Play(this->AsShared()); // Re-center the screen so that it feels like zooming around the cursor. ViewOffset = PointToMaintainGraphSpace - WidgetSpaceCursorPos / GetZoomAmount(); return FReply::Handled(); } FCursorReply SPaperZDToolViewport::OnCursorQuery(const FGeometry& MyGeometry, const FPointerEvent& CursorEvent) const { return bShowSoftwareCursor ? FCursorReply::Cursor(EMouseCursor::None) : FCursorReply::Cursor(EMouseCursor::Default); } int32 SPaperZDToolViewport::FindNearestZoomLevel(float InZoomAmount, bool bRoundDown) const { bool bFoundItem = false; int32 Result = 0; for (int32 ZoomLevelIndex = 0; ZoomLevelIndex < NumZoomLevels; ++ZoomLevelIndex) { if (InZoomAmount <= GetZoomLevel(ZoomLevelIndex).ZoomAmount) { Result = ZoomLevelIndex; bFoundItem = true; break; } } if (!bFoundItem) { return DefaultZoomLevel; } else { if (bFoundItem && bRoundDown && (Result > 0)) { return Result - 1; } else { return Result; } } } float SPaperZDToolViewport::GetZoomAmount() const { return GetZoomLevel(ZoomLevel).ZoomAmount; } FText SPaperZDToolViewport::GetZoomText() const { return GetZoomLevel(ZoomLevel).DisplayText; } FSlateColor SPaperZDToolViewport::GetZoomTextColorAndOpacity() const { return FLinearColor(1, 1, 1, 1.25f - ZoomLevelFade.GetLerp() * 0.75f); } FVector2D SPaperZDToolViewport::GetViewOffset() const { return ViewOffset; } FVector2D SPaperZDToolViewport::ComputeEdgePanAmount(const FGeometry& MyGeometry, const FVector2D& TargetPosition) { // How quickly to ramp up the pan speed as the user moves the mouse further past the edge of the graph panel. static const float EdgePanSpeedCoefficient = 0.1f; // Never pan slower than this, it's just unpleasant. static const float MinPanSpeed = 5.0f; // Start panning before we reach the edge of the graph panel. static const float EdgePanForgivenessZone = 30.0f; const FVector2D LocalCursorPos = MyGeometry.AbsoluteToLocal(TargetPosition); // If the mouse is outside of the graph area, then we want to pan in that direction. // The farther out the mouse is, the more we want to pan. FVector2D EdgePanThisTick(0, 0); if (LocalCursorPos.X <= EdgePanForgivenessZone) { EdgePanThisTick.X += FMath::Min(-MinPanSpeed, EdgePanSpeedCoefficient * (EdgePanForgivenessZone - LocalCursorPos.X)); } else if (LocalCursorPos.X >= MyGeometry.Size.X - EdgePanForgivenessZone) { EdgePanThisTick.X = FMath::Max(MinPanSpeed, EdgePanSpeedCoefficient * (MyGeometry.Size.X - EdgePanForgivenessZone - LocalCursorPos.X)); } if (LocalCursorPos.Y <= EdgePanForgivenessZone) { EdgePanThisTick.Y += FMath::Min(-MinPanSpeed, EdgePanSpeedCoefficient * (EdgePanForgivenessZone - LocalCursorPos.Y)); } else if (LocalCursorPos.Y >= MyGeometry.Size.Y - EdgePanForgivenessZone) { EdgePanThisTick.Y = FMath::Max(MinPanSpeed, EdgePanSpeedCoefficient * (MyGeometry.Size.Y - EdgePanForgivenessZone - LocalCursorPos.Y)); } return EdgePanThisTick; } void SPaperZDToolViewport::UpdateViewOffset(const FGeometry& MyGeometry, const FVector2D& TargetPosition) { const FVector2D PanAmount = ComputeEdgePanAmount(MyGeometry, TargetPosition) / GetZoomAmount(); ViewOffset += PanAmount; } void SPaperZDToolViewport::RequestDeferredPan(const FVector2D& UpdatePosition) { bRequestDeferredPan = true; DeferredPanPosition = UpdatePosition; } FVector2D SPaperZDToolViewport::GraphCoordToPanelCoord(const FVector2D& GraphSpaceCoordinate) const { return (GraphSpaceCoordinate - GetViewOffset()) * GetZoomAmount(); } FVector2D SPaperZDToolViewport::PanelCoordToGraphCoord(const FVector2D& PanelSpaceCoordinate) const { return PanelSpaceCoordinate / GetZoomAmount() + GetViewOffset(); } FSlateRect SPaperZDToolViewport::PanelRectToGraphRect(const FSlateRect& PanelSpaceRect) const { FVector2D UpperLeft = PanelCoordToGraphCoord(FVector2D(PanelSpaceRect.Left, PanelSpaceRect.Top)); FVector2D LowerRight = PanelCoordToGraphCoord(FVector2D(PanelSpaceRect.Right, PanelSpaceRect.Bottom)); return FSlateRect( UpperLeft.X, UpperLeft.Y, LowerRight.X, LowerRight.Y); } void SPaperZDToolViewport::PaintSoftwareCursor(const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 DrawLayerId) const { if (bShowSoftwareCursor) { const FSlateBrush* Brush = FAppStyle::GetBrush(TEXT("SoftwareCursor_Grab")); const FVector2D CursorSize = Brush->ImageSize / AllottedGeometry.Scale; FSlateDrawElement::MakeBox( OutDrawElements, DrawLayerId, AllottedGeometry.ToPaintGeometry(CursorSize, FSlateLayoutTransform(GraphCoordToPanelCoord(SoftwareCursorPosition) / AllottedGeometry.Scale - (CursorSize * .5f))), Brush); } } int32 SPaperZDToolViewport::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { int32 MaxLayerId = SCompoundWidget::OnPaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); ++MaxLayerId; PaintSoftwareCursor(AllottedGeometry, MyCullingRect, OutDrawElements, MaxLayerId); return MaxLayerId; } FText SPaperZDToolViewport::GetTitleText() const { return NSLOCTEXT("PaperZD", "TileSetPaletteTitle", "palette"); }