1362 lines
42 KiB
C++
1362 lines
42 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Widgets/Layout/SScrollBox.h"
|
|
#include "Rendering/DrawElements.h"
|
|
#include "Types/SlateConstants.h"
|
|
#include "Layout/LayoutUtils.h"
|
|
#include "Widgets/SOverlay.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Widgets/Images/SImage.h"
|
|
|
|
|
|
void SScrollBox::FSlot::Construct(const FChildren& SlotOwner, FSlotArguments&& InArgs)
|
|
{
|
|
TBasicLayoutWidgetSlot<FSlot>::Construct(SlotOwner, MoveTemp(InArgs));
|
|
TResizingWidgetSlotMixin<FSlot>::ConstructMixin(SlotOwner, MoveTemp(InArgs));
|
|
}
|
|
|
|
void SScrollBox::FSlot::RegisterAttributes(FSlateWidgetSlotAttributeInitializer& AttributeInitializer)
|
|
{
|
|
TBasicLayoutWidgetSlot<FSlot>::RegisterAttributes(AttributeInitializer);
|
|
TResizingWidgetSlotMixin<FSlot>::RegisterAttributesMixin(AttributeInitializer);
|
|
}
|
|
|
|
SScrollBox::FSlot::FSlotArguments SScrollBox::Slot()
|
|
{
|
|
return FSlot::FSlotArguments(MakeUnique<FSlot>());
|
|
}
|
|
|
|
void SScrollPanel::Construct(const FArguments& InArgs, TArray<SScrollBox::FSlot::FSlotArguments> InSlots)
|
|
{
|
|
PhysicalOffset = 0;
|
|
Children.AddSlots(MoveTemp(InSlots));
|
|
Orientation = InArgs._Orientation;
|
|
BackPadScrolling = InArgs._BackPadScrolling;
|
|
FrontPadScrolling = InArgs._FrontPadScrolling;
|
|
}
|
|
|
|
void SScrollPanel::OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const
|
|
{
|
|
const float ScrollPadding = Orientation == Orient_Vertical ? AllottedGeometry.GetLocalSize().Y : AllottedGeometry.GetLocalSize().X;
|
|
const float ChildrenOffset = -PhysicalOffset + (BackPadScrolling ? ScrollPadding : 0);
|
|
const bool AllowShrink = false;
|
|
|
|
if (Orientation == EOrientation::Orient_Horizontal)
|
|
{
|
|
ArrangeChildrenInStack<EOrientation::Orient_Horizontal>(GSlateFlowDirection, this->Children, AllottedGeometry, ArrangedChildren, ChildrenOffset, AllowShrink);
|
|
}
|
|
else
|
|
{
|
|
ArrangeChildrenInStack<EOrientation::Orient_Vertical>(GSlateFlowDirection, this->Children, AllottedGeometry, ArrangedChildren, ChildrenOffset, AllowShrink);
|
|
}
|
|
}
|
|
|
|
FVector2D SScrollPanel::ComputeDesiredSize(float) const
|
|
{
|
|
FVector2D ThisDesiredSize = FVector2D::ZeroVector;
|
|
for (int32 SlotIndex = 0; SlotIndex < Children.Num(); ++SlotIndex)
|
|
{
|
|
const SScrollBox::FSlot& ThisSlot = Children[SlotIndex];
|
|
if (ThisSlot.GetWidget()->GetVisibility() != EVisibility::Collapsed)
|
|
{
|
|
const FVector2D ChildDesiredSize = ThisSlot.GetWidget()->GetDesiredSize();
|
|
if (Orientation == Orient_Vertical)
|
|
{
|
|
ThisDesiredSize.X = FMath::Max(ChildDesiredSize.X + ThisSlot.GetPadding().GetTotalSpaceAlong<Orient_Horizontal>(), ThisDesiredSize.X);
|
|
ThisDesiredSize.Y += ChildDesiredSize.Y + ThisSlot.GetPadding().GetTotalSpaceAlong<Orient_Vertical>();
|
|
}
|
|
else
|
|
{
|
|
ThisDesiredSize.X += ChildDesiredSize.X + ThisSlot.GetPadding().GetTotalSpaceAlong<Orient_Horizontal>();
|
|
ThisDesiredSize.Y = FMath::Max(ChildDesiredSize.Y + ThisSlot.GetPadding().GetTotalSpaceAlong<Orient_Vertical>(), ThisDesiredSize.Y);
|
|
}
|
|
}
|
|
}
|
|
|
|
FVector2D::FReal ScrollPadding = Orientation == Orient_Vertical ? GetTickSpaceGeometry().GetLocalSize().Y : GetTickSpaceGeometry().GetLocalSize().X;
|
|
FVector2D::FReal& SizeSideToPad = Orientation == Orient_Vertical ? ThisDesiredSize.Y : ThisDesiredSize.X;
|
|
SizeSideToPad += BackPadScrolling ? ScrollPadding : 0;
|
|
SizeSideToPad += FrontPadScrolling ? ScrollPadding : 0;
|
|
|
|
return ThisDesiredSize;
|
|
}
|
|
|
|
SScrollBox::SScrollBox()
|
|
{
|
|
VerticalScrollBarSlot = nullptr;
|
|
bClippingProxy = true;
|
|
}
|
|
|
|
SScrollBox::~SScrollBox() = default;
|
|
|
|
void SScrollBox::Construct( const FArguments& InArgs )
|
|
{
|
|
check(InArgs._Style);
|
|
|
|
Style = InArgs._Style;
|
|
ScrollBarStyle = InArgs._ScrollBarStyle;
|
|
DesiredScrollOffset = 0;
|
|
bIsScrolling = false;
|
|
bAnimateScroll = false;
|
|
ScrollingAnimationInterpolationSpeed = InArgs._ScrollAnimationInterpSpeed;
|
|
AmountScrolledWhileRightMouseDown = 0;
|
|
PendingScrollTriggerAmount = 0;
|
|
bShowSoftwareCursor = false;
|
|
SoftwareCursorPosition = FVector2f::ZeroVector;
|
|
OnUserScrolled = InArgs._OnUserScrolled;
|
|
OnScrollBarVisibilityChanged = InArgs._OnScrollBarVisibilityChanged;
|
|
Orientation = InArgs._Orientation;
|
|
bScrollToEnd = false;
|
|
bIsScrollingActiveTimerRegistered = false;
|
|
bAllowsRightClickDragScrolling = false;
|
|
ConsumeMouseWheel = InArgs._ConsumeMouseWheel;
|
|
TickScrollDelta = 0;
|
|
AllowOverscroll = InArgs._AllowOverscroll;
|
|
BackPadScrolling = InArgs._BackPadScrolling;
|
|
FrontPadScrolling = InArgs._FrontPadScrolling;
|
|
bAnimateWheelScrolling = InArgs._AnimateWheelScrolling;
|
|
WheelScrollMultiplier = InArgs._WheelScrollMultiplier;
|
|
bEnableTouchScrolling = InArgs._EnableTouchScrolling;
|
|
bConsumePointerInput = InArgs._ConsumePointerInput;
|
|
NavigationScrollPadding = InArgs._NavigationScrollPadding;
|
|
NavigationDestination = InArgs._NavigationDestination;
|
|
ScrollWhenFocusChanges = InArgs._ScrollWhenFocusChanges;
|
|
OnScrollBoxFocusReceived = InArgs._OnFocusReceived;
|
|
OnScrollBoxFocusLost = InArgs._OnFocusLost;
|
|
bTouchPanningCapture = false;
|
|
bIsFocusable = false;
|
|
bVolatilityAlwaysInvalidatesPrepass = true;
|
|
|
|
if (InArgs._ExternalScrollbar.IsValid())
|
|
{
|
|
// An external scroll bar was specified by the user
|
|
ScrollBar = InArgs._ExternalScrollbar;
|
|
ScrollBar->SetOnUserScrolled(FOnUserScrolled::CreateSP(this, &SScrollBox::ScrollBar_OnUserScrolled));
|
|
ScrollBar->SetOnScrollBarVisibilityChanged(FOnScrollBarVisibilityChanged::CreateSP(this, &SScrollBox::ScrollBar_OnScrollBarVisibilityChanged));
|
|
bScrollBarIsExternal = true;
|
|
}
|
|
else
|
|
{
|
|
// Make a scroll bar
|
|
ScrollBar = ConstructScrollBar();
|
|
ScrollBar->SetDragFocusCause(InArgs._ScrollBarDragFocusCause);
|
|
ScrollBar->SetThickness(InArgs._ScrollBarThickness);
|
|
ScrollBar->SetUserVisibility(InArgs._ScrollBarVisibility);
|
|
ScrollBar->SetScrollBarAlwaysVisible(InArgs._ScrollBarAlwaysVisible);
|
|
ScrollBar->SetOnScrollBarVisibilityChanged(FOnScrollBarVisibilityChanged::CreateSP(this, &SScrollBox::ScrollBar_OnScrollBarVisibilityChanged));
|
|
ScrollBarSlotPadding = InArgs._ScrollBarPadding;
|
|
|
|
bScrollBarIsExternal = false;
|
|
}
|
|
|
|
SAssignNew(ScrollPanel, SScrollPanel, MoveTemp(const_cast<TArray<FSlot::FSlotArguments>&>(InArgs._Slots)))
|
|
.Clipping(InArgs._Clipping)
|
|
.Orientation(Orientation)
|
|
.BackPadScrolling(BackPadScrolling)
|
|
.FrontPadScrolling(FrontPadScrolling);
|
|
|
|
if (Orientation == Orient_Vertical)
|
|
{
|
|
ConstructVerticalLayout();
|
|
}
|
|
else
|
|
{
|
|
ConstructHorizontalLayout();
|
|
}
|
|
|
|
ScrollBar->SetState( 0.0f, 1.0f );
|
|
}
|
|
|
|
void SScrollBox::OnClippingChanged()
|
|
{
|
|
ScrollPanel->SetClipping(Clipping);
|
|
}
|
|
|
|
TSharedPtr<SScrollBar> SScrollBox::ConstructScrollBar()
|
|
{
|
|
return TSharedPtr<SScrollBar>(SNew(SScrollBar)
|
|
.Style(ScrollBarStyle)
|
|
.Orientation(Orientation)
|
|
.Padding(0.0f)
|
|
.OnUserScrolled(this, &SScrollBox::ScrollBar_OnUserScrolled));
|
|
}
|
|
|
|
void SScrollBox::ConstructVerticalLayout()
|
|
{
|
|
TSharedPtr<SHorizontalBox> PanelAndScrollbar;
|
|
this->ChildSlot
|
|
[
|
|
SAssignNew(PanelAndScrollbar, SHorizontalBox)
|
|
|
|
+ SHorizontalBox::Slot()
|
|
.FillWidth(1)
|
|
[
|
|
SNew(SOverlay)
|
|
|
|
+ SOverlay::Slot()
|
|
.Padding(Style->VerticalScrolledContentPadding)
|
|
[
|
|
// Scroll panel that presents the scrolled content
|
|
ScrollPanel.ToSharedRef()
|
|
]
|
|
|
|
+ SOverlay::Slot()
|
|
.HAlign(HAlign_Fill)
|
|
.VAlign(VAlign_Top)
|
|
[
|
|
// Shadow: Hint to scroll up
|
|
SNew(SImage)
|
|
.Visibility(EVisibility::HitTestInvisible)
|
|
.ColorAndOpacity(this, &SScrollBox::GetStartShadowOpacity)
|
|
.Image(&Style->TopShadowBrush)
|
|
]
|
|
|
|
+ SOverlay::Slot()
|
|
.HAlign(HAlign_Fill)
|
|
.VAlign(VAlign_Bottom)
|
|
[
|
|
// Shadow: a hint to scroll down
|
|
SNew(SImage)
|
|
.Visibility(EVisibility::HitTestInvisible)
|
|
.ColorAndOpacity(this, &SScrollBox::GetEndShadowOpacity)
|
|
.Image(&Style->BottomShadowBrush)
|
|
]
|
|
]
|
|
];
|
|
|
|
VerticalScrollBarSlot = nullptr;
|
|
if (!bScrollBarIsExternal)
|
|
{
|
|
PanelAndScrollbar->AddSlot()
|
|
.Padding(ScrollBarSlotPadding)
|
|
.AutoWidth()
|
|
.Expose(VerticalScrollBarSlot)
|
|
[
|
|
ScrollBar.ToSharedRef()
|
|
];
|
|
}
|
|
}
|
|
|
|
void SScrollBox::ConstructHorizontalLayout()
|
|
{
|
|
TSharedPtr<SVerticalBox> PanelAndScrollbar;
|
|
this->ChildSlot
|
|
[
|
|
SAssignNew(PanelAndScrollbar, SVerticalBox)
|
|
|
|
+ SVerticalBox::Slot()
|
|
.FillHeight(1)
|
|
[
|
|
SNew(SOverlay)
|
|
|
|
+ SOverlay::Slot()
|
|
.Padding(Style->HorizontalScrolledContentPadding)
|
|
[
|
|
// Scroll panel that presents the scrolled content
|
|
ScrollPanel.ToSharedRef()
|
|
]
|
|
|
|
+ SOverlay::Slot()
|
|
.HAlign(HAlign_Left)
|
|
.VAlign(VAlign_Fill)
|
|
[
|
|
// Shadow: Hint to left
|
|
SNew(SImage)
|
|
.Visibility(EVisibility::HitTestInvisible)
|
|
.ColorAndOpacity(this, &SScrollBox::GetStartShadowOpacity)
|
|
.Image(&Style->LeftShadowBrush)
|
|
]
|
|
|
|
+ SOverlay::Slot()
|
|
.HAlign(HAlign_Right)
|
|
.VAlign(VAlign_Fill)
|
|
[
|
|
// Shadow: a hint to scroll right
|
|
SNew(SImage)
|
|
.Visibility(EVisibility::HitTestInvisible)
|
|
.ColorAndOpacity(this, &SScrollBox::GetEndShadowOpacity)
|
|
.Image(&Style->RightShadowBrush)
|
|
]
|
|
]
|
|
];
|
|
|
|
HorizontalScrollBarSlot = nullptr;
|
|
if (!bScrollBarIsExternal)
|
|
{
|
|
PanelAndScrollbar->AddSlot()
|
|
.Padding(ScrollBarSlotPadding)
|
|
.AutoHeight()
|
|
.Expose(HorizontalScrollBarSlot)
|
|
[
|
|
ScrollBar.ToSharedRef()
|
|
];
|
|
}
|
|
}
|
|
|
|
/** Adds a slot to SScrollBox */
|
|
SScrollBox::FScopedWidgetSlotArguments SScrollBox::AddSlot()
|
|
{
|
|
return InsertSlot(INDEX_NONE);
|
|
}
|
|
|
|
SScrollBox::FScopedWidgetSlotArguments SScrollBox::InsertSlot(int32 Index)
|
|
{
|
|
return FScopedWidgetSlotArguments{ MakeUnique<FSlot>(), ScrollPanel->Children, Index };
|
|
}
|
|
|
|
const SScrollBox::FSlot& SScrollBox::GetSlot(int32 SlotIndex) const
|
|
{
|
|
check(ScrollPanel->Children.IsValidIndex(SlotIndex));
|
|
const FSlotBase& BaseSlot = static_cast<const FSlotBase&>(ScrollPanel->Children[SlotIndex]);
|
|
return static_cast<const FSlot&>(BaseSlot);
|
|
}
|
|
|
|
SScrollBox::FSlot& SScrollBox::GetSlot(int32 SlotIndex)
|
|
{
|
|
return const_cast<FSlot&>(const_cast<const SScrollBox*>(this)->GetSlot(SlotIndex));
|
|
}
|
|
|
|
void SScrollBox::RemoveSlot( const TSharedRef<SWidget>& WidgetToRemove )
|
|
{
|
|
ScrollPanel->Children.Remove(WidgetToRemove);
|
|
}
|
|
|
|
int32 SScrollBox::NumSlots() const
|
|
{
|
|
return ScrollPanel->Children.Num();
|
|
}
|
|
|
|
void SScrollBox::ClearChildren()
|
|
{
|
|
ScrollPanel->Children.Empty();
|
|
}
|
|
|
|
bool SScrollBox::IsRightClickScrolling() const
|
|
{
|
|
return FSlateApplication::IsInitialized() && AmountScrolledWhileRightMouseDown >= FSlateApplication::Get().GetDragTriggerDistance() && this->ScrollBar->IsNeeded();
|
|
}
|
|
|
|
float SScrollBox::GetScrollOffset() const
|
|
{
|
|
return DesiredScrollOffset;
|
|
}
|
|
|
|
float SScrollBox::GetOverscrollOffset() const
|
|
{
|
|
if (AllowOverscroll != EAllowOverscroll::Yes)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
const float ScrollOffset = ScrollPanel->PhysicalOffset;
|
|
if (ScrollOffset < 0.0f)
|
|
{
|
|
// Negative overscroll (before the start of the ScrollBox)
|
|
return ScrollOffset;
|
|
}
|
|
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
const float ViewSize = GetScrollComponentFromVector(CachedGeometry.GetLocalSize());
|
|
if (ScrollOffset > ContentSize - ViewSize)
|
|
{
|
|
// Positive overscroll (past the end of the ScrollBox)
|
|
return ScrollOffset - (ContentSize - ViewSize);
|
|
}
|
|
|
|
// No overscroll
|
|
return 0.0f;
|
|
}
|
|
|
|
float SScrollBox::GetOverscrollPercentage() const
|
|
{
|
|
if (AllowOverscroll != EAllowOverscroll::Yes)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
const float ViewSize = GetScrollComponentFromVector(CachedGeometry.GetLocalSize());
|
|
if (ViewSize <= 0.0f)
|
|
{
|
|
// Avoid division by zero
|
|
return 0.0f;
|
|
}
|
|
|
|
const float ScrollOffset = ScrollPanel->PhysicalOffset;
|
|
if (ScrollOffset < 0.0f)
|
|
{
|
|
// Negative overscroll (before the start of the ScrollBox)
|
|
return (ScrollOffset / ViewSize) * 100.0f;
|
|
}
|
|
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
if (ScrollOffset > ContentSize - ViewSize)
|
|
{
|
|
// Positive overscroll (past the end of the ScrollBox)
|
|
const float OverscrollAmount = ScrollOffset - (ContentSize - ViewSize);
|
|
return (OverscrollAmount / ViewSize) * 100.0f;
|
|
}
|
|
|
|
// No overscroll
|
|
return 0.0f;
|
|
}
|
|
|
|
float SScrollBox::GetScrollOffsetOfEnd() const
|
|
{
|
|
const FGeometry ScrollPanelGeometry = FindChildGeometry(CachedGeometry, ScrollPanel.ToSharedRef());
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
return FMath::Max(ContentSize - GetScrollComponentFromVector(ScrollPanelGeometry.Size), 0.0f);
|
|
}
|
|
|
|
float SScrollBox::GetViewFraction() const
|
|
{
|
|
const FGeometry ScrollPanelGeometry = FindChildGeometry(CachedGeometry, ScrollPanel.ToSharedRef());
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
|
|
return FMath::Clamp<float>(GetScrollComponentFromVector(CachedGeometry.GetLocalSize()) > 0 ? GetScrollComponentFromVector(ScrollPanelGeometry.Size) / ContentSize : 1, 0.0f, 1.0f);
|
|
}
|
|
|
|
float SScrollBox::GetViewOffsetFraction() const
|
|
{
|
|
const FGeometry ScrollPanelGeometry = FindChildGeometry(CachedGeometry, ScrollPanel.ToSharedRef());
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
|
|
const float ViewFraction = GetViewFraction();
|
|
return FMath::Clamp( DesiredScrollOffset/ContentSize, 0.f, 1.f - ViewFraction );
|
|
}
|
|
|
|
void SScrollBox::SetScrollOffset( float NewScrollOffset )
|
|
{
|
|
DesiredScrollOffset = NewScrollOffset;
|
|
bScrollToEnd = false;
|
|
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
}
|
|
|
|
void SScrollBox::ScrollToStart()
|
|
{
|
|
SetScrollOffset(0);
|
|
}
|
|
|
|
void SScrollBox::ScrollToEnd()
|
|
{
|
|
bScrollToEnd = true;
|
|
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
}
|
|
|
|
void SScrollBox::ScrollDescendantIntoView(const TSharedPtr<SWidget>& WidgetToScrollIntoView, bool InAnimateScroll, EDescendantScrollDestination InDestination, float InScrollPadding)
|
|
{
|
|
ScrollIntoViewRequest = [this, WidgetToScrollIntoView, InAnimateScroll, InDestination, InScrollPadding] (FGeometry AllottedGeometry) {
|
|
InternalScrollDescendantIntoView(AllottedGeometry, WidgetToScrollIntoView, InAnimateScroll, InDestination, InScrollPadding);
|
|
};
|
|
|
|
BeginInertialScrolling();
|
|
}
|
|
|
|
bool SScrollBox::InternalScrollDescendantIntoView(const FGeometry& MyGeometry, const TSharedPtr<SWidget>& WidgetToFind, bool InAnimateScroll, EDescendantScrollDestination InDestination, float InScrollPadding)
|
|
{
|
|
// We need to safely find the one WidgetToFind among our descendants.
|
|
TSet< TSharedRef<SWidget> > WidgetsToFind;
|
|
{
|
|
if (WidgetToFind.IsValid())
|
|
{
|
|
WidgetsToFind.Add(WidgetToFind.ToSharedRef());
|
|
}
|
|
}
|
|
TMap<TSharedRef<SWidget>, FArrangedWidget> Result;
|
|
|
|
FindChildGeometries( MyGeometry, WidgetsToFind, Result );
|
|
|
|
if (WidgetToFind.IsValid())
|
|
{
|
|
FArrangedWidget* WidgetGeometry = Result.Find(WidgetToFind.ToSharedRef());
|
|
if (!WidgetGeometry)
|
|
{
|
|
UE_LOG(LogSlate, Warning, TEXT("Unable to scroll to descendant as it's not a child of the scrollbox"));
|
|
}
|
|
else
|
|
{
|
|
float ScrollOffset = 0.0f;
|
|
if (InDestination == EDescendantScrollDestination::TopOrLeft)
|
|
{
|
|
// Calculate how much we would need to scroll to bring this to the top/left of the scroll box
|
|
const float WidgetPosition = GetScrollComponentFromVector(MyGeometry.AbsoluteToLocal(WidgetGeometry->Geometry.GetAbsolutePosition()));
|
|
const float MyPosition = InScrollPadding;
|
|
ScrollOffset = WidgetPosition - MyPosition;
|
|
}
|
|
else if (InDestination == EDescendantScrollDestination::BottomOrRight)
|
|
{
|
|
// Calculate how much we would need to scroll to bring this to the bottom/right of the scroll box
|
|
const float WidgetPosition = GetScrollComponentFromVector(MyGeometry.AbsoluteToLocal(WidgetGeometry->Geometry.GetAbsolutePosition() + WidgetGeometry->Geometry.GetAbsoluteSize()) - MyGeometry.GetLocalSize());
|
|
const float MyPosition = InScrollPadding;
|
|
ScrollOffset = WidgetPosition - MyPosition;
|
|
}
|
|
else if (InDestination == EDescendantScrollDestination::Center)
|
|
{
|
|
// Calculate how much we would need to scroll to bring this to the top/left of the scroll box
|
|
const float WidgetPosition = GetScrollComponentFromVector(MyGeometry.AbsoluteToLocal(WidgetGeometry->Geometry.GetAbsolutePosition()) + (WidgetGeometry->Geometry.GetLocalSize() / 2));
|
|
const float MyPosition = GetScrollComponentFromVector(MyGeometry.GetLocalSize() * FVector2f(0.5f, 0.5f));
|
|
ScrollOffset = WidgetPosition - MyPosition;
|
|
}
|
|
else
|
|
{
|
|
const float WidgetStartPosition = GetScrollComponentFromVector(MyGeometry.AbsoluteToLocal(WidgetGeometry->Geometry.GetAbsolutePosition()));
|
|
const float WidgetEndPosition = WidgetStartPosition + GetScrollComponentFromVector(WidgetGeometry->Geometry.GetLocalSize());
|
|
const float ViewStartPosition = InScrollPadding;
|
|
const float ViewEndPosition = GetScrollComponentFromVector(MyGeometry.GetLocalSize() - InScrollPadding);
|
|
|
|
const float ViewDelta = (ViewEndPosition - ViewStartPosition);
|
|
const float WidgetDelta = (WidgetEndPosition - WidgetStartPosition);
|
|
|
|
if (WidgetStartPosition < ViewStartPosition)
|
|
{
|
|
ScrollOffset = WidgetStartPosition - ViewStartPosition;
|
|
}
|
|
else if (WidgetEndPosition > ViewEndPosition)
|
|
{
|
|
ScrollOffset = (WidgetEndPosition - ViewDelta) - ViewStartPosition;
|
|
}
|
|
}
|
|
|
|
if (ScrollOffset != 0.0f)
|
|
{
|
|
DesiredScrollOffset = ScrollPanel->PhysicalOffset;
|
|
ScrollBy(MyGeometry, ScrollOffset, EAllowOverscroll::No, InAnimateScroll);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void SScrollBox::SetStyle(const FScrollBoxStyle* InStyle)
|
|
{
|
|
if (Style != InStyle)
|
|
{
|
|
Style = InStyle;
|
|
InvalidateStyle();
|
|
}
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarStyle(const FScrollBarStyle* InBarStyle)
|
|
{
|
|
if (InBarStyle != ScrollBarStyle)
|
|
{
|
|
ScrollBarStyle = InBarStyle;
|
|
if (!bScrollBarIsExternal && ScrollBar.IsValid())
|
|
{
|
|
ScrollBar->SetStyle(ScrollBarStyle);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SScrollBox::InvalidateStyle()
|
|
{
|
|
Invalidate(EInvalidateWidgetReason::Layout);
|
|
}
|
|
|
|
void SScrollBox::InvalidateScrollBarStyle()
|
|
{
|
|
if (ScrollBar.IsValid())
|
|
{
|
|
ScrollBar->InvalidateStyle();
|
|
}
|
|
}
|
|
|
|
EOrientation SScrollBox::GetOrientation()
|
|
{
|
|
return Orientation;
|
|
}
|
|
|
|
void SScrollBox::SetNavigationDestination(const EDescendantScrollDestination NewNavigationDestination)
|
|
{
|
|
NavigationDestination = NewNavigationDestination;
|
|
}
|
|
|
|
void SScrollBox::SetConsumeMouseWheel(EConsumeMouseWheel NewConsumeMouseWheel)
|
|
{
|
|
ConsumeMouseWheel = NewConsumeMouseWheel;
|
|
}
|
|
|
|
void SScrollBox::SetAnalogMouseWheelKey(FKey InAnalogKey)
|
|
{
|
|
ensure(!InAnalogKey.IsValid() || InAnalogKey.IsAnalog());
|
|
AnalogMouseWheelKey = InAnalogKey;
|
|
}
|
|
|
|
void SScrollBox::SetIsFocusable(bool bInIsFocusable)
|
|
{
|
|
bIsFocusable = bInIsFocusable;
|
|
}
|
|
|
|
void SScrollBox::SetOrientation(EOrientation InOrientation)
|
|
{
|
|
if (Orientation != InOrientation)
|
|
{
|
|
Orientation = InOrientation;
|
|
if (!bScrollBarIsExternal)
|
|
{
|
|
ScrollBar = ConstructScrollBar();
|
|
}
|
|
ScrollPanel->SetOrientation(Orientation);
|
|
if (Orientation == Orient_Vertical)
|
|
{
|
|
ConstructVerticalLayout();
|
|
}
|
|
else
|
|
{
|
|
ConstructHorizontalLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarVisibility(EVisibility InVisibility)
|
|
{
|
|
ScrollBar->SetUserVisibility(InVisibility);
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarAlwaysVisible(bool InAlwaysVisible)
|
|
{
|
|
ScrollBar->SetScrollBarAlwaysVisible(InAlwaysVisible);
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarTrackAlwaysVisible(bool InAlwaysVisible)
|
|
{
|
|
ScrollBar->SetScrollBarTrackAlwaysVisible(InAlwaysVisible);
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarThickness(UE::Slate::FDeprecateVector2DParameter InThickness)
|
|
{
|
|
ScrollBar->SetThickness(InThickness);
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarPadding(const FMargin& InPadding)
|
|
{
|
|
ScrollBarSlotPadding = InPadding;
|
|
|
|
if (Orientation == Orient_Vertical)
|
|
{
|
|
if (VerticalScrollBarSlot)
|
|
{
|
|
VerticalScrollBarSlot->SetPadding(ScrollBarSlotPadding);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (HorizontalScrollBarSlot)
|
|
{
|
|
HorizontalScrollBarSlot->SetPadding(ScrollBarSlotPadding);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SScrollBox::SetScrollBarRightClickDragAllowed(bool bIsAllowed)
|
|
{
|
|
bAllowsRightClickDragScrolling = bIsAllowed;
|
|
}
|
|
|
|
EActiveTimerReturnType SScrollBox::UpdateInertialScroll(double InCurrentTime, float InDeltaTime)
|
|
{
|
|
bool bKeepTicking = bIsScrolling;
|
|
|
|
if ( bIsScrolling )
|
|
{
|
|
InertialScrollManager.UpdateScrollVelocity(InDeltaTime);
|
|
const float ScrollVelocityLocal = InertialScrollManager.GetScrollVelocity() / CachedGeometry.Scale;
|
|
|
|
if (ScrollVelocityLocal != 0.f )
|
|
{
|
|
if ( CanUseInertialScroll(ScrollVelocityLocal) )
|
|
{
|
|
bKeepTicking = true;
|
|
ScrollBy(CachedGeometry, ScrollVelocityLocal * InDeltaTime, AllowOverscroll, false);
|
|
}
|
|
else
|
|
{
|
|
InertialScrollManager.ClearScrollVelocity();
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( AllowOverscroll == EAllowOverscroll::Yes )
|
|
{
|
|
// If we are currently in overscroll, the list will need refreshing.
|
|
// Do this before UpdateOverscroll, as that could cause GetOverscroll() to be 0
|
|
if ( Overscroll.GetOverscroll(CachedGeometry) != 0.0f )
|
|
{
|
|
bKeepTicking = true;
|
|
}
|
|
|
|
Overscroll.UpdateOverscroll(InDeltaTime);
|
|
}
|
|
|
|
TickScrollDelta = 0.f;
|
|
|
|
if ( !bKeepTicking )
|
|
{
|
|
bIsScrolling = false;
|
|
bIsScrollingActiveTimerRegistered = false;
|
|
Invalidate(EInvalidateWidget::LayoutAndVolatility);
|
|
UpdateInertialScrollHandle.Reset();
|
|
}
|
|
|
|
return bKeepTicking ? EActiveTimerReturnType::Continue : EActiveTimerReturnType::Stop;
|
|
}
|
|
|
|
void SScrollBox::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
|
|
{
|
|
CachedGeometry = AllottedGeometry;
|
|
|
|
if ( bTouchPanningCapture && (FSlateApplication::Get().GetCurrentTime() - LastScrollTime) > 0.10 )
|
|
{
|
|
InertialScrollManager.ClearScrollVelocity();
|
|
}
|
|
|
|
// If we needed a widget to be scrolled into view, make that happen.
|
|
if ( ScrollIntoViewRequest )
|
|
{
|
|
ScrollIntoViewRequest(AllottedGeometry);
|
|
ScrollIntoViewRequest = nullptr;
|
|
}
|
|
|
|
const FGeometry ScrollPanelGeometry = FindChildGeometry( AllottedGeometry, ScrollPanel.ToSharedRef() );
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
|
|
if ( bScrollToEnd )
|
|
{
|
|
DesiredScrollOffset = FMath::Max(ContentSize - GetScrollComponentFromVector(ScrollPanelGeometry.GetLocalSize()), 0.0f);
|
|
bScrollToEnd = false;
|
|
}
|
|
|
|
// If this scroll box has no size, do not compute a view fraction because it will be wrong and causes pop in when the size is available
|
|
const float ViewFraction = GetViewFraction();
|
|
const float TargetViewOffset = GetViewOffsetFraction();
|
|
|
|
const float CurrentViewOffset = bAnimateScroll ? FMath::FInterpTo(ScrollBar->DistanceFromTop(), TargetViewOffset, InDeltaTime, ScrollingAnimationInterpolationSpeed) : TargetViewOffset;
|
|
|
|
// Update the scrollbar with the clamped version of the offset
|
|
float NewPhysicalOffset = GetScrollComponentFromVector(CurrentViewOffset * ScrollPanel->GetDesiredSize());
|
|
if ( AllowOverscroll == EAllowOverscroll::Yes )
|
|
{
|
|
NewPhysicalOffset += Overscroll.GetOverscroll(AllottedGeometry);
|
|
}
|
|
|
|
const bool bWasScrolling = bIsScrolling;
|
|
bIsScrolling = !FMath::IsNearlyEqual(NewPhysicalOffset, ScrollPanel->PhysicalOffset, 0.001f);
|
|
|
|
ScrollPanel->PhysicalOffset = NewPhysicalOffset;
|
|
|
|
if (bWasScrolling && !bIsScrolling)
|
|
{
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
}
|
|
|
|
ScrollBar->SetState(CurrentViewOffset, ViewFraction);
|
|
if (!ScrollBar->IsNeeded())
|
|
{
|
|
// We cannot scroll, so ensure that there is no offset.
|
|
ScrollPanel->PhysicalOffset = 0.0f;
|
|
}
|
|
}
|
|
|
|
bool SScrollBox::ComputeVolatility() const
|
|
{
|
|
return bIsScrolling || IsRightClickScrolling();
|
|
}
|
|
|
|
FReply SScrollBox::OnPreviewMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
|
|
{
|
|
if (bEnableTouchScrolling && MouseEvent.IsTouchEvent() && !bFingerOwningTouchInteraction.IsSet())
|
|
{
|
|
// Clear any inertia
|
|
InertialScrollManager.ClearScrollVelocity();
|
|
// We have started a new interaction; track how far the user has moved since they put their finger down.
|
|
AmountScrolledWhileRightMouseDown = 0;
|
|
PendingScrollTriggerAmount = 0;
|
|
// Someone put their finger down in this list, so they probably want to drag the list.
|
|
bFingerOwningTouchInteraction = MouseEvent.GetPointerIndex();
|
|
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
}
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SScrollBox::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
|
|
{
|
|
if ( !bFingerOwningTouchInteraction.IsSet() )
|
|
{
|
|
EndInertialScrolling();
|
|
}
|
|
|
|
if ( MouseEvent.IsTouchEvent() )
|
|
{
|
|
return bConsumePointerInput ? FReply::Handled() : SWidget::OnMouseButtonDown(MyGeometry, MouseEvent);
|
|
}
|
|
else
|
|
{
|
|
if ( MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && ScrollBar->IsNeeded() && bAllowsRightClickDragScrolling)
|
|
{
|
|
AmountScrolledWhileRightMouseDown = 0;
|
|
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
|
|
return bConsumePointerInput ? FReply::Handled() : SWidget::OnMouseButtonDown(MyGeometry, MouseEvent);
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SScrollBox::OnMouseButtonUp( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
|
|
{
|
|
if ( MouseEvent.GetEffectingButton() == EKeys::RightMouseButton && bAllowsRightClickDragScrolling)
|
|
{
|
|
if ( !bIsScrollingActiveTimerRegistered && IsRightClickScrolling() )
|
|
{
|
|
// Register the active timer to handle the inertial scrolling
|
|
CachedGeometry = MyGeometry;
|
|
BeginInertialScrolling();
|
|
}
|
|
|
|
AmountScrolledWhileRightMouseDown = 0;
|
|
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
|
|
FReply Reply = bConsumePointerInput ? FReply::Handled().ReleaseMouseCapture() : SWidget::OnMouseButtonUp(MyGeometry, MouseEvent).ReleaseMouseCapture();
|
|
bShowSoftwareCursor = false;
|
|
|
|
// If we have mouse capture, snap the mouse back to the closest location that is within the panel's bounds
|
|
if ( HasMouseCapture() )
|
|
{
|
|
FSlateRect PanelScreenSpaceRect = MyGeometry.GetLayoutBoundingRect();
|
|
FVector2f CursorPosition = MyGeometry.LocalToAbsolute( SoftwareCursorPosition );
|
|
|
|
FIntPoint BestPositionInPanel(
|
|
FMath::RoundToInt( FMath::Clamp( CursorPosition.X, PanelScreenSpaceRect.Left, PanelScreenSpaceRect.Right ) ),
|
|
FMath::RoundToInt( FMath::Clamp( CursorPosition.Y, PanelScreenSpaceRect.Top, PanelScreenSpaceRect.Bottom ) )
|
|
);
|
|
|
|
Reply.SetMousePos(BestPositionInPanel);
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SScrollBox::OnMouseMove( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
|
|
{
|
|
const float ScrollByAmountScreen = GetScrollComponentFromVector(MouseEvent.GetCursorDelta());
|
|
const float ScrollByAmountLocal = ScrollByAmountScreen / MyGeometry.Scale;
|
|
|
|
if ( MouseEvent.IsTouchEvent() )
|
|
{
|
|
FReply Reply = FReply::Unhandled();
|
|
|
|
if ( !bTouchPanningCapture )
|
|
{
|
|
if ( bFingerOwningTouchInteraction.IsSet() && MouseEvent.IsTouchEvent() && !HasMouseCapture() )
|
|
{
|
|
PendingScrollTriggerAmount += ScrollByAmountScreen;
|
|
|
|
if ( FMath::Abs(PendingScrollTriggerAmount) > FSlateApplication::Get().GetDragTriggerDistance() )
|
|
{
|
|
bTouchPanningCapture = true;
|
|
ScrollBar->BeginScrolling();
|
|
|
|
// The user has moved the list some amount; they are probably
|
|
// trying to scroll. From now on, the list assumes the user is scrolling
|
|
// until they lift their finger.
|
|
Reply = bConsumePointerInput ? FReply::Handled().CaptureMouse(AsShared()) : SWidget::OnMouseMove(MyGeometry, MouseEvent).CaptureMouse(AsShared()) ;
|
|
}
|
|
else
|
|
{
|
|
Reply = bConsumePointerInput ? FReply::Handled() : SWidget::OnMouseMove(MyGeometry, MouseEvent);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if ( bFingerOwningTouchInteraction.IsSet() && HasMouseCaptureByUser(MouseEvent.GetUserIndex(), MouseEvent.GetPointerIndex()) )
|
|
{
|
|
LastScrollTime = FSlateApplication::Get().GetCurrentTime();
|
|
InertialScrollManager.AddScrollSample(-ScrollByAmountScreen, FSlateApplication::Get().GetCurrentTime());
|
|
ScrollBy(MyGeometry, -ScrollByAmountLocal, EAllowOverscroll::Yes, false);
|
|
|
|
Reply = bConsumePointerInput ? FReply::Handled() : SWidget::OnMouseMove(MyGeometry, MouseEvent);
|
|
}
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
else
|
|
{
|
|
if ( MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton) && bAllowsRightClickDragScrolling)
|
|
{
|
|
// If scrolling with the right mouse button, we need to remember how much we scrolled.
|
|
// If we did not scroll at all, we will bring up the context menu when the mouse is released.
|
|
AmountScrolledWhileRightMouseDown += FMath::Abs(ScrollByAmountScreen);
|
|
|
|
// Has the mouse moved far enough with the right mouse button held down to start capturing
|
|
// the mouse and dragging the view?
|
|
if ( IsRightClickScrolling() )
|
|
{
|
|
InertialScrollManager.AddScrollSample(-ScrollByAmountScreen, FPlatformTime::Seconds());
|
|
const bool bDidScroll = ScrollBy(MyGeometry, -ScrollByAmountLocal, AllowOverscroll, false);
|
|
|
|
FReply Reply = bConsumePointerInput ? FReply::Handled() : SWidget::OnMouseMove(MyGeometry, MouseEvent);
|
|
|
|
// Capture the mouse if we need to
|
|
if ( HasMouseCapture() == false )
|
|
{
|
|
Reply.CaptureMouse(AsShared()).UseHighPrecisionMouseMovement(AsShared());
|
|
SoftwareCursorPosition = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition());
|
|
bShowSoftwareCursor = true;
|
|
}
|
|
|
|
// Check if the mouse has moved.
|
|
if ( bDidScroll )
|
|
{
|
|
SetScrollComponentOnVector(SoftwareCursorPosition, GetScrollComponentFromVector(SoftwareCursorPosition) + ScrollByAmountLocal);
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SScrollBox::OnMouseEnter( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
|
|
{
|
|
if (bEnableTouchScrolling && MouseEvent.IsTouchEvent())
|
|
{
|
|
if ( !bFingerOwningTouchInteraction.IsSet() )
|
|
{
|
|
// If we currently do not have touch capture, allow this widget to begin scrolling on pointer enter events
|
|
// if it comes from a child widget
|
|
if ( MyGeometry.IsUnderLocation(MouseEvent.GetLastScreenSpacePosition()) )
|
|
{
|
|
bFingerOwningTouchInteraction = MouseEvent.GetPointerIndex();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SScrollBox::OnMouseLeave( const FPointerEvent& MouseEvent )
|
|
{
|
|
if ( HasMouseCapture() == false )
|
|
{
|
|
// No longer scrolling (unless we have mouse capture)
|
|
if ( AmountScrolledWhileRightMouseDown != 0 )
|
|
{
|
|
AmountScrolledWhileRightMouseDown = 0;
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
}
|
|
|
|
if ( MouseEvent.IsTouchEvent() )
|
|
{
|
|
bFingerOwningTouchInteraction.Reset();
|
|
}
|
|
}
|
|
}
|
|
|
|
FReply SScrollBox::OnMouseWheel( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
|
|
{
|
|
if ((ScrollBar->IsNeeded() && ConsumeMouseWheel != EConsumeMouseWheel::Never) || ConsumeMouseWheel == EConsumeMouseWheel::Always)
|
|
{
|
|
// Make sure scroll velocity is cleared so it doesn't fight with the mouse wheel input
|
|
InertialScrollManager.ClearScrollVelocity();
|
|
|
|
const bool bScrollWasHandled = ScrollBy(MyGeometry, -MouseEvent.GetWheelDelta() * GetGlobalScrollAmount() * WheelScrollMultiplier, EAllowOverscroll::No, bAnimateWheelScrolling);
|
|
|
|
if ( bScrollWasHandled && !bIsScrollingActiveTimerRegistered )
|
|
{
|
|
// Register the active timer to handle the inertial scrolling
|
|
CachedGeometry = MyGeometry;
|
|
BeginInertialScrolling();
|
|
}
|
|
|
|
return bScrollWasHandled ? FReply::Handled() : FReply::Unhandled();
|
|
}
|
|
else
|
|
{
|
|
return FReply::Unhandled();
|
|
}
|
|
}
|
|
|
|
FReply SScrollBox::OnAnalogValueChanged(const FGeometry& MyGeometry, const FAnalogInputEvent& InAnalogInputEvent)
|
|
{
|
|
const float AnalogInputThreshold = 0.2f;
|
|
if ((ScrollBar->IsNeeded() && ConsumeMouseWheel != EConsumeMouseWheel::Never) || ConsumeMouseWheel == EConsumeMouseWheel::Always)
|
|
{
|
|
const FKey& AnalogKey = InAnalogInputEvent.GetKey();
|
|
const float AnalogValue = InAnalogInputEvent.GetAnalogValue();
|
|
|
|
// If not the correct input, or not within input thresholds, ignore.
|
|
if (AnalogKey != AnalogMouseWheelKey || (AnalogValue < AnalogInputThreshold && AnalogValue > -AnalogInputThreshold))
|
|
{
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
// Make sure scroll velocity is cleared so it doesn't fight with the mouse wheel input
|
|
InertialScrollManager.ClearScrollVelocity();
|
|
|
|
const float ScrollAmount = GetGlobalScrollAmount() * GetGlobalGamepadScrollMultiplier() * WheelScrollMultiplier;
|
|
const bool bScrollWasHandled = ScrollBy(MyGeometry, -AnalogValue * ScrollAmount, EAllowOverscroll::No, bAnimateWheelScrolling);
|
|
|
|
if ( bScrollWasHandled && !bIsScrollingActiveTimerRegistered )
|
|
{
|
|
// Register the active timer to handle the inertial scrolling
|
|
CachedGeometry = MyGeometry;
|
|
BeginInertialScrolling();
|
|
}
|
|
|
|
return bScrollWasHandled ? FReply::Handled() : FReply::Unhandled();
|
|
}
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
bool SScrollBox::ScrollBy(const FGeometry& AllottedGeometry, float LocalScrollAmount, EAllowOverscroll Overscrolling, bool InAnimateScroll)
|
|
{
|
|
Invalidate(EInvalidateWidget::LayoutAndVolatility);
|
|
|
|
bAnimateScroll = InAnimateScroll;
|
|
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
const FGeometry ScrollPanelGeometry = FindChildGeometry( AllottedGeometry, ScrollPanel.ToSharedRef() );
|
|
|
|
const float PreviousScrollOffset = DesiredScrollOffset;
|
|
|
|
if (LocalScrollAmount != 0 )
|
|
{
|
|
const float ScrollMin = 0.0f;
|
|
const float ScrollMax = FMath::Max(ContentSize - GetScrollComponentFromVector(ScrollPanelGeometry.GetLocalSize()), 0.0f);
|
|
|
|
if ( AllowOverscroll == EAllowOverscroll::Yes && Overscrolling == EAllowOverscroll::Yes && Overscroll.ShouldApplyOverscroll(DesiredScrollOffset == 0, DesiredScrollOffset == ScrollMax, LocalScrollAmount) )
|
|
{
|
|
Overscroll.ScrollBy(AllottedGeometry, LocalScrollAmount);
|
|
}
|
|
else
|
|
{
|
|
DesiredScrollOffset = FMath::Clamp(DesiredScrollOffset + LocalScrollAmount, ScrollMin, ScrollMax);
|
|
}
|
|
}
|
|
|
|
OnUserScrolled.ExecuteIfBound(DesiredScrollOffset);
|
|
|
|
return ConsumeMouseWheel == EConsumeMouseWheel::Always || DesiredScrollOffset != PreviousScrollOffset;
|
|
}
|
|
|
|
FCursorReply SScrollBox::OnCursorQuery( const FGeometry& MyGeometry, const FPointerEvent& CursorEvent ) const
|
|
{
|
|
if ( IsRightClickScrolling() )
|
|
{
|
|
// We hide the native cursor as we'll be drawing the software EMouseCursor::GrabHandClosed cursor
|
|
return FCursorReply::Cursor( EMouseCursor::None );
|
|
}
|
|
else
|
|
{
|
|
return FCursorReply::Unhandled();
|
|
}
|
|
}
|
|
|
|
FReply SScrollBox::OnTouchEnded(const FGeometry& MyGeometry, const FPointerEvent& InTouchEvent)
|
|
{
|
|
CachedGeometry = MyGeometry;
|
|
|
|
if ( HasMouseCaptureByUser(InTouchEvent.GetUserIndex(), InTouchEvent.GetPointerIndex()) )
|
|
{
|
|
ScrollBar->EndScrolling();
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
|
|
BeginInertialScrolling();
|
|
|
|
return bConsumePointerInput ? FReply::Handled().ReleaseMouseCapture() : SWidget::OnTouchEnded(MyGeometry, InTouchEvent).ReleaseMouseCapture() ;
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SScrollBox::OnMouseCaptureLost(const FCaptureLostEvent& CaptureLostEvent)
|
|
{
|
|
SCompoundWidget::OnMouseCaptureLost(CaptureLostEvent);
|
|
AmountScrolledWhileRightMouseDown = 0;
|
|
PendingScrollTriggerAmount = 0;
|
|
bFingerOwningTouchInteraction.Reset();
|
|
bTouchPanningCapture = false;
|
|
|
|
}
|
|
|
|
FNavigationReply SScrollBox::OnNavigation(const FGeometry& MyGeometry, const FNavigationEvent& InNavigationEvent)
|
|
{
|
|
TSharedPtr<SWidget> FocusedChild;
|
|
int32 FocusedChildIndex = -1;
|
|
int32 FocusedChildDirection = 0;
|
|
|
|
// Find the child with focus currently so that we can find the next logical child we're going to move to.
|
|
TPanelChildren<SScrollBox::FSlot>& Children = ScrollPanel->Children;
|
|
for ( int32 SlotIndex=0; SlotIndex < Children.Num(); ++SlotIndex )
|
|
{
|
|
if ( Children[SlotIndex].GetWidget()->HasUserFocus(InNavigationEvent.GetUserIndex()).IsSet() ||
|
|
Children[SlotIndex].GetWidget()->HasUserFocusedDescendants(InNavigationEvent.GetUserIndex()) )
|
|
{
|
|
FocusedChild = Children[SlotIndex].GetWidget();
|
|
FocusedChildIndex = SlotIndex;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( FocusedChild.IsValid() )
|
|
{
|
|
if ( Orientation == Orient_Vertical )
|
|
{
|
|
switch ( InNavigationEvent.GetNavigationType() )
|
|
{
|
|
case EUINavigation::Up:
|
|
FocusedChildDirection = -1;
|
|
break;
|
|
case EUINavigation::Down:
|
|
FocusedChildDirection = 1;
|
|
break;
|
|
default:
|
|
// If we don't handle this direction in our current orientation we can
|
|
// just allow the behavior of the boundary rule take over.
|
|
return SCompoundWidget::OnNavigation(MyGeometry, InNavigationEvent);
|
|
}
|
|
}
|
|
else // Orient_Horizontal
|
|
{
|
|
switch ( InNavigationEvent.GetNavigationType() )
|
|
{
|
|
case EUINavigation::Left:
|
|
FocusedChildDirection = -1;
|
|
break;
|
|
case EUINavigation::Right:
|
|
FocusedChildDirection = 1;
|
|
break;
|
|
default:
|
|
// If we don't handle this direction in our current orientation we can
|
|
// just allow the behavior of the boundary rule take over.
|
|
return SCompoundWidget::OnNavigation(MyGeometry, InNavigationEvent);
|
|
}
|
|
}
|
|
|
|
// If the focused child index is in a valid range we know we can successfully focus
|
|
// the new child we're moving focus to.
|
|
if ( FocusedChildDirection != 0 )
|
|
{
|
|
TSharedPtr<SWidget> NextFocusableChild;
|
|
|
|
// Search in the direction we need to move for the next focusable child of the scrollbox.
|
|
for ( int32 ChildIndex = FocusedChildIndex + FocusedChildDirection; ChildIndex >= 0 && ChildIndex < Children.Num(); ChildIndex += FocusedChildDirection )
|
|
{
|
|
TSharedPtr<SWidget> PossiblyFocusableChild = GetKeyboardFocusableWidget(Children[ChildIndex].GetWidget());
|
|
if ( PossiblyFocusableChild.IsValid() )
|
|
{
|
|
NextFocusableChild = PossiblyFocusableChild;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we found a focusable child, scroll to it, and shift focus.
|
|
if ( NextFocusableChild.IsValid() )
|
|
{
|
|
InternalScrollDescendantIntoView(MyGeometry, NextFocusableChild, false, NavigationDestination, NavigationScrollPadding);
|
|
return FNavigationReply::Explicit(NextFocusableChild);
|
|
}
|
|
}
|
|
}
|
|
|
|
return SCompoundWidget::OnNavigation(MyGeometry, InNavigationEvent);
|
|
}
|
|
|
|
FReply SScrollBox::OnFocusReceived(const FGeometry& MyGeometry, const FFocusEvent& InFocusEvent)
|
|
{
|
|
if (OnScrollBoxFocusReceived.IsBound())
|
|
{
|
|
OnScrollBoxFocusReceived.ExecuteIfBound();
|
|
return FReply::Handled();
|
|
}
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SScrollBox::OnFocusLost(const FFocusEvent& InFocusEvent)
|
|
{
|
|
OnScrollBoxFocusLost.ExecuteIfBound();
|
|
}
|
|
|
|
void SScrollBox::OnFocusChanging(const FWeakWidgetPath& PreviousFocusPath, const FWidgetPath& NewWidgetPath, const FFocusEvent& InFocusEvent)
|
|
{
|
|
if (ScrollWhenFocusChanges != EScrollWhenFocusChanges::NoScroll)
|
|
{
|
|
if (NewWidgetPath.IsValid() && NewWidgetPath.ContainsWidget(this))
|
|
{
|
|
ScrollDescendantIntoView(NewWidgetPath.GetLastWidget(), ScrollWhenFocusChanges == EScrollWhenFocusChanges::AnimatedScroll ? true : false, NavigationDestination, NavigationScrollPadding);
|
|
}
|
|
}
|
|
}
|
|
|
|
TSharedPtr<SWidget> SScrollBox::GetKeyboardFocusableWidget(TSharedPtr<SWidget> InWidget)
|
|
{
|
|
if (EVisibility::DoesVisibilityPassFilter(InWidget->GetVisibility(), EVisibility::Visible))
|
|
{
|
|
if (InWidget->SupportsKeyboardFocus())
|
|
{
|
|
return InWidget;
|
|
}
|
|
else
|
|
{
|
|
FChildren* Children = InWidget->GetChildren();
|
|
for (int32 i = 0; i < Children->Num(); ++i)
|
|
{
|
|
TSharedPtr<SWidget> ChildWidget = Children->GetChildAt(i);
|
|
TSharedPtr<SWidget> FoucusableWidget = GetKeyboardFocusableWidget(ChildWidget);
|
|
if (FoucusableWidget.IsValid() && EVisibility::DoesVisibilityPassFilter(FoucusableWidget->GetVisibility(), EVisibility::Visible))
|
|
{
|
|
return FoucusableWidget;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int32 SScrollBox::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const
|
|
{
|
|
int32 NewLayerId = SCompoundWidget::OnPaint( Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled );
|
|
|
|
if( !bShowSoftwareCursor )
|
|
{
|
|
return NewLayerId;
|
|
}
|
|
|
|
const FSlateBrush* Brush = FCoreStyle::Get().GetBrush(TEXT("SoftwareCursor_Grab"));
|
|
const FVector2f CursorSize = Brush->ImageSize / AllottedGeometry.Scale;
|
|
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements,
|
|
++NewLayerId,
|
|
AllottedGeometry.ToPaintGeometry( CursorSize, FSlateLayoutTransform(SoftwareCursorPosition - (CursorSize *.5f )) ),
|
|
Brush
|
|
);
|
|
|
|
return NewLayerId;
|
|
}
|
|
|
|
void SScrollBox::ScrollBar_OnUserScrolled( float InScrollOffsetFraction )
|
|
{
|
|
bAnimateScroll = false;
|
|
|
|
const float ContentSize = GetScrollComponentFromVector(ScrollPanel->GetDesiredSize());
|
|
const FGeometry ScrollPanelGeometry = FindChildGeometry(CachedGeometry, ScrollPanel.ToSharedRef());
|
|
|
|
// Clamp to max scroll offset
|
|
DesiredScrollOffset = FMath::Min(InScrollOffsetFraction * ContentSize, ContentSize - GetScrollComponentFromVector(ScrollPanelGeometry.GetLocalSize()));
|
|
OnUserScrolled.ExecuteIfBound(DesiredScrollOffset);
|
|
|
|
Invalidate(EInvalidateWidget::Layout);
|
|
}
|
|
|
|
void SScrollBox::ScrollBar_OnScrollBarVisibilityChanged( EVisibility NewVisibility )
|
|
{
|
|
OnScrollBarVisibilityChanged.ExecuteIfBound(NewVisibility);
|
|
}
|
|
|
|
const float ShadowFadeDistance = 32.0f;
|
|
|
|
FSlateColor SScrollBox::GetStartShadowOpacity() const
|
|
{
|
|
// The shadow should only be visible when the user needs a hint that they can scroll up.
|
|
const float ShadowOpacity = FMath::Clamp( ScrollPanel->PhysicalOffset/ShadowFadeDistance, 0.0f, 1.0f);
|
|
|
|
return FLinearColor(1.0f, 1.0f, 1.0f, ShadowOpacity);
|
|
}
|
|
|
|
FSlateColor SScrollBox::GetEndShadowOpacity() const
|
|
{
|
|
// The shadow should only be visible when the user needs a hint that they can scroll down.
|
|
const float ShadowOpacity = (ScrollBar->DistanceFromBottom() * GetScrollComponentFromVector(ScrollPanel->GetDesiredSize()) / ShadowFadeDistance);
|
|
|
|
return FLinearColor(1.0f, 1.0f, 1.0f, ShadowOpacity);
|
|
}
|
|
|
|
bool SScrollBox::CanUseInertialScroll(float ScrollAmount) const
|
|
{
|
|
const auto CurrentOverscroll = Overscroll.GetOverscroll(CachedGeometry);
|
|
|
|
// We allow sampling for the inertial scroll if we are not in the overscroll region,
|
|
// Or if we are scrolling outwards of the overscroll region
|
|
return CurrentOverscroll == 0.f || FMath::Sign(CurrentOverscroll) != FMath::Sign(ScrollAmount);
|
|
}
|
|
|
|
EAllowOverscroll SScrollBox::GetAllowOverscroll() const
|
|
{
|
|
return AllowOverscroll;
|
|
}
|
|
|
|
void SScrollBox::SetAllowOverscroll(EAllowOverscroll NewAllowOverscroll)
|
|
{
|
|
AllowOverscroll = NewAllowOverscroll;
|
|
|
|
if (AllowOverscroll == EAllowOverscroll::No)
|
|
{
|
|
Overscroll.ResetOverscroll();
|
|
}
|
|
}
|
|
|
|
void SScrollBox::SetAnimateWheelScrolling(bool bInAnimateWheelScrolling)
|
|
{
|
|
bAnimateWheelScrolling = bInAnimateWheelScrolling;
|
|
}
|
|
|
|
void SScrollBox::SetScrollingAnimationInterpolationSpeed(float NewScrollingAnimationInterpolationSpeed)
|
|
{
|
|
ScrollingAnimationInterpolationSpeed = NewScrollingAnimationInterpolationSpeed;
|
|
}
|
|
|
|
void SScrollBox::SetWheelScrollMultiplier(float NewWheelScrollMultiplier)
|
|
{
|
|
WheelScrollMultiplier = NewWheelScrollMultiplier;
|
|
}
|
|
|
|
void SScrollBox::SetIsTouchScrollingEnabled(const bool bInEnableTouchScrolling)
|
|
{
|
|
bEnableTouchScrolling = bInEnableTouchScrolling;
|
|
ensureMsgf(!bFingerOwningTouchInteraction.IsSet(), TEXT("TouchScrollingEnabled flag should not be changed while scrolling."));
|
|
}
|
|
|
|
void SScrollBox::SetScrollWhenFocusChanges(EScrollWhenFocusChanges NewScrollWhenFocusChanges)
|
|
{
|
|
ScrollWhenFocusChanges = NewScrollWhenFocusChanges;
|
|
}
|
|
|
|
void SScrollBox::BeginInertialScrolling()
|
|
{
|
|
if ( !UpdateInertialScrollHandle.IsValid() )
|
|
{
|
|
bIsScrolling = true;
|
|
bIsScrollingActiveTimerRegistered = true;
|
|
UpdateInertialScrollHandle = RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SScrollBox::UpdateInertialScroll));
|
|
Invalidate(EInvalidateWidget::LayoutAndVolatility);
|
|
}
|
|
}
|
|
|
|
void SScrollBox::EndInertialScrolling()
|
|
{
|
|
bIsScrolling = false;
|
|
bIsScrollingActiveTimerRegistered = false;
|
|
Invalidate(EInvalidateWidget::LayoutAndVolatility);
|
|
if ( UpdateInertialScrollHandle.IsValid() )
|
|
{
|
|
UnRegisterActiveTimer(UpdateInertialScrollHandle.ToSharedRef());
|
|
UpdateInertialScrollHandle.Reset();
|
|
}
|
|
|
|
// Zero the scroll velocity so the panel stops immediately on mouse down, even if the user does not drag
|
|
InertialScrollManager.ClearScrollVelocity();
|
|
} |