1534 lines
56 KiB
C++
1534 lines
56 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Framework/Application/SlateUser.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Framework/Application/NavigationConfig.h"
|
|
#include "Misc/App.h"
|
|
#include "Widgets/SWindow.h"
|
|
#include "Widgets/SWeakWidget.h"
|
|
#include "GenericPlatform/GenericPlatformInputDeviceMapper.h"
|
|
|
|
#if PLATFORM_MICROSOFT
|
|
// Needed to be able to use RECT
|
|
#include "Microsoft/WindowsHWrapper.h"
|
|
#endif
|
|
|
|
DECLARE_CYCLE_STAT(TEXT("QueryCursor"), STAT_SlateQueryCursor, STATGROUP_Slate);
|
|
DECLARE_CYCLE_STAT(TEXT("Update Tooltip Time"), STAT_SlateUpdateTooltip, STATGROUP_Slate);
|
|
|
|
namespace SlateDefs
|
|
{
|
|
// How far tooltips should be offset from the mouse cursor position, in pixels
|
|
static const FVector2f TooltipOffsetFromMouse(12.0f, 8.0f);
|
|
|
|
// How far tooltips should be pushed out from a force field border, in pixels
|
|
static const FVector2f TooltipOffsetFromForceField(4.0f, 3.0f);
|
|
}
|
|
|
|
static bool bEnableSyntheticCursorMoves = true;
|
|
FAutoConsoleVariableRef CVarEnableSyntheticCursorMoves(
|
|
TEXT("Slate.EnableSyntheticCursorMoves"),
|
|
bEnableSyntheticCursorMoves,
|
|
TEXT("")
|
|
);
|
|
|
|
static bool bEnableCursorQueries = true;
|
|
FAutoConsoleVariableRef CVarEnableCursorQueries(
|
|
TEXT("Slate.EnableCursorQueries"),
|
|
bEnableCursorQueries,
|
|
TEXT(""));
|
|
|
|
static float SoftwareCursorScale = 1.0f;
|
|
FAutoConsoleVariableRef CVarSoftwareCursorScale(
|
|
TEXT("Slate.SoftwareCursorScale"),
|
|
SoftwareCursorScale,
|
|
TEXT("Scale factor applied to the software cursor. Requires the cursor widget to be scale-aware."));
|
|
|
|
static float TooltipSummonDelay = 0.15f;
|
|
FAutoConsoleVariableRef CVarTooltipSummonDelay(
|
|
TEXT("Slate.TooltipSummonDelay"),
|
|
TooltipSummonDelay,
|
|
TEXT("Delay in seconds before a tooltip is displayed near the mouse cursor when hovering over widgets that supply tooltip data."));
|
|
|
|
static float TooltipIntroDuration = 0.1f;
|
|
FAutoConsoleVariableRef CVarTooltipIntroDuration(
|
|
TEXT("Slate.TooltipIntroDuration"),
|
|
TooltipIntroDuration,
|
|
TEXT("How long it takes for a tooltip to animate into view, in seconds."));
|
|
|
|
static float CursorSignificantMoveDetectionThreshold = 0.0;
|
|
FAutoConsoleVariableRef CVarCursorSignificantMoveDetectionThreshold(
|
|
TEXT("Slate.CursorSignificantMoveDetectionThreshold"),
|
|
CursorSignificantMoveDetectionThreshold,
|
|
TEXT("The distance from previous cursor position above which the move will be considered significant (used to trigger the display of the tooltips)."));
|
|
|
|
static bool bAllowTooltipsWithHiddenCursor = false;
|
|
FAutoConsoleVariableRef CVarAllowTooltipsWithHiddenCursor(
|
|
TEXT("Slate.AllowTooltipsWithHiddenCursor"),
|
|
bAllowTooltipsWithHiddenCursor,
|
|
TEXT(""));
|
|
|
|
static bool bAllowTooltipsWhileMouseDown = false;
|
|
FAutoConsoleVariableRef CVarAllowTooltipsWhileMouseDown(
|
|
TEXT("Slate.AllowTooltipsWhileMouseDown"),
|
|
bAllowTooltipsWhileMouseDown,
|
|
TEXT(""));
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FSlateVirtualUserHandle
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
FSlateVirtualUserHandle::FSlateVirtualUserHandle(int32 InUserIndex, int32 InVirtualUserIndex)
|
|
: UserIndex(InUserIndex)
|
|
, VirtualUserIndex(InVirtualUserIndex)
|
|
{
|
|
}
|
|
|
|
FSlateVirtualUserHandle::~FSlateVirtualUserHandle()
|
|
{
|
|
if (FSlateApplication::IsInitialized())
|
|
{
|
|
FSlateApplication::Get().UnregisterUser(UserIndex);
|
|
}
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FActiveTooltipInfo
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
void FSlateUser::FActiveTooltipInfo::Reset()
|
|
{
|
|
if (SourceWidget.IsValid())
|
|
{
|
|
SourceWidget.Pin()->OnToolTipClosing();
|
|
}
|
|
|
|
if (Tooltip.IsValid())
|
|
{
|
|
Tooltip.Pin()->OnClosed();
|
|
}
|
|
|
|
Tooltip.Reset();
|
|
SourceWidget.Reset();
|
|
TooltipVisualizer.Reset();
|
|
OffsetDirection = ETooltipOffsetDirection::Undetermined;
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FSlateUser
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
TSharedRef<FSlateUser> FSlateUser::Create(int32 InUserIndex, TSharedPtr<ICursor> InCursor)
|
|
{
|
|
return MakeShareable(new FSlateUser(InUserIndex, InCursor));
|
|
}
|
|
|
|
TSharedRef<FSlateUser> FSlateUser::Create(FPlatformUserId InPlatformUserId, TSharedPtr<ICursor> InCursor)
|
|
{
|
|
return MakeShareable(new FSlateUser(InPlatformUserId, InCursor));
|
|
}
|
|
|
|
FSlateUser::FSlateUser(int32 InUserIndex, TSharedPtr<ICursor> InCursor)
|
|
: UserIndex(InUserIndex)
|
|
, Cursor(InCursor)
|
|
{
|
|
PlatformUser = FPlatformMisc::GetPlatformUserForUserIndex(InUserIndex);
|
|
UE_LOG(LogSlate, Log, TEXT("New Slate User Created. Platform User Id %d, User Index %d, Is Virtual User: %d"), PlatformUser.GetInternalId(), UserIndex, IsVirtualUser());
|
|
|
|
PointerPositionsByIndex.Add(FSlateApplication::CursorPointerIndex, FVector2f::ZeroVector);
|
|
PreviousPointerPositionsByIndex.Add(FSlateApplication::CursorPointerIndex, FVector2f::ZeroVector);
|
|
}
|
|
|
|
FSlateUser::FSlateUser(FPlatformUserId InPlatformUser, TSharedPtr<ICursor> InCursor)
|
|
: PlatformUser(InPlatformUser)
|
|
, Cursor(InCursor)
|
|
{
|
|
// TODO: Remove this part, its backwards compatible for now
|
|
UserIndex = InPlatformUser.GetInternalId();
|
|
|
|
UE_LOG(LogSlate, Log, TEXT("New Slate User Created. Platform User Id %d, Old User Index: %d , Is Virtual User: %d"), PlatformUser.GetInternalId(), UserIndex, IsVirtualUser());
|
|
|
|
PointerPositionsByIndex.Add(FSlateApplication::CursorPointerIndex, FVector2f::ZeroVector);
|
|
PreviousPointerPositionsByIndex.Add(FSlateApplication::CursorPointerIndex, FVector2f::ZeroVector);
|
|
}
|
|
|
|
FSlateUser::~FSlateUser()
|
|
{
|
|
UE_LOG(LogSlate, Log, TEXT("Slate User Destroyed. User Index %d, Is Virtual User: %d"), UserIndex, IsVirtualUser());
|
|
}
|
|
|
|
TSharedPtr<SWidget> FSlateUser::GetFocusedWidget() const
|
|
{
|
|
return WeakFocusPath.IsValid() ? WeakFocusPath.GetLastWidget().Pin() : nullptr;
|
|
}
|
|
|
|
TOptional<EFocusCause> FSlateUser::HasFocus(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
return Widget && GetFocusedWidget() == Widget ? FocusCause : TOptional<EFocusCause>();
|
|
}
|
|
|
|
bool FSlateUser::ShouldShowFocus(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
return HasFocus(Widget).IsSet() && bShowFocus;
|
|
}
|
|
|
|
bool FSlateUser::HasFocusedDescendants(TSharedRef<const SWidget> Widget) const
|
|
{
|
|
return WeakFocusPath.IsValid() && WeakFocusPath.GetLastWidget().Pin() != Widget && WeakFocusPath.ContainsWidget(&Widget.Get());
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetInFocusPath(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
return Widget && WeakFocusPath.IsValid() && WeakFocusPath.ContainsWidget(Widget.Get());
|
|
}
|
|
|
|
bool FSlateUser::SetFocus(const TSharedRef<SWidget>& WidgetToFocus, EFocusCause ReasonFocusIsChanging)
|
|
{
|
|
return FSlateApplication::Get().SetUserFocus(UserIndex, WidgetToFocus, ReasonFocusIsChanging);
|
|
}
|
|
|
|
void FSlateUser::ClearFocus(EFocusCause ReasonFocusIsChanging)
|
|
{
|
|
FSlateApplication::Get().ClearUserFocus(UserIndex, ReasonFocusIsChanging);
|
|
}
|
|
|
|
bool FSlateUser::HasAnyCapture() const
|
|
{
|
|
return PointerCaptorPathsByIndex.Num() > 0;
|
|
}
|
|
|
|
bool FSlateUser::HasCursorCapture() const
|
|
{
|
|
return HasCapture(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
bool FSlateUser::HasCapture(uint32 PointerIndex) const
|
|
{
|
|
const FWeakWidgetPath* CaptorPath = PointerCaptorPathsByIndex.Find(PointerIndex);
|
|
return CaptorPath && CaptorPath->IsValid();
|
|
}
|
|
|
|
bool FSlateUser::DoesWidgetHaveAnyCapture(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
for (const auto& IndexPathPair : PointerCaptorPathsByIndex)
|
|
{
|
|
if (IndexPathPair.Value.GetLastWidget().Pin() == Widget)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FSlateUser::DoesWidgetHaveCursorCapture(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
return DoesWidgetHaveCapture(Widget, FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
bool FSlateUser::DoesWidgetHaveCapture(TSharedPtr<const SWidget> Widget, uint32 PointerIndex) const
|
|
{
|
|
const FWeakWidgetPath* CaptorPath = PointerCaptorPathsByIndex.Find(PointerIndex);
|
|
return CaptorPath && CaptorPath->GetLastWidget().Pin() == Widget;
|
|
}
|
|
|
|
bool FSlateUser::SetCursorCaptor(TSharedRef<const SWidget> Widget, const FWidgetPath& EventPath)
|
|
{
|
|
return SetPointerCaptor(FSlateApplication::CursorPointerIndex, Widget, EventPath);
|
|
}
|
|
|
|
bool FSlateUser::SetPointerCaptor(uint32 PointerIndex, TSharedRef<const SWidget> Widget, const FWidgetPath& EventPath)
|
|
{
|
|
// Bail on any current capture we may have for this pointer before trying to establish the new captor
|
|
ReleaseCapture(PointerIndex);
|
|
|
|
if (ensureMsgf(EventPath.IsValid(), TEXT("An unknown widget is attempting to set capture to %s"), *Widget->ToString()))
|
|
{
|
|
FWidgetPath NewCaptorPath = EventPath.GetPathDownTo(Widget);
|
|
if (!NewCaptorPath.IsValid() || NewCaptorPath.GetLastWidget() != Widget)
|
|
{
|
|
// The target widget wasn't in the given event path, so try searching for it from the root of the event path
|
|
NewCaptorPath = EventPath.GetPathDownTo(EventPath.GetWindow());
|
|
NewCaptorPath.ExtendPathTo(FWidgetMatcher(Widget));
|
|
}
|
|
|
|
if (NewCaptorPath.IsValid() && NewCaptorPath.GetLastWidget() == Widget)
|
|
{
|
|
PointerCaptorPathsByIndex.Add(PointerIndex, NewCaptorPath);
|
|
|
|
#if WITH_SLATE_DEBUGGING
|
|
FSlateDebugging::BroadcastMouseCapture(UserIndex, PointerIndex, Widget);
|
|
#endif
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FSlateUser::ReleaseAllCapture()
|
|
{
|
|
TArray<uint32> CapturedPointerIndices;
|
|
PointerCaptorPathsByIndex.GenerateKeyArray(CapturedPointerIndices);
|
|
for (uint32 CapturedPointerIdx : CapturedPointerIndices)
|
|
{
|
|
ReleaseCapture(CapturedPointerIdx);
|
|
}
|
|
}
|
|
|
|
void FSlateUser::ReleaseCursorCapture()
|
|
{
|
|
ReleaseCapture(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
void FSlateUser::ReleaseCapture(uint32 PointerIndex)
|
|
{
|
|
if (const FWeakWidgetPath* CaptorPath = PointerCaptorPathsByIndex.Find(PointerIndex))
|
|
{
|
|
WidgetsUnderPointerLastEventByIndex.Add(PointerIndex, *CaptorPath);
|
|
|
|
if (TSharedPtr<SWidget> CaptorWidget = CaptorPath->GetLastWidget().Pin())
|
|
{
|
|
CaptorWidget->OnMouseCaptureLost(FCaptureLostEvent(UserIndex, PointerIndex));
|
|
#if WITH_SLATE_DEBUGGING
|
|
FSlateDebugging::BroadcastMouseCaptureLost(UserIndex, PointerIndex, CaptorWidget);
|
|
#endif
|
|
}
|
|
|
|
CaptorPath = nullptr;
|
|
PointerCaptorPathsByIndex.Remove(PointerIndex);
|
|
|
|
if (PointerIndex == FSlateApplication::CursorPointerIndex)
|
|
{
|
|
// If cursor capture changes, we should refresh the cursor state.
|
|
RequestCursorQuery();
|
|
}
|
|
}
|
|
}
|
|
|
|
TArray<FWidgetPath> FSlateUser::GetCaptorPaths()
|
|
{
|
|
TArray<FWidgetPath> CaptorPaths;
|
|
|
|
TArray<uint32> CaptureIndices;
|
|
PointerCaptorPathsByIndex.GenerateKeyArray(CaptureIndices);
|
|
for (uint32 PointerIndex : CaptureIndices)
|
|
{
|
|
FWidgetPath CaptorPath = GetCaptorPath(PointerIndex);
|
|
if (CaptorPath.IsValid())
|
|
{
|
|
CaptorPaths.Add(CaptorPath);
|
|
}
|
|
}
|
|
|
|
return CaptorPaths;
|
|
}
|
|
|
|
FWidgetPath FSlateUser::GetCursorCaptorPath(FWeakWidgetPath::EInterruptedPathHandling::Type InterruptedPathHandling, const FPointerEvent* PointerEvent)
|
|
{
|
|
return GetCaptorPath(FSlateApplication::CursorPointerIndex, InterruptedPathHandling, PointerEvent);
|
|
}
|
|
|
|
FWidgetPath FSlateUser::GetCaptorPath(uint32 PointerIndex, FWeakWidgetPath::EInterruptedPathHandling::Type InterruptedPathHandling, const FPointerEvent* PointerEvent)
|
|
{
|
|
FWidgetPath CaptorPath;
|
|
if (const FWeakWidgetPath* WeakCaptorPath = PointerCaptorPathsByIndex.Find(PointerIndex))
|
|
{
|
|
if (WeakCaptorPath->ToWidgetPath(CaptorPath, InterruptedPathHandling, PointerEvent) == FWeakWidgetPath::EPathResolutionResult::Truncated)
|
|
{
|
|
// The path was truncated, meaning it's not actually valid anymore, so we want to clear our entry for it out immediately
|
|
WeakCaptorPath = nullptr;
|
|
ReleaseCapture(PointerIndex);
|
|
}
|
|
}
|
|
return CaptorPath;
|
|
}
|
|
|
|
FWeakWidgetPath FSlateUser::GetWeakCursorCapturePath() const
|
|
{
|
|
return GetWeakCapturePath(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
FWeakWidgetPath FSlateUser::GetWeakCapturePath(uint32 PointerIndex) const
|
|
{
|
|
const FWeakWidgetPath* WeakCaptorPath = PointerCaptorPathsByIndex.Find(PointerIndex);
|
|
return WeakCaptorPath ? *WeakCaptorPath : FWeakWidgetPath();
|
|
}
|
|
|
|
TArray<TSharedRef<SWidget>> FSlateUser::GetCaptorWidgets() const
|
|
{
|
|
TArray<TSharedRef<SWidget>> AllCaptors;
|
|
AllCaptors.Reserve(PointerCaptorPathsByIndex.Num());
|
|
for (const auto& IndexPathPair : PointerCaptorPathsByIndex)
|
|
{
|
|
if (TSharedPtr<SWidget> CaptorWidget = IndexPathPair.Value.GetLastWidget().Pin())
|
|
{
|
|
AllCaptors.Add(CaptorWidget.ToSharedRef());
|
|
}
|
|
}
|
|
return AllCaptors;
|
|
}
|
|
|
|
TSharedPtr<SWidget> FSlateUser::GetCursorCaptor() const
|
|
{
|
|
return GetPointerCaptor(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
TSharedPtr<SWidget> FSlateUser::GetPointerCaptor(uint32 PointerIndex) const
|
|
{
|
|
const FWeakWidgetPath* WeakCaptorPath = PointerCaptorPathsByIndex.Find(PointerIndex);
|
|
return WeakCaptorPath ? WeakCaptorPath->GetLastWidget().Pin() : nullptr;
|
|
}
|
|
|
|
void FSlateUser::SetCursorVisibility(bool bDrawCursor)
|
|
{
|
|
bCanDrawCursor = bDrawCursor;
|
|
|
|
if (bCanDrawCursor)
|
|
{
|
|
RequestCursorQuery();
|
|
}
|
|
else
|
|
{
|
|
ProcessCursorReply(FCursorReply::Cursor(EMouseCursor::None));
|
|
}
|
|
}
|
|
|
|
bool FSlateUser::IsCursorVisible() const
|
|
{
|
|
// Consider the cursor valid so long as we can draw it and it is not the "None" type
|
|
return bCanDrawCursor && Cursor.IsValid() && Cursor->GetType() != EMouseCursor::None;
|
|
}
|
|
|
|
void FSlateUser::SetCursorPosition(int32 PosX, int32 PosY)
|
|
{
|
|
SetPointerPosition(FSlateApplication::CursorPointerIndex, PosX, PosY);
|
|
}
|
|
|
|
void FSlateUser::SetCursorPosition(const UE::Slate::FDeprecateVector2DParameter& NewCursorPos)
|
|
{
|
|
SetCursorPosition((int32)NewCursorPos.X, (int32)NewCursorPos.Y);
|
|
}
|
|
|
|
void FSlateUser::SetPointerPosition(uint32 PointerIndex, int32 PosX, int32 PosY)
|
|
{
|
|
if (Cursor && PointerIndex == FSlateApplication::CursorPointerIndex)
|
|
{
|
|
UE_LOG(LogSlate, Verbose, TEXT("SlateUser [%d] moving cursor @ (%d, %d)"), UserIndex, PosX, PosY );
|
|
|
|
Cursor->SetPosition(PosX, PosY);
|
|
}
|
|
|
|
UpdatePointerPosition(PointerIndex, FVector2f(PosX, PosY));
|
|
}
|
|
|
|
void FSlateUser::SetPointerPosition(uint32 PointerIndex, const UE::Slate::FDeprecateVector2DParameter& NewPointerPos)
|
|
{
|
|
SetPointerPosition(PointerIndex, (int32)NewPointerPos.X, (int32)NewPointerPos.Y);
|
|
}
|
|
|
|
UE::Slate::FDeprecateVector2DResult FSlateUser::GetCursorPosition() const
|
|
{
|
|
return GetPointerPosition(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
UE::Slate::FDeprecateVector2DResult FSlateUser::GetPreviousCursorPosition() const
|
|
{
|
|
return GetPreviousPointerPosition(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
UE::Slate::FDeprecateVector2DResult FSlateUser::GetPointerPosition(uint32 PointerIndex) const
|
|
{
|
|
if (Cursor && PointerIndex == FSlateApplication::CursorPointerIndex)
|
|
{
|
|
return UE::Slate::CastToVector2f(Cursor->GetPosition());
|
|
}
|
|
const FVector2f* FoundPosition = PointerPositionsByIndex.Find(PointerIndex);
|
|
return FoundPosition ? *FoundPosition : FVector2f::ZeroVector;
|
|
}
|
|
|
|
UE::Slate::FDeprecateVector2DResult FSlateUser::GetPreviousPointerPosition(uint32 PointerIndex) const
|
|
{
|
|
const FVector2f* FoundPosition = PreviousPointerPositionsByIndex.Find(PointerIndex);
|
|
return FoundPosition ? *FoundPosition : GetPointerPosition(PointerIndex);
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetUnderCursor(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
return IsWidgetUnderPointer(Widget, FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetUnderPointer(TSharedPtr<const SWidget> Widget, uint32 PointerIndex) const
|
|
{
|
|
const FWeakWidgetPath* WidgetsUnderPointer = WidgetsUnderPointerLastEventByIndex.Find(PointerIndex);
|
|
return Widget && WidgetsUnderPointer && WidgetsUnderPointer->ContainsWidget(Widget.Get());
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetUnderAnyPointer(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
if (Widget)
|
|
{
|
|
for (const auto& IndexPathPair : WidgetsUnderPointerLastEventByIndex)
|
|
{
|
|
if (IndexPathPair.Value.ContainsWidget(Widget.Get()))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetDirectlyUnderCursor(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
return IsWidgetDirectlyUnderPointer(Widget, FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetDirectlyUnderPointer(TSharedPtr<const SWidget> Widget, uint32 PointerIndex) const
|
|
{
|
|
const FWeakWidgetPath* WidgetsUnderPointer = WidgetsUnderPointerLastEventByIndex.Find(PointerIndex);
|
|
return WidgetsUnderPointer && WidgetsUnderPointer->IsValid() && WidgetsUnderPointer->GetLastWidget().Pin() == Widget;
|
|
}
|
|
|
|
bool FSlateUser::IsWidgetDirectlyUnderAnyPointer(TSharedPtr<const SWidget> Widget) const
|
|
{
|
|
for (const auto& IndexPathPair : WidgetsUnderPointerLastEventByIndex)
|
|
{
|
|
if (IndexPathPair.Value.IsValid() && IndexPathPair.Value.GetLastWidget().Pin() == Widget)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FWeakWidgetPath FSlateUser::GetLastWidgetsUnderCursor() const
|
|
{
|
|
return GetLastWidgetsUnderPointer(FSlateApplication::CursorPointerIndex);
|
|
}
|
|
|
|
FWeakWidgetPath FSlateUser::GetLastWidgetsUnderPointer(uint32 PointerIndex) const
|
|
{
|
|
return WidgetsUnderPointerLastEventByIndex.FindRef(PointerIndex);
|
|
}
|
|
|
|
bool FSlateUser::IsDragDropping() const
|
|
{
|
|
return DragDropContent.IsValid();
|
|
}
|
|
|
|
bool FSlateUser::IsDragDroppingAffected(const FPointerEvent& InPointerEvent) const
|
|
{
|
|
return DragDropContent.IsValid() && DragDropContent->AffectedByPointerEvent(InPointerEvent);
|
|
}
|
|
|
|
void FSlateUser::CancelDragDrop()
|
|
{
|
|
if (DragDropContent.IsValid())
|
|
{
|
|
const FPointerEvent EmptyPointerEvent;
|
|
const FDragDropEvent DragDropEvent(EmptyPointerEvent, DragDropContent);
|
|
|
|
for (const auto& IndexPathPair : WidgetsUnderPointerLastEventByIndex)
|
|
{
|
|
FWidgetPath WidgetsToDragLeave = IndexPathPair.Value.ToWidgetPath();
|
|
if (WidgetsToDragLeave.IsValid())
|
|
{
|
|
for (int32 WidgetIndex = WidgetsToDragLeave.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex)
|
|
{
|
|
WidgetsToDragLeave.Widgets[WidgetIndex].Widget->OnDragLeave(DragDropEvent);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cancel dragdrop operation correctly firing off callbacks
|
|
DragDropContent->OnDrop(false, EmptyPointerEvent);
|
|
|
|
// We always reset the cache of widgets under our pointers whenever we enter/exit drag-drop mode
|
|
WidgetsUnderPointerLastEventByIndex.Reset();
|
|
|
|
DragDropContent.Reset();
|
|
}
|
|
}
|
|
|
|
void FSlateUser::ShowTooltip(const TSharedRef<IToolTip>& InTooltip, const UE::Slate::FDeprecateVector2DParameter& InLocation)
|
|
{
|
|
CloseTooltip();
|
|
|
|
ActiveTooltipInfo.Tooltip = InTooltip;
|
|
|
|
// Establish the tooltip content in the window
|
|
TSharedRef<SWindow> TooltipWindow = GetOrCreateTooltipWindow();
|
|
TooltipWindow->SetContent(
|
|
SNew(SWeakWidget)
|
|
.PossiblyNullContent(InTooltip->AsWidget()));
|
|
|
|
// Make sure the desired size is valid
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
TooltipWindow->SlatePrepass(SlateApp.GetApplicationScale() * TooltipWindow->GetNativeWindow()->GetDPIScaleFactor());
|
|
|
|
// Place the window as close to the given location as possible (MoveWindowTo will adjust the window's position to stay onscreen, if needed)
|
|
const FSlateRect Anchor(InLocation.X, InLocation.Y, InLocation.X, InLocation.Y);
|
|
ActiveTooltipInfo.DesiredLocation = SlateApp.CalculateTooltipWindowPosition(Anchor, TooltipWindow->GetDesiredSizeDesktopPixels(), /*bAutoAdjustForDPIScale =*/false, (InTooltip->IsInteractive()) ? EPopupCursorOverlapMode::AllowOverlap : EPopupCursorOverlapMode::PreventOverlap);
|
|
TooltipWindow->MoveWindowTo(ActiveTooltipInfo.DesiredLocation);
|
|
|
|
TooltipWindow->SetOpacity(0.0f);
|
|
TooltipWindow->ShowWindow();
|
|
ActiveTooltipInfo.SummonTime = FPlatformTime::Seconds();
|
|
}
|
|
|
|
void FSlateUser::CloseTooltip()
|
|
{
|
|
ActiveTooltipInfo.Reset();
|
|
|
|
// Hide the tooltip window as well (don't destroy it - we'll reuse it)
|
|
TSharedPtr<SWindow> TooltipWindow = TooltipWindowPtr.Pin();
|
|
if (TooltipWindow && TooltipWindow->IsVisible())
|
|
{
|
|
TooltipWindow->HideWindow();
|
|
}
|
|
}
|
|
|
|
FVector2f FSlateUser::GetTooltipPosition() const
|
|
{
|
|
if (TooltipWindowPtr.IsValid())
|
|
{
|
|
return TooltipWindowPtr.Pin()->GetPositionInScreen();
|
|
}
|
|
return FVector2f::Zero();
|
|
}
|
|
|
|
void FSlateUser::SetUserNavigationConfig(TSharedPtr<FNavigationConfig> InNavigationConfig)
|
|
{
|
|
if (UserNavigationConfig)
|
|
{
|
|
UserNavigationConfig->OnUnregister();
|
|
}
|
|
|
|
UserNavigationConfig = InNavigationConfig;
|
|
|
|
if (InNavigationConfig)
|
|
{
|
|
InNavigationConfig->OnRegister();
|
|
}
|
|
|
|
#if WITH_SLATE_DEBUGGING
|
|
FSlateApplication::Get().TryDumpNavigationConfig(UserNavigationConfig);
|
|
#endif // WITH_SLATE_DEBUGGING
|
|
}
|
|
|
|
bool FSlateUser::IsTouchPointerActive(int32 TouchPointerIndex) const
|
|
{
|
|
return TouchPointerIndex < (int32)ETouchIndex::CursorPointerIndex && PointerPositionsByIndex.Contains(TouchPointerIndex);
|
|
}
|
|
|
|
void FSlateUser::DrawWindowlessDragDropContent(const TSharedRef<SWindow>& WindowToDraw, FSlateWindowElementList& WindowElementList, int32& MaxLayerId)
|
|
{
|
|
if (DragDropContent && DragDropContent->IsWindowlessOperation())
|
|
{
|
|
TSharedPtr<SWindow> DragDropWindow = DragDropWindowPtr.Pin();
|
|
if (DragDropWindow && DragDropWindow == WindowToDraw)
|
|
{
|
|
TSharedPtr<SWidget> DecoratorWidget = DragDropContent->GetDefaultDecorator();
|
|
if (DecoratorWidget && DecoratorWidget->GetVisibility().IsVisible())
|
|
{
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
const float WindowRootScale = SlateApp.GetApplicationScale() * DragDropWindow->GetNativeWindow()->GetDPIScaleFactor();
|
|
|
|
DecoratorWidget->SetVisibility(EVisibility::HitTestInvisible);
|
|
DecoratorWidget->SlatePrepass(WindowRootScale);
|
|
|
|
FVector2f DragDropContentInWindowSpace = WindowToDraw->GetWindowGeometryInScreen().AbsoluteToLocal(DragDropContent->GetDecoratorPosition()) * WindowRootScale;
|
|
const FGeometry DragDropContentGeometry = FGeometry::MakeRoot(DecoratorWidget->GetDesiredSize(), FSlateLayoutTransform(DragDropContentInWindowSpace));
|
|
|
|
DecoratorWidget->Paint(
|
|
FPaintArgs(&WindowToDraw.Get(), WindowToDraw->GetHittestGrid(), WindowToDraw->GetPositionInScreen(), SlateApp.GetCurrentTime(), SlateApp.GetDeltaTime()),
|
|
DragDropContentGeometry, WindowToDraw->GetClippingRectangleInWindow(),
|
|
WindowElementList,
|
|
++MaxLayerId,
|
|
FWidgetStyle(),
|
|
WindowToDraw->IsEnabled());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSlateUser::DrawCursor(const TSharedRef<SWindow>& WindowToDraw, FSlateWindowElementList& WindowElementList, int32& MaxLayerId)
|
|
{
|
|
TSharedPtr<SWindow> CursorWindow = CursorWindowPtr.Pin();
|
|
if (bCanDrawCursor && CursorWindow && WindowToDraw == CursorWindow)
|
|
{
|
|
if (TSharedPtr<SWidget> CursorWidget = CursorWidgetPtr.Pin())
|
|
{
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
const float WindowRootScale = FSlateApplication::Get().GetApplicationScale() * CursorWindow->GetNativeWindow()->GetDPIScaleFactor();
|
|
|
|
CursorWidget->SetVisibility(EVisibility::HitTestInvisible);
|
|
CursorWidget->SlatePrepass(WindowRootScale);
|
|
|
|
FVector2f CursorInScreen = GetCursorPosition();
|
|
FVector2f CursorPosInWindowSpace = WindowToDraw->GetWindowGeometryInScreen().AbsoluteToLocal(CursorInScreen) * WindowRootScale;
|
|
CursorPosInWindowSpace += (CursorWidget->GetDesiredSize() * SoftwareCursorScale * -0.5);
|
|
const FGeometry CursorGeometry = FGeometry::MakeRoot(CursorWidget->GetDesiredSize() * SoftwareCursorScale, FSlateLayoutTransform(CursorPosInWindowSpace));
|
|
|
|
CursorWidget->Paint(
|
|
FPaintArgs(&WindowToDraw.Get(), WindowToDraw->GetHittestGrid(), WindowToDraw->GetPositionInScreen(), SlateApp.GetCurrentTime(), SlateApp.GetDeltaTime()),
|
|
CursorGeometry, WindowToDraw->GetClippingRectangleInWindow(),
|
|
WindowElementList,
|
|
++MaxLayerId,
|
|
FWidgetStyle(),
|
|
WindowToDraw->IsEnabled());
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSlateUser::QueueSyntheticCursorMove()
|
|
{
|
|
// Q: Wait, why 2?
|
|
// A: Synthesized moves are processed last, after all other inputs (other inputs are often what call this),
|
|
// but Slate won't have had a chance yet to do any actual widget updating. We need to make sure we synthesize the
|
|
// move *after* slate has had a chance to update.
|
|
|
|
// Q: Ok, then shouldn't we only synthesize the move when we go from 1 -> 0?
|
|
// A: Nope - imagine a user provided input last frame without moving the mouse.
|
|
// That queued a synthetic move and was processed (possibly unnecessarily), going from 2 -> 1.
|
|
// This frame, input has been provided again, resetting this value back to 2.
|
|
// If we wait until we go from 1 -> 0, we would not synthesize a move until a frame without input, and
|
|
// we'd indefinitely delay the very update the synthetic move is intended to trigger until the user stopped providing any input.
|
|
NumPendingSyntheticCursorMoves = 2;
|
|
}
|
|
|
|
bool FSlateUser::SynthesizeCursorMoveIfNeeded()
|
|
{
|
|
// The slate loading widget thread is not allowed to execute this code as it is unsafe to read the hittest grid in another thread
|
|
if (bEnableSyntheticCursorMoves && IsInGameThread() && ensure(Cursor) && --NumPendingSyntheticCursorMoves >= 0)
|
|
{
|
|
// Synthetic cursor/mouse events accomplish two goals:
|
|
// 1) The UI can change even if the mouse doesn't move.
|
|
// Synthesizing a mouse move sends out events.
|
|
// In this case, the current and previous position will be the same.
|
|
//
|
|
// 2) The mouse moves, but the OS decided not to send us an event.
|
|
// e.g. Mouse moved outside of our window.
|
|
// In this case, the previous and current positions differ.
|
|
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
|
|
FInputDeviceId InputDeviceId = IPlatformInputDeviceMapper::Get().GetPrimaryInputDeviceForUser(GetPlatformUserId());
|
|
|
|
// The input device might be invalid if a split screen player has logged off but still has their controller plugged in
|
|
if (InputDeviceId.IsValid())
|
|
{
|
|
const bool bHasHardwareCursor = SlateApp.GetPlatformCursor() == Cursor;
|
|
const TSet<FKey> EmptySet;
|
|
FPointerEvent SyntheticCursorMoveEvent(
|
|
InputDeviceId,
|
|
FSlateApplication::CursorPointerIndex,
|
|
GetCursorPosition(),
|
|
GetPreviousCursorPosition(),
|
|
bHasHardwareCursor ? SlateApp.GetPressedMouseButtons() : EmptySet,
|
|
EKeys::Invalid,
|
|
0,
|
|
bHasHardwareCursor ? SlateApp.GetPlatformApplication()->GetModifierKeys() : FModifierKeysState(),
|
|
UserIndex);
|
|
|
|
SlateApp.ProcessMouseMoveEvent(SyntheticCursorMoveEvent, true);
|
|
return true;
|
|
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FSlateUser::SetFocusPath(const FWidgetPath& NewFocusPath, EFocusCause InFocusCause, bool bInShowFocus)
|
|
{
|
|
StrongFocusPath.Reset();
|
|
WeakFocusPath = NewFocusPath;
|
|
FocusCause = InFocusCause;
|
|
bShowFocus = bInShowFocus;
|
|
}
|
|
|
|
void FSlateUser::FinishFrame()
|
|
{
|
|
StrongFocusPath.Reset();
|
|
}
|
|
|
|
void FSlateUser::NotifyWindowDestroyed(TSharedRef<SWindow> DestroyedWindow)
|
|
{
|
|
if (StrongFocusPath && StrongFocusPath->IsValid() && DestroyedWindow == StrongFocusPath->GetWindow())
|
|
{
|
|
StrongFocusPath.Reset();
|
|
}
|
|
}
|
|
|
|
void FSlateUser::QueryCursor()
|
|
{
|
|
bQueryCursorRequested = false;
|
|
|
|
// The slate loading widget thread is not allowed to execute this code (it's unsafe to read the hittest grid in another thread)
|
|
if (bCanDrawCursor && Cursor && IsInGameThread() && FApp::CanEverRender())
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_SlateQueryCursor);
|
|
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
FCursorReply CursorReply = FCursorReply::Unhandled();
|
|
|
|
// Drag-drop gets first dibs if it exists
|
|
if (DragDropContent)
|
|
{
|
|
CursorReply = DragDropContent->OnCursorQuery();
|
|
}
|
|
|
|
if (!CursorReply.IsEventHandled())
|
|
{
|
|
const bool bHasHardwareCursor = SlateApp.GetPlatformCursor() == Cursor;
|
|
const FVector2f CurrentCursorPosition = GetCursorPosition();
|
|
const FVector2f LastCursorPosition = GetPreviousCursorPosition();
|
|
|
|
const TSet<FKey> EmptySet;
|
|
const FPointerEvent CursorEvent(
|
|
SlateApp.GetUserIndexForMouse(),
|
|
FSlateApplication::CursorPointerIndex,
|
|
CurrentCursorPosition,
|
|
LastCursorPosition,
|
|
CurrentCursorPosition - LastCursorPosition,
|
|
bHasHardwareCursor ? SlateApp.GetPressedMouseButtons() : EmptySet,
|
|
bHasHardwareCursor ? SlateApp.GetModifierKeys() : FModifierKeysState());
|
|
|
|
FWidgetPath WidgetsToQueryForCursor;
|
|
if (HasCursorCapture())
|
|
{
|
|
// Query widgets with mouse capture for the cursor
|
|
FWidgetPath MouseCaptorPath = GetCursorCaptorPath(FWeakWidgetPath::EInterruptedPathHandling::Truncate, &CursorEvent);
|
|
if (MouseCaptorPath.IsValid())
|
|
{
|
|
TSharedRef<SWindow> CaptureWindow = MouseCaptorPath.GetWindow();
|
|
const TSharedPtr<SWindow> ActiveModalWindow = SlateApp.GetActiveModalWindow();
|
|
|
|
// Never query the mouse captor path if it is outside an active modal window
|
|
if (!ActiveModalWindow || CaptureWindow == ActiveModalWindow || CaptureWindow->IsDescendantOf(ActiveModalWindow))
|
|
{
|
|
WidgetsToQueryForCursor = MouseCaptorPath;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
WidgetsToQueryForCursor = SlateApp.LocateWindowUnderMouse(CurrentCursorPosition, SlateApp.GetInteractiveTopLevelWindows(), false, UserIndex);
|
|
}
|
|
|
|
if (WidgetsToQueryForCursor.IsValid())
|
|
{
|
|
// Switch worlds for widgets in the current path
|
|
FScopedSwitchWorldHack SwitchWorld(WidgetsToQueryForCursor);
|
|
|
|
for (int32 WidgetIndex = WidgetsToQueryForCursor.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex)
|
|
{
|
|
const FArrangedWidget& ArrangedWidget = WidgetsToQueryForCursor.Widgets[WidgetIndex];
|
|
|
|
CursorReply = ArrangedWidget.Widget->OnCursorQuery(ArrangedWidget.Geometry, CursorEvent);
|
|
if (CursorReply.IsEventHandled())
|
|
{
|
|
#if WITH_SLATE_DEBUGGING
|
|
FSlateDebugging::BroadcastCursorQuery(ArrangedWidget.Widget, CursorReply);
|
|
#endif
|
|
|
|
if (!CursorReply.GetCursorWidget().IsValid())
|
|
{
|
|
for (; WidgetIndex >= 0; --WidgetIndex)
|
|
{
|
|
TOptional<TSharedRef<SWidget>> CursorWidget = WidgetsToQueryForCursor.Widgets[WidgetIndex].Widget->OnMapCursor(CursorReply);
|
|
if (CursorWidget.IsSet())
|
|
{
|
|
CursorReply.SetCursorWidget(WidgetsToQueryForCursor.GetWindow(), CursorWidget.GetValue());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!CursorReply.IsEventHandled() && WidgetsToQueryForCursor.IsValid())
|
|
{
|
|
// Query was NOT handled, and we are still over a slate window.
|
|
CursorReply = FCursorReply::Cursor(EMouseCursor::Default);
|
|
|
|
#if WITH_SLATE_DEBUGGING
|
|
FSlateDebugging::BroadcastCursorQuery(TSharedPtr<SWidget>(), CursorReply);
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Set the default cursor when there isn't an active window under the cursor and the mouse isn't captured
|
|
CursorReply = FCursorReply::Cursor(EMouseCursor::Default);
|
|
|
|
#if WITH_SLATE_DEBUGGING
|
|
FSlateDebugging::BroadcastCursorQuery(TSharedPtr<SWidget>(), CursorReply);
|
|
#endif
|
|
}
|
|
}
|
|
ProcessCursorReply(CursorReply);
|
|
}
|
|
}
|
|
|
|
void FSlateUser::LockCursor(const TSharedRef<SWidget>& Widget)
|
|
{
|
|
if (Cursor)
|
|
{
|
|
// Get a path to this widget so we know the position and size of its geometry
|
|
FWidgetPath WidgetPath;
|
|
const bool bFoundWidget = FSlateApplication::Get().GeneratePathToWidgetUnchecked(Widget, WidgetPath);
|
|
if (ensureMsgf(bFoundWidget, TEXT("Attempting to LockCursor() to widget but could not find widget %s"), *Widget->ToString()))
|
|
{
|
|
LockCursorInternal(WidgetPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSlateUser::UnlockCursor()
|
|
{
|
|
if (Cursor)
|
|
{
|
|
Cursor->Lock(nullptr);
|
|
LockingWidgetPath = FWeakWidgetPath();
|
|
}
|
|
}
|
|
|
|
void FSlateUser::UpdateCursor()
|
|
{
|
|
if (!Cursor)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (LockingWidgetPath.IsValid())
|
|
{
|
|
const FWidgetPath PathToWidget = LockingWidgetPath.ToWidgetPath(FWeakWidgetPath::EInterruptedPathHandling::ReturnInvalid);
|
|
if (PathToWidget.IsValid())
|
|
{
|
|
const FSlateRect ComputedClipRect = PathToWidget.Widgets.Last().Geometry.GetLayoutBoundingRect();
|
|
if (ComputedClipRect != LastComputedLockBounds)
|
|
{
|
|
// The locking widget is still valid, but its bounds have changed - gotta update the lock boundaries on the cursor to match
|
|
LockCursorInternal(PathToWidget);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Unlock immediately if the locking widget is no longer around
|
|
UnlockCursor();
|
|
}
|
|
}
|
|
|
|
// When Slate captures the mouse, it is up to us to set the cursor because the OS assumes that we own the mouse.
|
|
if (HasAnyCapture() || bQueryCursorRequested)
|
|
{
|
|
QueryCursor();
|
|
}
|
|
|
|
const double MoveEpsilonSquared = CursorSignificantMoveDetectionThreshold * CursorSignificantMoveDetectionThreshold;
|
|
if (FVector2D::DistSquared(GetPreviousCursorPosition(), GetCursorPosition()) > MoveEpsilonSquared)
|
|
{
|
|
LastCursorSignificantMoveTime = FPlatformTime::Seconds();
|
|
}
|
|
}
|
|
|
|
void FSlateUser::ProcessCursorReply(const FCursorReply& CursorReply)
|
|
{
|
|
if (Cursor && CursorReply.IsEventHandled())
|
|
{
|
|
if (bCanDrawCursor)
|
|
{
|
|
CursorWidgetPtr = CursorReply.GetCursorWidget();
|
|
if (CursorReply.GetCursorWidget().IsValid())
|
|
{
|
|
CursorReply.GetCursorWidget()->SetVisibility(EVisibility::HitTestInvisible);
|
|
CursorWindowPtr = CursorReply.GetCursorWindow();
|
|
if (!FSlateApplication::Get().IsFakingTouchEvents())
|
|
{
|
|
Cursor->SetType(EMouseCursor::Custom);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CursorWindowPtr.Reset();
|
|
Cursor->SetType(CursorReply.GetCursorType());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Cursor->SetType(EMouseCursor::None);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CursorWindowPtr.Reset();
|
|
CursorWidgetPtr.Reset();
|
|
}
|
|
}
|
|
|
|
void FSlateUser::LockCursorInternal(const FWidgetPath& WidgetPath)
|
|
{
|
|
check(Cursor);
|
|
check(WidgetPath.IsValid());
|
|
|
|
// Do not attempt to lock the cursor to the window if its not in the foreground. It would cause annoying side effects
|
|
TSharedPtr<const FGenericWindow> NativeWindow = WidgetPath.GetWindow()->GetNativeWindow();
|
|
if (NativeWindow && NativeWindow->IsForegroundWindow())
|
|
{
|
|
// The last widget in the path should be the widget we are locking the cursor to
|
|
FSlateRect SlateClipRect = WidgetPath.Widgets.Last().Geometry.GetLayoutBoundingRect();
|
|
LastComputedLockBounds = SlateClipRect;
|
|
LockingWidgetPath = WidgetPath;
|
|
|
|
// Generate a screen space clip rect based on the widget's geometry
|
|
#if PLATFORM_DESKTOP
|
|
const bool bIsBorderlessGameWindow = NativeWindow->IsDefinitionValid() && NativeWindow->GetDefinition().Type == EWindowType::GameWindow && !NativeWindow->GetDefinition().HasOSWindowBorder;
|
|
const int32 ClipRectAdjustment = bIsBorderlessGameWindow ? 0 : 1;
|
|
#else
|
|
const int32 ClipRectAdjustment = 0;
|
|
#endif
|
|
// Screen space mapping scales everything. When viewport resolution doesn't match platform resolution,
|
|
// this causes offset cursor hit-tests in fullscreen. Correct when capturing mouse as viewport widget may be smaller than screen in pixels.
|
|
if (FSlateApplication::Get().GetTransformFullscreenMouseInput() && !GIsEditor && NativeWindow->GetWindowMode() == EWindowMode::Fullscreen)
|
|
{
|
|
FDisplayMetrics CachedDisplayMetrics;
|
|
FSlateApplication::Get().GetCachedDisplayMetrics(CachedDisplayMetrics);
|
|
FVector2f DisplaySize = { (float)CachedDisplayMetrics.PrimaryDisplayWidth, (float)CachedDisplayMetrics.PrimaryDisplayHeight };
|
|
FVector2f DisplayDistortion = SlateClipRect.GetSize() / DisplaySize;
|
|
|
|
SlateClipRect.Left /= DisplayDistortion.X;
|
|
SlateClipRect.Top /= DisplayDistortion.Y;
|
|
SlateClipRect.Right /= DisplayDistortion.X;
|
|
SlateClipRect.Bottom /= DisplayDistortion.Y;
|
|
}
|
|
|
|
// Note: We round the upper left coordinate of the clip rect so we guarantee the rect is inside the geometry of the widget. If we truncated when there is a half pixel we would cause the clip
|
|
// rect to be half a pixel larger than the geometry and cause the mouse to go outside of the geometry.
|
|
RECT ClipRect;
|
|
ClipRect.left = FMath::RoundToInt(SlateClipRect.Left + ClipRectAdjustment);
|
|
ClipRect.top = FMath::RoundToInt(SlateClipRect.Top + ClipRectAdjustment);
|
|
ClipRect.right = FMath::TruncToInt(SlateClipRect.Right - ClipRectAdjustment);
|
|
ClipRect.bottom = FMath::TruncToInt(SlateClipRect.Bottom - ClipRectAdjustment);
|
|
|
|
// Lock the cursor to the widget
|
|
Cursor->Lock(&ClipRect);
|
|
}
|
|
}
|
|
|
|
TSharedRef<SWindow> FSlateUser::GetOrCreateTooltipWindow()
|
|
{
|
|
if (TooltipWindowPtr.IsValid())
|
|
{
|
|
return TooltipWindowPtr.Pin().ToSharedRef();
|
|
}
|
|
|
|
// If we don't have a window already, make a new one and add it (but don't show it until we've put stuff in)
|
|
TSharedRef<SWindow> NewTooltipWindow = SWindow::MakeToolTipWindow();
|
|
TooltipWindowPtr = NewTooltipWindow;
|
|
FSlateApplication::Get().AddWindow(NewTooltipWindow, /*bShowImmediately =*/false);
|
|
return NewTooltipWindow;
|
|
}
|
|
|
|
void FSlateUser::NotifyTouchStarted(const FPointerEvent& TouchEvent)
|
|
{
|
|
UE_CLOG(PointerPositionsByIndex.Contains(TouchEvent.GetPointerIndex()), LogSlate, Error, TEXT("SlateUser [%d] notified of a touch starting for pointer [%d] without finding out it ever ended."), TouchEvent.GetUserIndex(), TouchEvent.GetPointerIndex());
|
|
|
|
GestureDetector.OnTouchStarted(TouchEvent.GetPointerIndex(), TouchEvent.GetScreenSpacePosition());
|
|
PointerPositionsByIndex.FindOrAdd(TouchEvent.GetPointerIndex()) = TouchEvent.GetScreenSpacePosition();
|
|
}
|
|
|
|
void FSlateUser::NotifyPointerMoveBegin(const FPointerEvent& PointerEvent)
|
|
{
|
|
if (PointerEvent.IsTouchEvent())
|
|
{
|
|
GestureDetector.OnTouchMoved(PointerEvent.GetPointerIndex(), PointerEvent.GetScreenSpacePosition());
|
|
}
|
|
PointerPositionsByIndex.FindOrAdd(PointerEvent.GetPointerIndex()) = PointerEvent.GetScreenSpacePosition();
|
|
}
|
|
|
|
void FSlateUser::NotifyPointerMoveComplete(const FPointerEvent& PointerEvent, const FWidgetPath& WidgetsUnderPointer)
|
|
{
|
|
// Give the current drag drop operation a chance to do something custom (e.g. update the Drag/Drop preview based on content)
|
|
if (IsDragDroppingAffected(PointerEvent))
|
|
{
|
|
FDragDropEvent DragDropEvent(PointerEvent, DragDropContent);
|
|
|
|
#if WITH_EDITOR
|
|
//@TODO VREDITOR - Remove and move to interaction component
|
|
if (FSlateApplication::Get().OnDragDropCheckOverride.IsBound() && DragDropEvent.GetOperation())
|
|
{
|
|
DragDropEvent.GetOperation()->SetDecoratorVisibility(false);
|
|
DragDropEvent.GetOperation()->SetCursorOverride(EMouseCursor::None);
|
|
DragDropContent->SetCursorOverride(EMouseCursor::None);
|
|
}
|
|
#endif
|
|
|
|
FScopedSwitchWorldHack SwitchWorld(WidgetsUnderPointer);
|
|
DragDropContent->OnDragged(DragDropEvent);
|
|
|
|
// Update the window we're under for rendering the drag drop operation if it's a windowless drag drop operation.
|
|
if (WidgetsUnderPointer.IsValid())
|
|
{
|
|
DragDropWindowPtr = WidgetsUnderPointer.GetWindow();
|
|
}
|
|
else
|
|
{
|
|
DragDropWindowPtr.Reset();
|
|
}
|
|
|
|
if (ensure(Cursor))
|
|
{
|
|
FCursorReply CursorReply = DragDropContent->OnCursorQuery();
|
|
if (!CursorReply.IsEventHandled())
|
|
{
|
|
// Set the default cursor when there isn't an active window under the cursor and the mouse isn't captured
|
|
CursorReply = FCursorReply::Cursor(EMouseCursor::Default);
|
|
}
|
|
|
|
ProcessCursorReply(CursorReply);
|
|
}
|
|
|
|
}
|
|
else if (!IsDragDropping())
|
|
{
|
|
DragDropWindowPtr.Reset();
|
|
}
|
|
|
|
PreviousPointerPositionsByIndex.Add(PointerEvent.GetPointerIndex(), PointerEvent.GetScreenSpacePosition());
|
|
WidgetsUnderPointerLastEventByIndex.Add(PointerEvent.GetPointerIndex(), FWeakWidgetPath(WidgetsUnderPointer));
|
|
}
|
|
|
|
void FSlateUser::UpdatePointerPosition(const FPointerEvent& PointerEvent)
|
|
{
|
|
UpdatePointerPosition(PointerEvent.GetPointerIndex(), PointerEvent.GetScreenSpacePosition());
|
|
}
|
|
|
|
void FSlateUser::UpdatePointerPosition(uint32 PointerIndex, const FVector2f& Position)
|
|
{
|
|
PointerPositionsByIndex.FindOrAdd(PointerIndex) = Position;
|
|
PreviousPointerPositionsByIndex.FindOrAdd(PointerIndex) = Position;
|
|
}
|
|
|
|
void FSlateUser::StartDragDetection(const FWidgetPath& PathToWidget, int32 PointerIndex, FKey DragButton, UE::Slate::FDeprecateVector2DParameter StartLocation)
|
|
{
|
|
DragStatesByPointerIndex.Add(PointerIndex, FDragDetectionState(PathToWidget, PointerIndex, DragButton, StartLocation));
|
|
}
|
|
|
|
FWidgetPath FSlateUser::DetectDrag(const FPointerEvent& PointerEvent, float DragTriggerDistance)
|
|
{
|
|
if (FDragDetectionState* DragState = DragStatesByPointerIndex.Find(PointerEvent.GetPointerIndex()))
|
|
{
|
|
const FVector2f DragDelta = DragState->DragStartLocation - PointerEvent.GetScreenSpacePosition();
|
|
if (DragDelta.SizeSquared() > FMath::Square(DragTriggerDistance))
|
|
{
|
|
FWidgetPath DragDetectionPath = DragState->DetectDragForWidget.ToWidgetPath(FWeakWidgetPath::EInterruptedPathHandling::ReturnInvalid);
|
|
if (DragDetectionPath.IsValid())
|
|
{
|
|
ResetDragDetection();
|
|
return DragDetectionPath;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FWidgetPath();
|
|
}
|
|
|
|
bool FSlateUser::IsDetectingDrag(uint32 PointerIndex) const
|
|
{
|
|
return DragStatesByPointerIndex.Contains(PointerIndex);
|
|
}
|
|
|
|
void FSlateUser::NotifyPointerReleased(const FPointerEvent& PointerEvent, const FWidgetPath& WidgetsUnderCursor, TSharedPtr<FDragDropOperation> DroppedContent, bool bWasHandled)
|
|
{
|
|
const int32 PointerIdx = PointerEvent.GetPointerIndex();
|
|
|
|
const FDragDetectionState* DragState = DragStatesByPointerIndex.Find(PointerIdx);
|
|
if (DragState && DragState->TriggerButton == PointerEvent.GetEffectingButton())
|
|
{
|
|
// The user has released the button (or finger) that was supposed to start the drag; stop detecting it.
|
|
DragState = nullptr;
|
|
DragStatesByPointerIndex.Remove(PointerIdx);
|
|
}
|
|
|
|
if (!HasCapture(PointerIdx))
|
|
{
|
|
// When we perform a touch end, we need to also send a mouse leave as if it were a cursor.
|
|
if (PointerEvent.IsTouchEvent() && !FSlateApplication::Get().IsFakingTouchEvents())
|
|
{
|
|
for (int32 WidgetIndex = WidgetsUnderCursor.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex)
|
|
{
|
|
WidgetsUnderCursor.Widgets[WidgetIndex].Widget->OnMouseLeave(PointerEvent);
|
|
}
|
|
}
|
|
#if WITH_SLATE_DEBUGGING
|
|
const bool bIsFingerCapturedPostRelease = PointerEvent.IsTouchEvent() && HasCapture(PointerEvent.GetPointerIndex());
|
|
ensureMsgf(!bIsFingerCapturedPostRelease, TEXT("Touch pointer was captured while in the process of being released. Since touch pointers cease to exist when the touch ends, this makes no sense."));
|
|
#endif
|
|
|
|
// Note: FSlateApplication caches this content off BEFORE routing the pointer up, as OnDrop can result in re-entrance of the pointer up routing
|
|
// To avoid executing the drop operation twice, our cached DragDropContent is wiped before the first routing.
|
|
// Thus, we rely on the provided DroppedContent here, which will never be provided as valid more than once
|
|
if (DroppedContent && DroppedContent->AffectedByPointerEvent(PointerEvent))
|
|
{
|
|
// @todo slate : depending on SetEventPath() is not ideal.
|
|
FPointerEvent ModifiedEvent(PointerEvent);
|
|
ModifiedEvent.SetEventPath(WidgetsUnderCursor);
|
|
DroppedContent->OnDrop(bWasHandled, ModifiedEvent);
|
|
WidgetsUnderPointerLastEventByIndex.Remove(PointerIdx);
|
|
}
|
|
}
|
|
|
|
if (PointerEvent.IsTouchEvent())
|
|
{
|
|
GestureDetector.OnTouchEnded(PointerIdx, PointerEvent.GetScreenSpacePosition());
|
|
|
|
// For touch events, we always invalidate capture for the pointer. There's no reason to ever maintain capture for
|
|
// fingers no longer in contact with the screen.
|
|
ReleaseCapture(PointerIdx);
|
|
|
|
// When touch pointers are released, they also effectively cease to exist until a touch begins again
|
|
PointerPositionsByIndex.Remove(PointerIdx);
|
|
PreviousPointerPositionsByIndex.Remove(PointerIdx);
|
|
WidgetsUnderPointerLastEventByIndex.Remove(PointerIdx);
|
|
}
|
|
}
|
|
|
|
void FSlateUser::ResetDragDetection()
|
|
{
|
|
DragStatesByPointerIndex.Reset();
|
|
}
|
|
|
|
void FSlateUser::SetDragDropContent(TSharedRef<FDragDropOperation> InDragDropContent)
|
|
{
|
|
checkf(!IsDragDropping(), TEXT("Drag and Drop already in progress!"));
|
|
DragDropContent = InDragDropContent;
|
|
}
|
|
|
|
void FSlateUser::ResetDragDropContent()
|
|
{
|
|
DragDropContent.Reset();
|
|
}
|
|
|
|
void FSlateUser::UpdateTooltip(const FMenuStack& MenuStack, bool bCanSpawnNewTooltip)
|
|
{
|
|
FSlateApplication& SlateApp = FSlateApplication::Get();
|
|
if (!SlateApp.GetAllowTooltips())
|
|
{
|
|
CloseTooltip();
|
|
return;
|
|
}
|
|
|
|
SCOPE_CYCLE_COUNTER(STAT_SlateUpdateTooltip);
|
|
|
|
const double MotionLessDurationBeforeAllowingNewToolTip = 0.05;
|
|
bCanSpawnNewTooltip = bCanSpawnNewTooltip && (bCanDrawCursor || bAllowTooltipsWithHiddenCursor) && (FPlatformTime::Seconds() - LastCursorSignificantMoveTime > MotionLessDurationBeforeAllowingNewToolTip);
|
|
|
|
float DPIScaleFactor = 1.0f; //todo: this value is never changed, we should investigate if it is necessary or not to handle it for the force field.
|
|
FWidgetPath WidgetsToQueryForTooltip;
|
|
|
|
const bool bCheckForTooltipChanges =
|
|
IsInGameThread() && // We should never allow the slate loading thread to create new windows or interact with the hittest grid
|
|
!SlateApp.IsUsingHighPrecisionMouseMovment() && // If we are using HighPrecision movement then we can't rely on the OS cursor to be accurate
|
|
!IsDragDropping() && // We must not currently be in the middle of a drag-drop action
|
|
(!SlateApp.GetPressedMouseButtons().Contains(EKeys::LeftMouseButton) || bAllowTooltipsWhileMouseDown) && // We must not currently be clicking on a widget
|
|
(
|
|
SlateApp.IsActive() || // Assume we need update if app is active
|
|
//@todo DanH: We need to check if OUR cursor is over a slate window, not just the platform cursor.
|
|
// See about adding FPlatformApplication::GetSlateWindowUnderPoint(FVector2D) or something.
|
|
SlateApp.GetPlatformApplication()->IsCursorDirectlyOverSlateWindow() // The cursor must be over a Slate window
|
|
);
|
|
|
|
if (bCheckForTooltipChanges)
|
|
{
|
|
// We're gonna check each widget under the cursor (including disabled widgets) until we find one with a tooltip to show
|
|
FWidgetPath WidgetsUnderCursor = SlateApp.LocateWindowUnderMouse(GetCursorPosition(), SlateApp.GetInteractiveTopLevelWindows(), /*bIgnoreEnabledStatus =*/true, UserIndex);
|
|
if (WidgetsUnderCursor.IsValid() && WidgetsUnderCursor.GetWindow() != TooltipWindowPtr.Pin())
|
|
{
|
|
WidgetsToQueryForTooltip = WidgetsUnderCursor;
|
|
}
|
|
}
|
|
|
|
TOptional<FSlateRect> ForceFieldRect;
|
|
TSharedPtr<IToolTip> NewTooltip;
|
|
TSharedPtr<SWidget> WidgetProvidingNewTooltip;
|
|
for (int32 WidgetIndex = WidgetsToQueryForTooltip.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex)
|
|
{
|
|
const FArrangedWidget* ArrangedWidget = &WidgetsToQueryForTooltip.Widgets[WidgetIndex];
|
|
const TSharedRef<SWidget>& CurWidget = ArrangedWidget->Widget;
|
|
|
|
if (!NewTooltip.IsValid())
|
|
{
|
|
// Make sure the tooltip has something to show before we pick it
|
|
TSharedPtr<IToolTip> WidgetTooltip = CurWidget->GetToolTip();
|
|
if (WidgetTooltip.IsValid() && !WidgetTooltip->IsEmpty())
|
|
{
|
|
WidgetProvidingNewTooltip = CurWidget;
|
|
NewTooltip = WidgetTooltip;
|
|
}
|
|
}
|
|
|
|
// Make sure we account for all widgets in the path with a force field, even if we've already found the tooltip we'll be showing
|
|
if (CurWidget->HasToolTipForceField())
|
|
{
|
|
if (!ForceFieldRect.IsSet())
|
|
{
|
|
ForceFieldRect = ArrangedWidget->Geometry.GetLayoutBoundingRect();
|
|
}
|
|
else
|
|
{
|
|
// Grow the rect to encompass this geometry to be super safe
|
|
// The parent's rect should always be inclusive of its child, so this should usually be overkill.
|
|
ForceFieldRect = ForceFieldRect->Expand(ArrangedWidget->Geometry.GetLayoutBoundingRect());
|
|
}
|
|
ForceFieldRect = (1.0f / DPIScaleFactor) * ForceFieldRect.GetValue();
|
|
}
|
|
}
|
|
|
|
TSharedPtr<SWidget> NewTooltipVisualizer;
|
|
TSharedPtr<IToolTip> ActiveTooltip = ActiveTooltipInfo.Tooltip.Pin();
|
|
const bool bTooltipChanged = NewTooltip != ActiveTooltip;
|
|
if (bTooltipChanged)
|
|
{
|
|
// Remove existing tooltip if there is one.
|
|
if (ActiveTooltipInfo.TooltipVisualizer.IsValid())
|
|
{
|
|
ActiveTooltipInfo.TooltipVisualizer.Pin()->OnVisualizeTooltip(nullptr);
|
|
}
|
|
|
|
// Notify the new tooltip that it's about to be opened.
|
|
if (NewTooltip && bCanSpawnNewTooltip)
|
|
{
|
|
NewTooltip->OnOpening();
|
|
ActiveTooltipInfo.HasBeenPositioned = false;
|
|
}
|
|
|
|
// Some widgets might want to provide an alternative Tooltip Handler.
|
|
if (bCanSpawnNewTooltip || !NewTooltip)
|
|
{
|
|
TSharedPtr<SWidget> NewTooltipWidget = NewTooltip ? NewTooltip->AsWidget() : TSharedPtr<SWidget>();
|
|
for (int32 WidgetIndex = WidgetsToQueryForTooltip.Widgets.Num() - 1; WidgetIndex >= 0; --WidgetIndex)
|
|
{
|
|
const TSharedRef<SWidget>& CurWidget = WidgetsToQueryForTooltip.Widgets[WidgetIndex].Widget;
|
|
if (CurWidget->OnVisualizeTooltip(NewTooltipWidget))
|
|
{
|
|
// Someone is taking care of visualizing this tooltip
|
|
NewTooltipVisualizer = CurWidget;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If a widget under the cursor has a tool-tip forcefield active, then go through any menus
|
|
// in the menu stack that are above that widget's window, and make sure those windows also
|
|
// prevent the tool-tip from encroaching. This prevents tool-tips from drawing over sub-menus
|
|
// spawned from menu items in a different window, for example.
|
|
if (ForceFieldRect.IsSet() && WidgetsToQueryForTooltip.IsValid())
|
|
{
|
|
if (TSharedPtr<IMenu> MenuInPath = MenuStack.FindMenuInWidgetPath(WidgetsToQueryForTooltip))
|
|
{
|
|
FSlateRect MenuStackRectangle;
|
|
const bool bWasSolutionFound = MenuStack.GetToolTipForceFieldRect(MenuInPath.ToSharedRef(), WidgetsToQueryForTooltip, MenuStackRectangle);
|
|
if (bWasSolutionFound)
|
|
{
|
|
ForceFieldRect = ForceFieldRect->Expand(MenuStackRectangle);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start by targeting the position calculated last frame.
|
|
FVector2f DesiredLocation = ActiveTooltipInfo.DesiredLocation;
|
|
|
|
// When a tooltip changes from interactive back to regular, there are a few frames during which IsInteractive()
|
|
// returns false, but the tooltip window is still rendering the interactive content. Computing a new desired location
|
|
// during this state can lead to the tooltip jumping around visibly. This variable indicates when we're in this situation,
|
|
// so that we don't look for a new desired location yet.
|
|
bool bStillAtInteractiveSize = !bTooltipChanged && ActiveTooltip && ActiveTooltipInfo.WasInteractive && TooltipWindowPtr.IsValid() && ActiveTooltipInfo.DesiredSize == TooltipWindowPtr.Pin()->GetDesiredSizeDesktopPixels();
|
|
|
|
// Calculate a new desired location for the tooltip if:
|
|
// - it's a non-interactive tooltip, do this every frame so that it moves with the cursor,
|
|
// unless the tooltip window is still rendering interactive content.
|
|
// - if it's interactive, only do this if the tooltip hasn't been positioned yet.
|
|
if (ActiveTooltip)
|
|
{
|
|
if ((!ActiveTooltip->IsInteractive() && !bStillAtInteractiveSize) || !ActiveTooltipInfo.HasBeenPositioned)
|
|
{
|
|
// New tooltips and non-interactive tooltips appear offset from the cursor position, and they follow the cursor as it moves.
|
|
DesiredLocation = GetPreviousCursorPosition() + SlateDefs::TooltipOffsetFromMouse;
|
|
ActiveTooltipInfo.WasInteractive = false;
|
|
ActiveTooltipInfo.HasBeenPositioned = true;
|
|
|
|
// Allow interactive tooltips to adjust the window location
|
|
if (ActiveTooltip->IsInteractive() && !NewTooltipVisualizer.IsValid())
|
|
{
|
|
FVector2D DesiredLocation2d(DesiredLocation);
|
|
ActiveTooltip->OnSetInteractiveWindowLocation(DesiredLocation2d);
|
|
DesiredLocation = UE::Slate::CastToVector2f(DesiredLocation2d);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move the desired position back from the edges of the screen if needed.
|
|
FVector2f TooltipSize(ForceInitToZero);
|
|
if (TooltipWindowPtr.IsValid())
|
|
{
|
|
TooltipSize = TooltipWindowPtr.Pin()->GetDesiredSizeDesktopPixels();
|
|
FSlateRect Anchor(DesiredLocation.X, DesiredLocation.Y, DesiredLocation.X, DesiredLocation.Y);
|
|
DesiredLocation = SlateApp.CalculatePopupWindowPosition(Anchor, TooltipSize, /*bAutoAdjustForDPIScale =*/false, FVector2f::ZeroVector, EOrientation::Orient_Vertical, EPopupLayoutMode::ToolTip);
|
|
}
|
|
|
|
// Repel tooltip from a force field, if necessary
|
|
if (ForceFieldRect.IsSet() && !ActiveTooltipInfo.WasInteractive)
|
|
{
|
|
FVector2f TooltipShift;
|
|
TooltipShift.X = (ForceFieldRect->Right + SlateDefs::TooltipOffsetFromForceField.X) - DesiredLocation.X;
|
|
TooltipShift.Y = (ForceFieldRect->Bottom + SlateDefs::TooltipOffsetFromForceField.Y) - DesiredLocation.Y;
|
|
|
|
// Make sure the tooltip needs to be offset
|
|
if (TooltipShift.X != 0.0f || TooltipShift.Y != 0.0f)
|
|
{
|
|
// Find the best edge to move the tooltip towards
|
|
if (ActiveTooltipInfo.OffsetDirection == ETooltipOffsetDirection::Undetermined)
|
|
{
|
|
ETooltipOffsetDirection PotentialOffsetDirection = FMath::Abs(TooltipShift.X) < FMath::Abs(TooltipShift.Y) ? ETooltipOffsetDirection::Right : ETooltipOffsetDirection::Down;
|
|
if (PotentialOffsetDirection == ETooltipOffsetDirection::Right)
|
|
{
|
|
FVector2f TentativeDesiredLocation = DesiredLocation + FVector2f(TooltipShift.X, 0);
|
|
FSlateRect Anchor(TentativeDesiredLocation.X, TentativeDesiredLocation.Y, TentativeDesiredLocation.X, TentativeDesiredLocation.Y);
|
|
FVector2f NewDesiredLocation = SlateApp.CalculatePopupWindowPosition(Anchor, TooltipSize, /*bAutoAdjustForDPIScale =*/false, FVector2f::ZeroVector, EOrientation::Orient_Vertical, EPopupLayoutMode::ToolTip);
|
|
// If after adjustment the tooltip still overlaps with the force field, try the other direction
|
|
if (FSlateRect::DoRectanglesIntersect(ForceFieldRect.GetValue(), FSlateRect(NewDesiredLocation, NewDesiredLocation + TooltipSize)))
|
|
{
|
|
PotentialOffsetDirection = ETooltipOffsetDirection::Down;
|
|
}
|
|
}
|
|
|
|
if (PotentialOffsetDirection == ETooltipOffsetDirection::Down)
|
|
{
|
|
FVector2f TentativeDesiredLocation = DesiredLocation + FVector2f(0, TooltipShift.Y);
|
|
FSlateRect Anchor(TentativeDesiredLocation.X, TentativeDesiredLocation.Y, TentativeDesiredLocation.X, TentativeDesiredLocation.Y);
|
|
FVector2f NewDesiredLocation = SlateApp.CalculatePopupWindowPosition(Anchor, TooltipSize, /*bAutoAdjustForDPIScale =*/false, FVector2f::ZeroVector, EOrientation::Orient_Vertical, EPopupLayoutMode::ToolTip);
|
|
// If after adjustment the tooltip still overlaps with the force field, try the other direction
|
|
if (FSlateRect::DoRectanglesIntersect(ForceFieldRect.GetValue(), FSlateRect(NewDesiredLocation, NewDesiredLocation + TooltipSize)))
|
|
{
|
|
PotentialOffsetDirection = ETooltipOffsetDirection::Right;
|
|
}
|
|
}
|
|
|
|
ActiveTooltipInfo.OffsetDirection = PotentialOffsetDirection;
|
|
}
|
|
|
|
check(ActiveTooltipInfo.OffsetDirection != ETooltipOffsetDirection::Undetermined);
|
|
if (ActiveTooltipInfo.OffsetDirection == ETooltipOffsetDirection::Right)
|
|
{
|
|
// Move right
|
|
DesiredLocation.X += TooltipShift.X;
|
|
ActiveTooltipInfo.OffsetDirection = ETooltipOffsetDirection::Right;
|
|
}
|
|
else
|
|
{
|
|
// Move down
|
|
DesiredLocation.Y += TooltipShift.Y;
|
|
ActiveTooltipInfo.OffsetDirection = ETooltipOffsetDirection::Down;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the desired location so that interactive tooltips can continue to target it in future frames even after the mouse moves away
|
|
ActiveTooltipInfo.DesiredLocation = DesiredLocation;
|
|
|
|
// The tool tip changed...
|
|
if (bTooltipChanged)
|
|
{
|
|
// Close any existing tooltips; Unless the current tooltip is interactive and we don't have a valid tooltip to replace it
|
|
if (NewTooltip || (ActiveTooltip && !ActiveTooltip->IsInteractive()))
|
|
{
|
|
CloseTooltip();
|
|
|
|
if (NewTooltip && bCanSpawnNewTooltip)
|
|
{
|
|
if (NewTooltipVisualizer)
|
|
{
|
|
ActiveTooltipInfo.TooltipVisualizer = NewTooltipVisualizer;
|
|
ActiveTooltipInfo.Tooltip = NewTooltip;
|
|
}
|
|
else
|
|
{
|
|
ShowTooltip(NewTooltip.ToSharedRef(), DesiredLocation);
|
|
ActiveTooltipInfo.SourceWidget = WidgetProvidingNewTooltip;
|
|
}
|
|
ActiveTooltip = NewTooltip;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TooltipWindowPtr.IsValid())
|
|
{
|
|
// Only enable tooltip transitions if we're running at a decent frame rate
|
|
const bool bAllowAnimations = FSlateApplication::Get().IsRunningAtTargetFrameRate();
|
|
|
|
// How long since the tooltip was summoned?
|
|
const double PlatformSeconds = FPlatformTime::Seconds();
|
|
const float TimeSinceSummon = (float)(PlatformSeconds - TooltipSummonDelay - ActiveTooltipInfo.SummonTime);
|
|
const float TooltipOpacity = FMath::Clamp<float>(TimeSinceSummon / TooltipIntroDuration, 0.0f, 1.0f);
|
|
|
|
// Update window opacity
|
|
TSharedRef<SWindow> TooltipWindow = TooltipWindowPtr.Pin().ToSharedRef();
|
|
TooltipWindow->SetOpacity(TooltipOpacity);
|
|
|
|
// How far tool tips should slide
|
|
const FVector2f SlideDistance(30.0f, 5.0f);
|
|
|
|
// Apply steep inbound curve to the movement, so it looks like it quickly decelerating
|
|
const float SlideProgress = bAllowAnimations ? FMath::Pow(1.0f - TooltipOpacity, 3.0f) : 0.0f;
|
|
|
|
FVector2f WindowLocation = DesiredLocation + (SlideProgress * SlideDistance);
|
|
if (WindowLocation != TooltipWindow->GetPositionInScreen()
|
|
|| TooltipWindow->GetDesiredSize() != ActiveTooltipInfo.DesiredSize)
|
|
{
|
|
// already handled
|
|
const bool bAutoAdjustForDPIScale = false;
|
|
|
|
// Avoid the edges of the desktop
|
|
FSlateRect Anchor(WindowLocation.X, WindowLocation.Y, WindowLocation.X, WindowLocation.Y);
|
|
WindowLocation = SlateApp.CalculateTooltipWindowPosition(Anchor, TooltipWindow->GetDesiredSizeDesktopPixels(), bAutoAdjustForDPIScale, (ActiveTooltip && ActiveTooltip->IsInteractive()) ? EPopupCursorOverlapMode::AllowOverlap : EPopupCursorOverlapMode::PreventOverlap);
|
|
|
|
// Cache the size to compare against in future frames
|
|
ActiveTooltipInfo.DesiredSize = TooltipWindow->GetDesiredSize();
|
|
|
|
// Update the tool tip window positioning
|
|
// SetCachedScreenPosition is a hack (issue tracked as TTP #347070) which is needed because code in TickWindowAndChildren()/DrawPrepass()
|
|
// assumes GetPositionInScreen() to correspond to the new window location in the same tick. This is true on Windows, but other
|
|
// OSes (Linux in particular) may not update cached screen position until next time events are polled.
|
|
TooltipWindow->SetCachedScreenPosition(WindowLocation);
|
|
TooltipWindow->MoveWindowTo(WindowLocation);
|
|
}
|
|
|
|
// Cache whether the tooltip is in interactive mode.
|
|
if (ActiveTooltip.IsValid() && ActiveTooltip->IsInteractive())
|
|
{
|
|
ActiveTooltipInfo.WasInteractive = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FSlateUser::ResetTooltipWindow()
|
|
{
|
|
if (TooltipWindowPtr.IsValid())
|
|
{
|
|
TooltipWindowPtr.Pin()->RequestDestroyWindow();
|
|
TooltipWindowPtr.Reset();
|
|
}
|
|
}
|
|
|
|
bool FSlateUser::IsWindowHousingInteractiveTooltip(const TSharedRef<const SWindow>& WindowToTest) const
|
|
{
|
|
if (WindowToTest == TooltipWindowPtr.Pin())
|
|
{
|
|
const TSharedPtr<IToolTip> ActiveTooltip = ActiveTooltipInfo.Tooltip.Pin();
|
|
return ActiveTooltip && ActiveTooltip->IsInteractive();
|
|
}
|
|
return false;
|
|
}
|