571 lines
22 KiB
C++
571 lines
22 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#pragma once
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "InputCoreTypes.h"
|
|
#include "Layout/Visibility.h"
|
|
#include "Widgets/DeclarativeSyntaxSupport.h"
|
|
#include "Input/Reply.h"
|
|
#include "Styling/SlateTypes.h"
|
|
#include "Framework/SlateDelegates.h"
|
|
#include "Widgets/Text/STextBlock.h"
|
|
#include "Widgets/Views/STableViewBase.h"
|
|
#include "Framework/Views/TableViewTypeTraits.h"
|
|
#include "Framework/Layout/Overscroll.h"
|
|
#include "Widgets/Views/SListView.h"
|
|
|
|
/**
|
|
* A TileView widget is a list which arranges its items horizontally until there is no more space then creates a new row.
|
|
* Items are spaced evenly horizontally.
|
|
*/
|
|
template <typename ItemType>
|
|
class STileView : public SListView<ItemType>
|
|
{
|
|
public:
|
|
typedef typename TListTypeTraits< ItemType >::NullableType NullableItemType;
|
|
|
|
typedef typename TSlateDelegates< ItemType >::FOnGenerateRow FOnGenerateRow;
|
|
typedef typename TSlateDelegates< ItemType >::FOnRefreshRow FOnRefreshRow;
|
|
typedef typename TSlateDelegates< ItemType >::FOnItemScrolledIntoView FOnItemScrolledIntoView;
|
|
typedef typename TSlateDelegates< ItemType >::FOnMouseButtonClick FOnMouseButtonClick;
|
|
typedef typename TSlateDelegates< ItemType >::FOnMouseButtonDoubleClick FOnMouseButtonDoubleClick;
|
|
typedef typename TSlateDelegates< NullableItemType >::FOnSelectionChanged FOnSelectionChanged;
|
|
typedef typename TSlateDelegates< ItemType >::FIsSelectableOrNavigable FIsSelectableOrNavigable;
|
|
|
|
typedef typename TSlateDelegates< ItemType >::FOnItemToString_Debug FOnItemToString_Debug;
|
|
|
|
using FOnWidgetToBeRemoved = typename SListView<ItemType>::FOnWidgetToBeRemoved;
|
|
|
|
public:
|
|
|
|
SLATE_BEGIN_ARGS(STileView<ItemType>)
|
|
: _OnGenerateTile()
|
|
, _OnRefreshTile()
|
|
, _OnTileReleased()
|
|
, _ItemHeight(128)
|
|
, _ItemWidth(128)
|
|
, _ItemAlignment(EListItemAlignment::EvenlyDistributed)
|
|
, _OnContextMenuOpening()
|
|
, _OnItemsRebuilt()
|
|
, _OnMouseButtonClick()
|
|
, _OnMouseButtonDoubleClick()
|
|
, _OnSelectionChanged()
|
|
, _OnIsSelectableOrNavigable()
|
|
, _SelectionMode(ESelectionMode::Multi)
|
|
, _ClearSelectionOnClick(true)
|
|
, _ExternalScrollbar()
|
|
, _Orientation(Orient_Vertical)
|
|
, _EnableAnimatedScrolling(false)
|
|
, _ScrollbarVisibility(EVisibility::Visible)
|
|
, _ScrollbarDragFocusCause(EFocusCause::Mouse)
|
|
, _AllowOverscroll(EAllowOverscroll::Yes)
|
|
, _ScrollBarStyle(&FAppStyle::Get().GetWidgetStyle<FScrollBarStyle>("ScrollBar"))
|
|
, _ScrollbarDisabledVisibility(EVisibility::Collapsed)
|
|
, _ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible)
|
|
, _WheelScrollMultiplier(GetGlobalScrollAmount())
|
|
, _HandleGamepadEvents(true)
|
|
, _HandleDirectionalNavigation(true)
|
|
, _IsFocusable(true)
|
|
, _OnItemToString_Debug()
|
|
, _OnEnteredBadState()
|
|
, _WrapHorizontalNavigation(true)
|
|
{
|
|
this->_Clipping = EWidgetClipping::ClipToBounds;
|
|
}
|
|
|
|
SLATE_EVENT( FOnGenerateRow, OnGenerateTile )
|
|
|
|
SLATE_EVENT( FOnRefreshRow, OnRefreshTile )
|
|
|
|
SLATE_EVENT( FOnWidgetToBeRemoved, OnTileReleased )
|
|
|
|
SLATE_EVENT( FOnTableViewScrolled, OnTileViewScrolled )
|
|
|
|
SLATE_EVENT( FOnFinishedScrolling, OnFinishedScrolling )
|
|
|
|
SLATE_EVENT( FOnItemScrolledIntoView, OnItemScrolledIntoView )
|
|
|
|
SLATE_ITEMS_SOURCE_ARGUMENT( ItemType, ListItemsSource )
|
|
|
|
SLATE_ATTRIBUTE( float, ItemHeight )
|
|
|
|
SLATE_ATTRIBUTE( float, ItemWidth )
|
|
|
|
SLATE_ATTRIBUTE( EListItemAlignment, ItemAlignment )
|
|
|
|
SLATE_EVENT( FOnContextMenuOpening, OnContextMenuOpening )
|
|
|
|
SLATE_EVENT( FSimpleDelegate, OnItemsRebuilt )
|
|
|
|
SLATE_EVENT( FOnMouseButtonClick, OnMouseButtonClick )
|
|
|
|
SLATE_EVENT( FOnMouseButtonDoubleClick, OnMouseButtonDoubleClick )
|
|
|
|
SLATE_EVENT( FOnSelectionChanged, OnSelectionChanged )
|
|
|
|
SLATE_EVENT( FIsSelectableOrNavigable, OnIsSelectableOrNavigable)
|
|
|
|
SLATE_ATTRIBUTE( ESelectionMode::Type, SelectionMode )
|
|
|
|
SLATE_ARGUMENT ( bool, ClearSelectionOnClick )
|
|
|
|
SLATE_ARGUMENT( TSharedPtr<SScrollBar>, ExternalScrollbar )
|
|
|
|
SLATE_ARGUMENT(EOrientation, Orientation)
|
|
|
|
SLATE_ARGUMENT( bool, EnableAnimatedScrolling)
|
|
|
|
SLATE_ARGUMENT( TOptional<double>, FixedLineScrollOffset )
|
|
|
|
SLATE_ATTRIBUTE(EVisibility, ScrollbarVisibility)
|
|
|
|
SLATE_ARGUMENT(EFocusCause, ScrollbarDragFocusCause)
|
|
|
|
SLATE_ARGUMENT( EAllowOverscroll, AllowOverscroll );
|
|
|
|
SLATE_STYLE_ARGUMENT( FScrollBarStyle, ScrollBarStyle );
|
|
|
|
SLATE_ARGUMENT( EVisibility, ScrollbarDisabledVisibility );
|
|
|
|
SLATE_ARGUMENT( EConsumeMouseWheel, ConsumeMouseWheel );
|
|
|
|
SLATE_ARGUMENT( float, WheelScrollMultiplier );
|
|
|
|
SLATE_ARGUMENT( bool, HandleGamepadEvents );
|
|
|
|
SLATE_ARGUMENT( bool, HandleDirectionalNavigation );
|
|
|
|
SLATE_ATTRIBUTE(bool, IsFocusable)
|
|
|
|
/** Assign this to get more diagnostics from the list view. */
|
|
SLATE_EVENT(FOnItemToString_Debug, OnItemToString_Debug)
|
|
|
|
SLATE_EVENT(FOnTableViewBadState, OnEnteredBadState);
|
|
|
|
SLATE_ARGUMENT(bool, WrapHorizontalNavigation);
|
|
|
|
SLATE_END_ARGS()
|
|
|
|
/**
|
|
* Construct this widget
|
|
*
|
|
* @param InArgs The declaration data for this widget
|
|
*/
|
|
void Construct( const typename STileView<ItemType>::FArguments& InArgs )
|
|
{
|
|
this->Clipping = InArgs._Clipping;
|
|
|
|
this->OnGenerateRow = InArgs._OnGenerateTile;
|
|
this->OnRefreshRow = InArgs._OnRefreshTile;
|
|
this->OnRowReleased = InArgs._OnTileReleased;
|
|
this->OnItemScrolledIntoView = InArgs._OnItemScrolledIntoView;
|
|
|
|
this->SetItemsSource(InArgs.MakeListItemsSource(this->SharedThis(this)));
|
|
this->OnContextMenuOpening = InArgs._OnContextMenuOpening;
|
|
this->OnItemsRebuilt = InArgs._OnItemsRebuilt;
|
|
this->OnClick = InArgs._OnMouseButtonClick;
|
|
this->OnDoubleClick = InArgs._OnMouseButtonDoubleClick;
|
|
this->OnSelectionChanged = InArgs._OnSelectionChanged;
|
|
this->OnIsSelectableOrNavigable = InArgs._OnIsSelectableOrNavigable;
|
|
this->SelectionMode = InArgs._SelectionMode;
|
|
|
|
this->bClearSelectionOnClick = InArgs._ClearSelectionOnClick;
|
|
|
|
this->AllowOverscroll = InArgs._AllowOverscroll;
|
|
this->ConsumeMouseWheel = InArgs._ConsumeMouseWheel;
|
|
this->WheelScrollMultiplier = InArgs._WheelScrollMultiplier;
|
|
|
|
this->bHandleGamepadEvents = InArgs._HandleGamepadEvents;
|
|
this->bHandleDirectionalNavigation = InArgs._HandleDirectionalNavigation;
|
|
this->IsFocusable = InArgs._IsFocusable;
|
|
|
|
this->bEnableAnimatedScrolling = InArgs._EnableAnimatedScrolling;
|
|
this->FixedLineScrollOffset = InArgs._FixedLineScrollOffset;
|
|
|
|
this->OnItemToString_Debug = InArgs._OnItemToString_Debug.IsBound()
|
|
? InArgs._OnItemToString_Debug
|
|
: SListView< ItemType >::GetDefaultDebugDelegate();
|
|
this->OnEnteredBadState = InArgs._OnEnteredBadState;
|
|
|
|
this->bWrapHorizontalNavigation = InArgs._WrapHorizontalNavigation;
|
|
|
|
// Check for any parameters that the coder forgot to specify.
|
|
FString ErrorString;
|
|
{
|
|
if ( !this->OnGenerateRow.IsBound() )
|
|
{
|
|
ErrorString += TEXT("Please specify an OnGenerateTile. \n");
|
|
}
|
|
|
|
if ( !this->HasValidItemsSource() )
|
|
{
|
|
ErrorString += TEXT("Please specify a ListItemsSource. \n");
|
|
}
|
|
}
|
|
|
|
if (ErrorString.Len() > 0)
|
|
{
|
|
// Let the coder know what they forgot
|
|
this->ChildSlot
|
|
.HAlign(HAlign_Center)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(FText::FromString(ErrorString))
|
|
];
|
|
}
|
|
else
|
|
{
|
|
// Make the TableView
|
|
this->ConstructChildren(InArgs._ItemWidth, InArgs._ItemHeight, InArgs._ItemAlignment, TSharedPtr<SHeaderRow>(), InArgs._ExternalScrollbar, InArgs._Orientation, InArgs._OnTileViewScrolled, InArgs._ScrollBarStyle);
|
|
if (this->ScrollBar.IsValid())
|
|
{
|
|
this->ScrollBar->SetDragFocusCause(InArgs._ScrollbarDragFocusCause);
|
|
this->ScrollBar->SetUserVisibility(InArgs._ScrollbarVisibility);
|
|
this->ScrollBar->SetScrollbarDisabledVisibility(InArgs._ScrollbarDisabledVisibility);
|
|
}
|
|
this->AddMetadata(MakeShared<TTableViewMetadata<ItemType>>(this->SharedThis(this)));
|
|
}
|
|
}
|
|
|
|
STileView( ETableViewMode::Type InListMode = ETableViewMode::Tile )
|
|
: SListView<ItemType>( InListMode )
|
|
{
|
|
}
|
|
|
|
public:
|
|
|
|
// SWidget overrides
|
|
|
|
virtual FNavigationReply OnNavigation(const FGeometry& MyGeometry, const FNavigationEvent& InNavigationEvent) override
|
|
{
|
|
if (this->HasValidItemsSource() && this->bHandleDirectionalNavigation && (this->bHandleGamepadEvents || InNavigationEvent.GetNavigationGenesis() != ENavigationGenesis::Controller))
|
|
{
|
|
const TArrayView<const ItemType>& ItemsSourceRef = this->GetItems();
|
|
|
|
const int32 NumItemsPerLine = GetNumItemsPerLine();
|
|
const int32 CurSelectionIndex = (!TListTypeTraits<ItemType>::IsPtrValid(this->SelectorItem)) ? 0 : ItemsSourceRef.Find(TListTypeTraits<ItemType>::NullableItemTypeConvertToItemType(this->SelectorItem));
|
|
int32 AttemptSelectIndex = -1;
|
|
|
|
const EUINavigation NavType = InNavigationEvent.GetNavigationType();
|
|
if ((this->Orientation == Orient_Vertical && NavType == EUINavigation::Left) ||
|
|
(this->Orientation == Orient_Horizontal && NavType == EUINavigation::Up))
|
|
{
|
|
if (bWrapHorizontalNavigation || (CurSelectionIndex % NumItemsPerLine) > 0)
|
|
{
|
|
AttemptSelectIndex = CurSelectionIndex - 1;
|
|
}
|
|
}
|
|
else if ((this->Orientation == Orient_Vertical && NavType == EUINavigation::Right) ||
|
|
(this->Orientation == Orient_Horizontal && NavType == EUINavigation::Down))
|
|
{
|
|
if (bWrapHorizontalNavigation || (CurSelectionIndex % NumItemsPerLine) < (NumItemsPerLine - 1))
|
|
{
|
|
AttemptSelectIndex = CurSelectionIndex + 1;
|
|
}
|
|
}
|
|
|
|
// If it's valid we'll scroll it into view and return an explicit widget in the FNavigationReply
|
|
if (ItemsSourceRef.IsValidIndex(AttemptSelectIndex) && this->bIsGamepadScrollingEnabled)
|
|
{
|
|
this->NavigationSelect(ItemsSourceRef[AttemptSelectIndex], InNavigationEvent);
|
|
return FNavigationReply::Explicit(nullptr);
|
|
}
|
|
}
|
|
|
|
return SListView<ItemType>::OnNavigation(MyGeometry, InNavigationEvent);
|
|
}
|
|
|
|
public:
|
|
|
|
virtual STableViewBase::FReGenerateResults ReGenerateItems( const FGeometry& MyGeometry ) override
|
|
{
|
|
// Clear all the items from our panel. We will re-add them in the correct order momentarily.
|
|
this->ClearWidgets();
|
|
|
|
const TArrayView<const ItemType> Items = this->GetItems();
|
|
if (Items.Num() > 0)
|
|
{
|
|
// Item width and height is constant by design.
|
|
FTableViewDimensions TileDimensions = GetTileDimensions();
|
|
FTableViewDimensions AllottedDimensions(this->Orientation, MyGeometry.GetLocalSize());
|
|
|
|
const int32 NumItems = Items.Num();
|
|
const int32 NumItemsPerLine = GetNumItemsPerLine();
|
|
const int32 NumItemsPaddedToFillLastLine = (NumItems % NumItemsPerLine != 0)
|
|
? NumItems + NumItemsPerLine - NumItems % NumItemsPerLine
|
|
: NumItems;
|
|
|
|
const double LinesPerScreen = AllottedDimensions.ScrollAxis / TileDimensions.ScrollAxis;
|
|
const double EndOfListOffset = NumItemsPaddedToFillLastLine - NumItemsPerLine * LinesPerScreen;
|
|
const double ClampedScrollOffset = FMath::Clamp(this->CurrentScrollOffset, 0.0, EndOfListOffset);
|
|
const float LayoutScaleMultiplier = MyGeometry.GetAccumulatedLayoutTransform().GetScale();
|
|
|
|
// Once we run out of vertical and horizontal space, we stop generating widgets.
|
|
FTableViewDimensions DimensionsUsedSoFar(this->Orientation);
|
|
|
|
// Index of the item at which we start generating based on how far scrolled down we are
|
|
int32 StartIndex = FMath::Max( 0, FMath::FloorToInt32(ClampedScrollOffset / NumItemsPerLine) * NumItemsPerLine);
|
|
|
|
// Let the WidgetGenerator know that we are starting a pass so that it can keep track of data items and widgets.
|
|
this->WidgetGenerator.OnBeginGenerationPass();
|
|
|
|
// Actually generate the widgets.
|
|
bool bIsAtEndOfList = false;
|
|
bool bHasFilledAvailableArea = false;
|
|
bool bNewLine = true;
|
|
bool bFirstLine = true;
|
|
double NumLinesShownOnScreen = 0;
|
|
for( int32 ItemIndex = StartIndex; !bHasFilledAvailableArea && ItemIndex < NumItems; ++ItemIndex )
|
|
{
|
|
const ItemType& CurItem = Items[ItemIndex];
|
|
|
|
if (bNewLine)
|
|
{
|
|
bNewLine = false;
|
|
|
|
float LineFraction = 1.f;
|
|
if (bFirstLine)
|
|
{
|
|
bFirstLine = false;
|
|
LineFraction -= (float)FMath::Fractional(ClampedScrollOffset / NumItemsPerLine);
|
|
}
|
|
|
|
DimensionsUsedSoFar.ScrollAxis += TileDimensions.ScrollAxis * LineFraction;
|
|
|
|
if (DimensionsUsedSoFar.ScrollAxis > AllottedDimensions.ScrollAxis)
|
|
{
|
|
NumLinesShownOnScreen += FMath::Max(1.0f - ((DimensionsUsedSoFar.ScrollAxis - AllottedDimensions.ScrollAxis) / TileDimensions.ScrollAxis), 0.0f);
|
|
}
|
|
else
|
|
{
|
|
NumLinesShownOnScreen += LineFraction;
|
|
}
|
|
}
|
|
|
|
SListView<ItemType>::GenerateWidgetForItem(CurItem, ItemIndex, StartIndex, LayoutScaleMultiplier);
|
|
|
|
// The widget used up some of the available space for the current line
|
|
DimensionsUsedSoFar.LineAxis += TileDimensions.LineAxis;
|
|
|
|
bIsAtEndOfList = ItemIndex >= NumItems - 1;
|
|
|
|
if (DimensionsUsedSoFar.LineAxis + TileDimensions.LineAxis > AllottedDimensions.LineAxis)
|
|
{
|
|
// A new line of widgets was completed - time to start another one
|
|
DimensionsUsedSoFar.LineAxis = 0;
|
|
bNewLine = true;
|
|
}
|
|
|
|
if (bIsAtEndOfList || bNewLine)
|
|
{
|
|
// We've filled all the available area when we've finished a line that's partially clipped by the end of the view
|
|
const float FloatPrecisionOffset = 0.001f;
|
|
bHasFilledAvailableArea = DimensionsUsedSoFar.ScrollAxis > AllottedDimensions.ScrollAxis + FloatPrecisionOffset;
|
|
}
|
|
}
|
|
|
|
// We have completed the generation pass. The WidgetGenerator will clean up unused Widgets.
|
|
this->WidgetGenerator.OnEndGenerationPass();
|
|
|
|
const float TotalGeneratedLineAxisSize = (float)(FMath::CeilToFloat(NumLinesShownOnScreen) * TileDimensions.ScrollAxis);
|
|
return STableViewBase::FReGenerateResults(ClampedScrollOffset, TotalGeneratedLineAxisSize, NumLinesShownOnScreen, bIsAtEndOfList && !bHasFilledAvailableArea);
|
|
}
|
|
|
|
return STableViewBase::FReGenerateResults(0, 0, 0, false);
|
|
|
|
}
|
|
|
|
virtual int32 GetNumItemsBeingObserved() const override
|
|
{
|
|
const int32 NumItemsBeingObserved = this->GetItems().Num();
|
|
const int32 NumItemsPerLine = GetNumItemsPerLine();
|
|
|
|
int32 NumEmptySpacesAtEnd = 0;
|
|
if ( NumItemsPerLine > 0 )
|
|
{
|
|
NumEmptySpacesAtEnd = NumItemsPerLine - (NumItemsBeingObserved % NumItemsPerLine);
|
|
if ( NumEmptySpacesAtEnd >= NumItemsPerLine )
|
|
{
|
|
NumEmptySpacesAtEnd = 0;
|
|
}
|
|
}
|
|
|
|
return NumItemsBeingObserved + NumEmptySpacesAtEnd;
|
|
}
|
|
|
|
protected:
|
|
|
|
FTableViewDimensions GetTileDimensions() const
|
|
{
|
|
return FTableViewDimensions(this->Orientation, this->GetItemWidth(), this->GetItemHeight());
|
|
}
|
|
|
|
virtual float ScrollBy( const FGeometry& MyGeometry, float ScrollByAmountInSlateUnits, EAllowOverscroll InAllowOverscroll ) override
|
|
{
|
|
const bool bWholeListVisible = this->DesiredScrollOffset == 0 && this->bWasAtEndOfList;
|
|
if (InAllowOverscroll == EAllowOverscroll::Yes && this->Overscroll.ShouldApplyOverscroll(this->DesiredScrollOffset == 0, this->bWasAtEndOfList, ScrollByAmountInSlateUnits))
|
|
{
|
|
const float UnclampedScrollDelta = ScrollByAmountInSlateUnits / (float)GetNumItemsPerLine();
|
|
const float ActuallyScrolledBy = this->Overscroll.ScrollBy(MyGeometry, UnclampedScrollDelta);
|
|
if (ActuallyScrolledBy != 0.0f)
|
|
{
|
|
this->RequestListRefresh();
|
|
}
|
|
return ActuallyScrolledBy;
|
|
}
|
|
else if (!bWholeListVisible)
|
|
{
|
|
const double NewScrollOffset = this->DesiredScrollOffset + ((ScrollByAmountInSlateUnits * (float)GetNumItemsPerLine()) / GetTileDimensions().ScrollAxis);
|
|
|
|
return this->ScrollTo( (float)NewScrollOffset );
|
|
}
|
|
|
|
return 0.f;
|
|
}
|
|
|
|
virtual int32 GetNumItemsPerLine() const override
|
|
{
|
|
FTableViewDimensions PanelDimensions(this->Orientation, this->PanelGeometryLastTick.GetLocalSize());
|
|
FTableViewDimensions TileDimensions = GetTileDimensions();
|
|
|
|
const int32 NumItemsPerLine = TileDimensions.LineAxis > 0 ? FMath::FloorToInt(PanelDimensions.LineAxis / TileDimensions.LineAxis) : 1;
|
|
return FMath::Max(1, NumItemsPerLine);
|
|
}
|
|
|
|
/**
|
|
* If there is a pending request to scroll an item into view, do so.
|
|
*
|
|
* @param ListViewGeometry The geometry of the listView; can be useful for centering the item.
|
|
*/
|
|
virtual typename SListView<ItemType>::EScrollIntoViewResult ScrollIntoView(const FGeometry& ListViewGeometry) override
|
|
{
|
|
if (TListTypeTraits<ItemType>::IsPtrValid(this->ItemToScrollIntoView) && this->HasValidItemsSource())
|
|
{
|
|
const int32 IndexOfItem = this->GetItems().Find(TListTypeTraits<ItemType>::NullableItemTypeConvertToItemType(this->ItemToScrollIntoView));
|
|
if (IndexOfItem != INDEX_NONE)
|
|
{
|
|
const float NumLinesInView = FTableViewDimensions(this->Orientation, ListViewGeometry.GetLocalSize()).ScrollAxis / GetTileDimensions().ScrollAxis;
|
|
|
|
double NumLiveWidgets = this->GetNumLiveWidgets();
|
|
if (NumLiveWidgets == 0 && this->IsPendingRefresh())
|
|
{
|
|
// Use the last number of widgets on screen to estimate if we actually need to scroll.
|
|
NumLiveWidgets = this->LastGenerateResults.ExactNumLinesOnScreen;
|
|
|
|
// If we still don't have any widgets, we're not in a situation where we can scroll an item into view
|
|
// (probably as nothing has been generated yet), so we'll defer this again until the next frame
|
|
if (NumLiveWidgets == 0)
|
|
{
|
|
return SListView<ItemType>::EScrollIntoViewResult::Deferred;
|
|
}
|
|
}
|
|
|
|
this->EndInertialScrolling();
|
|
|
|
// Only scroll the item into view if it's not already in the visible range
|
|
const int32 NumItemsPerLine = GetNumItemsPerLine();
|
|
const double ScrollLineOffset = this->GetTargetScrollOffset() / NumItemsPerLine;
|
|
const int32 LineOfItem = FMath::FloorToInt((float)IndexOfItem / (float)NumItemsPerLine);
|
|
const int32 NumFullLinesInView = FMath::FloorToInt32(ScrollLineOffset + NumLinesInView) - FMath::CeilToInt32(ScrollLineOffset);
|
|
|
|
const double MinDisplayedLine = this->bNavigateOnScrollIntoView ? FMath::FloorToDouble(ScrollLineOffset) : FMath::CeilToDouble(ScrollLineOffset);
|
|
const double MaxDisplayedLine = this->bNavigateOnScrollIntoView ? FMath::CeilToDouble(ScrollLineOffset + NumFullLinesInView) : FMath::FloorToDouble(ScrollLineOffset + NumFullLinesInView);
|
|
|
|
if (LineOfItem < MinDisplayedLine || LineOfItem > MaxDisplayedLine)
|
|
{
|
|
float NewLineOffset = 0.0f;
|
|
if (LineOfItem < MinDisplayedLine - 1 || LineOfItem > MaxDisplayedLine + 1)
|
|
{
|
|
// The item was completely hidden so we scroll to center it.
|
|
|
|
// Set the line with the item at the beginning of the view area
|
|
NewLineOffset = (float)LineOfItem;
|
|
// Center the line in the view area
|
|
NewLineOffset -= NumLinesInView * 0.5f;
|
|
// Center the tile itself
|
|
NewLineOffset += 0.5f;
|
|
}
|
|
else if (LineOfItem == MinDisplayedLine - 1)
|
|
{
|
|
// The item was just on the line above, set the line with the item at the beginning of the view area
|
|
// This prevent issues when navigating the list element by element triggers this code since we don't want to suddenly center an element
|
|
NewLineOffset = (float)LineOfItem;
|
|
}
|
|
else // LineOfItem == MaxDisplayedLine + 1
|
|
{
|
|
// The item was just on the next line, scroll it into view
|
|
// This prevent issues when navigating the list element by element triggers this code since we don't want to suddenly center an element
|
|
if (this->FixedLineScrollOffset.IsSet())
|
|
{
|
|
// We should scroll to align a line with the top of the view while putting the item at the bottom
|
|
const int32 FullLinesInView = FMath::FloorToInt32(NumLinesInView);
|
|
NewLineOffset = (float)LineOfItem - ((float)FullLinesInView - 1.f);
|
|
}
|
|
else
|
|
{
|
|
// We should scroll just enough to show the item on the last line
|
|
NewLineOffset = (float)LineOfItem - NumLinesInView + 1.f + this->NavigationScrollOffset;
|
|
}
|
|
}
|
|
|
|
// Convert the line offset into an item offset
|
|
double NewScrollOffset = NewLineOffset * (double)NumItemsPerLine;
|
|
// And clamp the scroll offset within the allowed limits
|
|
NewScrollOffset = FMath::Clamp(NewScrollOffset, 0., (double)GetNumItemsBeingObserved() - (double)NumItemsPerLine * NumLinesInView);
|
|
|
|
this->SetScrollOffset((float)NewScrollOffset);
|
|
}
|
|
else if (this->bNavigateOnScrollIntoView)
|
|
{
|
|
// Make sure the line containing the existing entry for this item is fully in view
|
|
if (LineOfItem == MinDisplayedLine)
|
|
{
|
|
// This line is clipped at the top/left, so set it as the new offset
|
|
this->SetScrollOffset((float)(LineOfItem * NumItemsPerLine) - (this->FixedLineScrollOffset.IsSet() && LineOfItem > 0 ? 0.f : this->NavigationScrollOffset));
|
|
}
|
|
else if (LineOfItem == MaxDisplayedLine)
|
|
{
|
|
if (this->FixedLineScrollOffset.IsSet())
|
|
{
|
|
// This line is clipped at the end but since FixedLineScrollOffset is set we must align a line to the top of the list,
|
|
// therefore we just scroll to the next line
|
|
const float HiddenLinesCount = (float)LineOfItem - FMath::FloorToFloat(NumLinesInView);
|
|
this->SetScrollOffset((1.0f + HiddenLinesCount) * (float)NumItemsPerLine);
|
|
}
|
|
else
|
|
{
|
|
// This line is clipped at the end, so we need to advance just enough to bring it fully into view
|
|
// Since all tiles are required to be of the same size, this is straightforward
|
|
const float NewLineOffset = (float)LineOfItem - NumLinesInView + 1.f + this->NavigationScrollOffset;
|
|
this->SetScrollOffset(NewLineOffset * (float)NumItemsPerLine);
|
|
}
|
|
}
|
|
}
|
|
|
|
this->RequestListRefresh();
|
|
|
|
this->ItemToNotifyWhenInView = this->ItemToScrollIntoView;
|
|
}
|
|
|
|
TListTypeTraits<ItemType>::ResetPtr(this->ItemToScrollIntoView);
|
|
}
|
|
|
|
if (TListTypeTraits<ItemType>::IsPtrValid(this->ItemToNotifyWhenInView))
|
|
{
|
|
if (this->bEnableAnimatedScrolling)
|
|
{
|
|
// When we have a target item we're shooting for, we haven't succeeded with the scroll until a widget for it exists
|
|
const bool bHasWidgetForItem = this->WidgetFromItem(TListTypeTraits<ItemType>::NullableItemTypeConvertToItemType(this->ItemToNotifyWhenInView)).IsValid();
|
|
return bHasWidgetForItem ? SListView<ItemType>::EScrollIntoViewResult::Success : SListView<ItemType>::EScrollIntoViewResult::Deferred;
|
|
}
|
|
}
|
|
|
|
return SListView<ItemType>::EScrollIntoViewResult::Success;
|
|
}
|
|
|
|
/** Should the left and right navigations be handled as a wrap when hitting the bounds. (you'll move to the previous / next row when appropriate) */
|
|
bool bWrapHorizontalNavigation = true;
|
|
};
|