Files
UnrealEngine/Engine/Source/Runtime/UMG/Public/Components/ListViewBase.h
2025-05-18 13:04:45 +08:00

835 lines
36 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Components/Widget.h"
#include "Slate/SObjectTableRow.h"
#include "Blueprint/UserWidgetPool.h"
#include "Widgets/Layout/SSpacer.h"
#include "Widgets/Views/STileView.h"
#include "Widgets/Views/STreeView.h"
#include "Framework/Application/SlateApplication.h"
#include "Styling/UMGCoreStyle.h"
#include "ListViewBase.generated.h"
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnListEntryGeneratedDynamic, UUserWidget*, Widget);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnListEntryReleasedDynamic, UUserWidget*, Widget);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnListEntriesGeneratedDynamic, int32, NumEntries);
//////////////////////////////////////////////////////////////////////////
// ITypedUMGListView<T>
//////////////////////////////////////////////////////////////////////////
/**
* Mirrored SListView<T> API for easier interaction with a bound UListViewBase widget
* See declarations on SListView for more info on each function and event
*
* Note that, being a template class, this is not a UClass and therefore cannot be exposed to Blueprint.
* If you are using UObject* items, just use (or inherit from) UListView directly
* Otherwise, it is up to the child class to propagate events and/or expose functions to BP as needed
*
* Use the IMPLEMENT_TYPED_UMG_LIST() macro for the implementation boilerplate in your implementing class.
* @see UListView for an implementation example.
*/
template <typename ItemType>
class ITypedUMGListView
{
public:
using NullableItemType = typename SListView<ItemType>::NullableItemType;
//////////////////////////////////////////////////////////////////////////
// Automatically implemented via IMPLEMENT_TYPED_UMG_LIST()
//////////////////////////////////////////////////////////////////////////
DECLARE_MULTICAST_DELEGATE_OneParam(FSimpleListItemEvent, ItemType);
virtual FSimpleListItemEvent& OnItemClicked() const = 0;
virtual FSimpleListItemEvent& OnItemDoubleClicked() const = 0;
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnItemIsHoveredChanged, ItemType, bool);
virtual FOnItemIsHoveredChanged& OnItemIsHoveredChanged() const = 0;
DECLARE_MULTICAST_DELEGATE_OneParam(FOnItemSelectionChanged, NullableItemType);
virtual FOnItemSelectionChanged& OnItemSelectionChanged() const = 0;
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnListViewScrolled, float, float);
virtual FOnListViewScrolled& OnListViewScrolled() const = 0;
DECLARE_MULTICAST_DELEGATE(FOnFinishedScrolling);
virtual FOnFinishedScrolling& OnFinishedScrolling() const = 0;
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnItemScrolledIntoView, ItemType, UUserWidget&);
virtual FOnItemScrolledIntoView& OnItemScrolledIntoView() const = 0;
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnItemExpansionChanged, ItemType, bool);
virtual FOnItemExpansionChanged& OnItemExpansionChanged() const = 0;
DECLARE_DELEGATE_RetVal_OneParam(TSubclassOf<UUserWidget>, FOnGetEntryClassForItem, ItemType);
virtual FOnGetEntryClassForItem& OnGetEntryClassForItem() const = 0;
virtual TSubclassOf<UUserWidget> GetDefaultEntryClass() const = 0;
DECLARE_DELEGATE_RetVal_OneParam(bool, FOnIsItemSelectableOrNavigable, ItemType);
virtual FOnIsItemSelectableOrNavigable& OnIsItemSelectableOrNavigable() const = 0;
protected:
virtual SListView<ItemType>* GetMyListView() const = 0;
virtual uint32 GetOwningUserIndex() const = 0;
virtual bool IsDesignerPreview() const = 0;
//////////////////////////////////////////////////////////////////////////
public:
/**
* Default behavior is to check the delegate, then fall back to the default if that fails.
* Feel free to override directly in child classes to determine the class yourself.
*/
virtual TSubclassOf<UUserWidget> GetDesiredEntryClassForItem(ItemType Item) const
{
//@todo DanH: Need some way to allow the design time preview entries to match up with the various possible runtime entries without a possibility for inaccuracy
if (!IsDesignerPreview())
{
TSubclassOf<UUserWidget> CustomClass = OnGetEntryClassForItem().IsBound() ? OnGetEntryClassForItem().Execute(Item) : nullptr;
if (CustomClass)
{
return CustomClass;
}
}
return GetDefaultEntryClass();
}
//////////////////////////////////////////////////////////////////////////
// Public API to match that of SListView
//////////////////////////////////////////////////////////////////////////
NullableItemType GetSelectedItem() const
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
TArray<ItemType> SelectedItems = MyListView->GetSelectedItems();
if (SelectedItems.Num() > 0)
{
return SelectedItems[0];
}
}
return TListTypeTraits<ItemType>::MakeNullPtr();
}
const TObjectPtrWrapTypeOf<ItemType>* ItemFromEntryWidget(const UUserWidget& EntryWidget) const
{
SListView<ItemType>* MyListView = GetMyListView();
if (ensure(EntryWidget.Implements<UUserListEntry>()) && MyListView)
{
TSharedPtr<SObjectTableRow<ItemType>> ObjectTableRow = StaticCastSharedPtr<SObjectTableRow<ItemType>>(EntryWidget.GetCachedWidget());
if (ObjectTableRow.IsValid())
{
return MyListView->ItemFromWidget(ObjectTableRow.Get());
}
}
return nullptr;
}
template <typename RowWidgetT = UUserWidget>
RowWidgetT* GetEntryWidgetFromItem(const ItemType& Item) const
{
TSharedPtr<SObjectTableRow<ItemType>> ObjectRow = GetObjectRowFromItem(Item);
if (ObjectRow.IsValid())
{
return Cast<RowWidgetT>(ObjectRow->GetWidgetObject());
}
return nullptr;
}
int32 GetIndexInList(const ItemType& Item) const
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
if (TSharedPtr<ITableRow> RowWidget = MyListView->WidgetFromItem(Item))
{
return RowWidget->GetIndexInList();
}
}
return INDEX_NONE;
}
int32 GetSelectedItems(TArray<ItemType>& OutSelectedItems) const
{
SListView<ItemType>* MyListView = GetMyListView();
return MyListView ? MyListView->GetSelectedItems(OutSelectedItems) : 0;
}
int32 GetNumItemsSelected() const
{
SListView<ItemType>* MyListView = GetMyListView();
return MyListView ? MyListView->GetNumItemsSelected() : 0;
}
void SetSelectedItem(const ItemType& SoleSelectedItem, ESelectInfo::Type SelectInfo = ESelectInfo::Direct)
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
MyListView->SetSelection(SoleSelectedItem, SelectInfo);
}
}
void SetItemSelection(const ItemType& Item, bool bIsSelected, ESelectInfo::Type SelectInfo = ESelectInfo::Direct)
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
MyListView->SetItemSelection(Item, bIsSelected, SelectInfo);
}
}
void ClearSelection()
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
MyListView->ClearSelection();
}
}
bool IsItemVisible(const ItemType& Item) const
{
SListView<ItemType>* MyListView = GetMyListView();
return MyListView ? MyListView->IsItemVisible(Item) : false;
}
bool IsItemSelected(const ItemType& Item) const
{
SListView<ItemType>* MyListView = GetMyListView();
return MyListView ? MyListView->IsItemSelected(Item) : false;
}
void RequestNavigateToItem(const ItemType& Item)
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
MyListView->RequestNavigateToItem(Item, GetOwningUserIndex());
}
}
void RequestScrollItemIntoView(const ItemType& Item)
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
MyListView->RequestScrollIntoView(Item, GetOwningUserIndex());
}
}
void CancelScrollIntoView()
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
MyListView->CancelScrollIntoView();
}
}
//////////////////////////////////////////////////////////////////////////
protected:
/**
* ListView construction helpers
* Use these instead of SNew-ing your owned ListView directly to get exposed events for free
*/
struct FListViewConstructArgs
{
bool bAllowFocus = true;
ESelectionMode::Type SelectionMode = ESelectionMode::Single;
bool bClearSelectionOnClick = false;
EConsumeMouseWheel ConsumeMouseWheel = EConsumeMouseWheel::WhenScrollingPossible;
bool bReturnFocusToSelection = false;
bool bClearScrollVelocityOnSelection = true;
EOrientation Orientation = Orient_Vertical;
EScrollIntoViewAlignment ScrollIntoViewAlignment = EScrollIntoViewAlignment::CenterAligned;
const FTableViewStyle* ListViewStyle = &FUMGCoreStyle::Get().GetWidgetStyle<FTableViewStyle>("ListView");
const FScrollBarStyle* ScrollBarStyle = &FUMGCoreStyle::Get().GetWidgetStyle<FScrollBarStyle>("ScrollBar");
FMargin ScrollBarPadding = FMargin(0.0f);
bool bPreventThrottling = false;
};
template <template<typename> class ListViewT = SListView, typename UListViewBaseT>
static TSharedRef<ListViewT<ItemType>> ConstructListView(UListViewBaseT* Implementer,
const TArray<ItemType>& ListItems,
const FListViewConstructArgs& Args = FListViewConstructArgs())
{
static_assert(TIsDerivedFrom<ListViewT<ItemType>, SListView<ItemType>>::IsDerived, "ConstructListView can only construct instances of SListView classes");
TSharedRef<ListViewT<ItemType>> ListView = SNew(ListViewT<ItemType>)
.HandleGamepadEvents(true)
.ListItemsSource(&ListItems)
.IsFocusable(Args.bAllowFocus)
.ClearSelectionOnClick(Args.bClearSelectionOnClick)
.ConsumeMouseWheel(Args.ConsumeMouseWheel)
.SelectionMode(Args.SelectionMode)
.ReturnFocusToSelection(Args.bReturnFocusToSelection)
.ClearScrollVelocityOnSelection(Args.bClearScrollVelocityOnSelection)
.Orientation(Args.Orientation)
.ScrollIntoViewAlignment(Args.ScrollIntoViewAlignment)
.ListViewStyle(Args.ListViewStyle)
.ScrollBarStyle(Args.ScrollBarStyle)
.ScrollBarPadding(Args.ScrollBarPadding)
.PreventThrottling(Args.bPreventThrottling)
.OnGenerateRow_UObject(Implementer, &UListViewBaseT::HandleGenerateRow)
.OnSelectionChanged_UObject(Implementer, &UListViewBaseT::HandleSelectionChanged)
.OnIsSelectableOrNavigable_UObject(Implementer, &UListViewBaseT::HandleIsSelectableOrNavigable)
.OnRowReleased_UObject(Implementer, &UListViewBaseT::HandleRowReleased)
.OnItemScrolledIntoView_UObject(Implementer, &UListViewBaseT::HandleItemScrolledIntoView)
.OnListViewScrolled_UObject(Implementer, &UListViewBaseT::HandleListViewScrolled)
.OnFinishedScrolling_UObject(Implementer, &UListViewBaseT::HandleFinishedScrolling)
.OnMouseButtonClick_UObject(Implementer, &UListViewBaseT::HandleItemClicked)
.OnMouseButtonDoubleClick_UObject(Implementer, &UListViewBaseT::HandleItemDoubleClicked);
ListView->BindToRefreshRow(TSlateDelegates< ItemType >::FOnRefreshRow::CreateUObject(Implementer, &UListViewBaseT::HandleRefreshRow));
return ListView;
}
struct FTileViewConstructArgs : public FListViewConstructArgs
{
EListItemAlignment TileAlignment = EListItemAlignment::EvenlyDistributed;
TAttribute<float> EntryHeight;
TAttribute<float> EntryWidth;
bool bWrapDirectionalNavigation = false;
const FScrollBarStyle* ScrollBarStyle = &FUMGCoreStyle::Get().GetWidgetStyle<FScrollBarStyle>("ScrollBar");
EVisibility ScrollbarDisabledVisibility = EVisibility::Collapsed;
};
template <template<typename> class TileViewT = STileView, typename UListViewBaseT>
static TSharedRef<TileViewT<ItemType>> ConstructTileView(UListViewBaseT* Implementer,
const TArray<ItemType>& ListItems,
const FTileViewConstructArgs& Args = FTileViewConstructArgs())
{
static_assert(TIsDerivedFrom<TileViewT<ItemType>, STileView<ItemType>>::IsDerived, "ConstructTileView can only construct instances of STileView classes");
TSharedRef<TileViewT<ItemType>> TileView = SNew(TileViewT<ItemType>)
.HandleGamepadEvents(true)
.ListItemsSource(&ListItems)
.IsFocusable(Args.bAllowFocus)
.ClearSelectionOnClick(Args.bClearSelectionOnClick)
.WrapHorizontalNavigation(Args.bWrapDirectionalNavigation)
.ConsumeMouseWheel(Args.ConsumeMouseWheel)
.SelectionMode(Args.SelectionMode)
.ItemHeight(Args.EntryHeight)
.ItemWidth(Args.EntryWidth)
.ItemAlignment(Args.TileAlignment)
.Orientation(Args.Orientation)
.ScrollBarStyle(Args.ScrollBarStyle)
.ScrollbarDisabledVisibility(Args.ScrollbarDisabledVisibility)
.OnGenerateTile_UObject(Implementer, &UListViewBaseT::HandleGenerateRow)
.OnTileReleased_UObject(Implementer, &UListViewBaseT::HandleRowReleased)
.OnSelectionChanged_UObject(Implementer, &UListViewBaseT::HandleSelectionChanged)
.OnIsSelectableOrNavigable_UObject(Implementer, &UListViewBaseT::HandleIsSelectableOrNavigable)
.OnItemScrolledIntoView_UObject(Implementer, &UListViewBaseT::HandleItemScrolledIntoView)
.OnTileViewScrolled_UObject(Implementer, &UListViewBaseT::HandleListViewScrolled)
.OnFinishedScrolling_UObject(Implementer, &UListViewBaseT::HandleFinishedScrolling)
.OnMouseButtonClick_UObject(Implementer, &UListViewBaseT::HandleItemClicked)
.OnMouseButtonDoubleClick_UObject(Implementer, &UListViewBaseT::HandleItemDoubleClicked);
TileView->BindToRefreshRow(TSlateDelegates< ItemType >::FOnRefreshRow::CreateUObject(Implementer, &UListViewBaseT::HandleRefreshRow));
return TileView;
}
struct FTreeViewConstructArgs
{
ESelectionMode::Type SelectionMode = ESelectionMode::Single;
bool bClearSelectionOnClick = false;
EConsumeMouseWheel ConsumeMouseWheel = EConsumeMouseWheel::WhenScrollingPossible;
bool bReturnFocusToSelection = false;
const FTableViewStyle* TreeViewStyle = &FUMGCoreStyle::Get().GetWidgetStyle<FTableViewStyle>("TreeView");
const FScrollBarStyle* ScrollBarStyle = &FUMGCoreStyle::Get().GetWidgetStyle<FScrollBarStyle>("ScrollBar");
};
template <template<typename> class TreeViewT = STreeView, typename UListViewBaseT>
static TSharedRef<TreeViewT<ItemType>> ConstructTreeView(UListViewBaseT* Implementer,
const TArray<ItemType>& ListItems,
const FTreeViewConstructArgs& Args = FTreeViewConstructArgs())
{
static_assert(TIsDerivedFrom<TreeViewT<ItemType>, STreeView<ItemType>>::IsDerived, "ConstructTreeView can only construct instances of STreeView classes");
TSharedRef<TreeViewT<ItemType>> TreeView = SNew(TreeViewT<ItemType>)
.HandleGamepadEvents(true)
.TreeItemsSource(&ListItems)
.ClearSelectionOnClick(Args.bClearSelectionOnClick)
.ConsumeMouseWheel(Args.ConsumeMouseWheel)
.SelectionMode(Args.SelectionMode)
.ReturnFocusToSelection(Args.bReturnFocusToSelection)
.TreeViewStyle(Args.TreeViewStyle)
.ScrollBarStyle(Args.ScrollBarStyle)
.OnGenerateRow_UObject(Implementer, &UListViewBaseT::HandleGenerateRow)
.OnSelectionChanged_UObject(Implementer, &UListViewBaseT::HandleSelectionChanged)
.OnIsSelectableOrNavigable_UObject(Implementer, &UListViewBaseT::HandleIsSelectableOrNavigable)
.OnRowReleased_UObject(Implementer, &UListViewBaseT::HandleRowReleased)
.OnItemScrolledIntoView_UObject(Implementer, &UListViewBaseT::HandleItemScrolledIntoView)
.OnTreeViewScrolled_UObject(Implementer, &UListViewBaseT::HandleListViewScrolled)
.OnFinishedScrolling_UObject(Implementer, &UListViewBaseT::HandleFinishedScrolling)
.OnMouseButtonClick_UObject(Implementer, &UListViewBaseT::HandleItemClicked)
.OnMouseButtonDoubleClick_UObject(Implementer, &UListViewBaseT::HandleItemDoubleClicked)
.OnGetChildren_UObject(Implementer, &UListViewBaseT::HandleGetChildren)
.OnExpansionChanged_UObject(Implementer, &UListViewBaseT::HandleExpansionChanged);
TreeView->BindToRefreshRow(TSlateDelegates< ItemType >::FOnRefreshRow::CreateUObject(Implementer, &UListViewBaseT::HandleRefreshRow));
return TreeView;
}
protected:
/** Gets the SObjectTableRow underlying the UMG EntryWidget that represents the given item (if one exists) */
template <template<typename> class ObjectRowT = SObjectTableRow>
TSharedPtr<ObjectRowT<ItemType>> GetObjectRowFromItem(const ItemType& Item) const
{
static_assert(TIsDerivedFrom<ObjectRowT<ItemType>, SObjectTableRow<ItemType>>::IsDerived, "All UMG table rows must be or derive from SObjectTableRow.");
if (SListView<ItemType>* MyListView = GetMyListView())
{
TSharedPtr<ITableRow> RowWidget = MyListView->WidgetFromItem(Item);
return StaticCastSharedPtr<ObjectRowT<ItemType>>(RowWidget);
}
return nullptr;
}
/**
* Generates the actual entry widget that represents the given item.
* Expected to be used in concert with UListViewBase::GenerateTypedEntry().
*/
virtual UUserWidget& OnGenerateEntryWidgetInternal(ItemType Item, TSubclassOf<UUserWidget> DesiredEntryClass, const TSharedRef<STableViewBase>& OwnerTable) = 0;
/** Gets the desired padding for the entry representing the given item */
virtual FMargin GetDesiredEntryPadding(ItemType Item) const { return FMargin(0.f); }
/** TreeViews only. Gets the items to consider children of the given item when generating child entries. */
virtual void OnGetChildrenInternal(ItemType Item, TArray<ItemType>& OutChildren) const {}
/** ListView events - implement these instead of binding handlers directly to a list */
virtual void OnItemClickedInternal(ItemType Item) {}
virtual void OnItemDoubleClickedInternal(ItemType Item) {}
virtual void OnSelectionChangedInternal(NullableItemType FirstSelectedItem) {}
virtual bool OnIsSelectableOrNavigableInternal(ItemType FirstSelectedItem) { return OnIsItemSelectableOrNavigable().IsBound() ? OnIsItemSelectableOrNavigable().Execute(FirstSelectedItem) : true; }
virtual void OnItemScrolledIntoViewInternal(ItemType Item, UUserWidget& EntryWidget) {}
virtual void OnListViewScrolledInternal(float ItemOffset, float DistanceRemaining) {}
virtual void OnListViewFinishedScrollingInternal() {}
virtual void OnItemExpansionChangedInternal(ItemType Item, bool bIsExpanded) {}
private:
TSharedRef<ITableRow> HandleGenerateRow(ItemType Item, const TSharedRef<STableViewBase>& OwnerTable)
{
TSubclassOf<UUserWidget> DesiredEntryClass = GetDesiredEntryClassForItem(Item);
UUserWidget& EntryWidget = OnGenerateEntryWidgetInternal(Item, DesiredEntryClass, OwnerTable);
// Combine the desired entry padding with the padding the widget wants natively on the CDO.
const FMargin DefaultPadding = EntryWidget.GetClass()->GetDefaultObject<UUserWidget>()->GetPadding();
EntryWidget.SetPadding(DefaultPadding + GetDesiredEntryPadding(Item));
TSharedPtr<SWidget> CachedWidget = EntryWidget.GetCachedWidget();
CachedWidget->SetCanTick(true); // this is a hack to force ticking to true so selection works (which should NOT require ticking! but currently does)
return StaticCastSharedPtr<SObjectTableRow<ItemType>>(CachedWidget).ToSharedRef();
}
void HandleRefreshRow(ItemType Item)
{
UUserWidget* RowWidget = GetEntryWidgetFromItem(Item);
if (RowWidget)
{
const FMargin DefaultPadding = RowWidget->GetClass()->GetDefaultObject<UUserWidget>()->GetPadding();
RowWidget->SetPadding(DefaultPadding + GetDesiredEntryPadding(Item));
}
}
void HandleItemClicked(ItemType Item)
{
OnItemClickedInternal(Item);
OnItemClicked().Broadcast(Item);
}
void HandleItemDoubleClicked(ItemType Item)
{
OnItemDoubleClickedInternal(Item);
OnItemDoubleClicked().Broadcast(Item);
}
void HandleSelectionChanged(NullableItemType Item, ESelectInfo::Type SelectInfo)
{
//@todo DanH ListView: This really isn't the event that many will expect it to be - is it worth having at all?
// It only works for single selection lists, and even then only broadcasts at the end - you don't get anything for de-selection
OnSelectionChangedInternal(Item);
OnItemSelectionChanged().Broadcast(Item);
}
bool HandleIsSelectableOrNavigable(ItemType Item)
{
return OnIsSelectableOrNavigableInternal(Item);
}
void HandleListViewScrolled(double OffsetInItems)
{
if (SListView<ItemType>* MyListView = GetMyListView())
{
const FVector2f DistanceRemaining = UE::Slate::CastToVector2f(MyListView->GetScrollDistanceRemaining());
OnListViewScrolledInternal(static_cast<float>(OffsetInItems), DistanceRemaining.Y);
OnListViewScrolled().Broadcast(static_cast<float>(OffsetInItems), DistanceRemaining.Y);
}
}
void HandleFinishedScrolling()
{
OnListViewFinishedScrollingInternal();
OnFinishedScrolling().Broadcast();
}
void HandleItemScrolledIntoView(ItemType Item, const TSharedPtr<ITableRow>& InWidget)
{
UUserWidget* RowWidget = GetEntryWidgetFromItem(Item);
if (ensure(RowWidget))
{
OnItemScrolledIntoViewInternal(Item, *RowWidget);
OnItemScrolledIntoView().Broadcast(Item, *RowWidget);
}
}
void HandleExpansionChanged(ItemType Item, bool bIsExpanded)
{
// If this item is currently visible (i.e. has a widget representing it), notify the widget of the expansion change
auto ObjectRow = GetObjectRowFromItem(Item);
if (ObjectRow.IsValid())
{
ObjectRow->NotifyItemExpansionChanged(bIsExpanded);
}
OnItemExpansionChangedInternal(Item, bIsExpanded);
OnItemExpansionChanged().Broadcast(Item, bIsExpanded);
}
void HandleGetChildren(ItemType Item, TArray<ItemType>& OutChildren) const
{
OnGetChildrenInternal(Item, OutChildren);
}
};
//////////////////////////////////////////////////////////////////////////
// UListViewBase
//////////////////////////////////////////////////////////////////////////
/**
* Bare-bones base class to make creating custom UListView widgets easier.
* Child classes should also inherit from ITypedUMGListView<T> to get a basic public ListView API for free.
*
* Child classes will own the actual SListView<T> widgets, but this provides some boilerplate functionality for generating entries.
* To generate a row for the child list, use GenerateTypedRow with the appropriate SObjectTableRow<T> type for your list
*
* Additionally, the entry widget class can be filtered for a particular class and interface with the EntryClass and EntryInterface metadata arguments
* This can be specified either on the class directly (see below) or on any BindWidget FProperty
*
* Example:
* class UMyUserWidget : public UUserWidget
* {
* UPROPERTY(BindWidget, meta = (EntryClass = MyListEntryWidget))
* UListView* ListView_InventoryItems;
* }
*
*/
UCLASS(Abstract, NotBlueprintable, hidedropdown, meta = (EntryInterface = UserListEntry), MinimalAPI)
class UListViewBase : public UWidget
{
GENERATED_BODY()
public:
UMG_API UListViewBase(const FObjectInitializer& ObjectInitializer);
#if WITH_EDITOR
UMG_API virtual const FText GetPaletteCategory() override;
UMG_API virtual void ValidateCompiledDefaults(IWidgetCompilerLog& CompileLog) const override;
#endif
TSubclassOf<UUserWidget> GetEntryWidgetClass() const { return EntryWidgetClass; }
/** Gets all of the list entry widgets currently being displayed by the list */
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API const TArray<UUserWidget*>& GetDisplayedEntryWidgets() const;
/** Get the scroll offset of this view (in items) */
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API float GetScrollOffset() const;
/** Get the corresponding list object for this userwidget entry. Override this to call ITypedUMGListView::ItemFromEntryWidget in concrete widgets. */
virtual UObject* GetListObjectFromEntry(UUserWidget& EntryWidget) { return nullptr; }
/**
* Full regeneration of all entries in the list. Note that the entry UWidget instances will not be destroyed, but they will be released and re-generated.
* In other words, entry widgets will not receive Destruct/Construct events. They will receive OnEntryReleased and IUserObjectListEntry implementations will receive OnListItemObjectSet.
*/
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void RegenerateAllEntries();
/** Scroll the entire list up to the first item */
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void ScrollToTop();
/** Scroll the entire list down to the bottom-most item */
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void ScrollToBottom();
/** Set the scroll offset of this view (in items) */
UFUNCTION(BlueprintCallable, Category = ListView)
UMG_API void SetScrollOffset(const float InScrollOffset);
/** Stops the scroll inertia */
UFUNCTION(BlueprintCallable, Category = ListView)
UMG_API void EndInertialScrolling();
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void SetWheelScrollMultiplier(float NewWheelScrollMultiplier);
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void SetScrollbarVisibility(ESlateVisibility InVisibility);
/** Enable/Disable the ability of the list to scroll. This should be use as a temporary disable. */
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void SetIsPointerScrollingEnabled(bool bInIsPointerScrollingEnabled);
/** Enable/Disable the ability of the list to scroll via gamepad. */
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void SetIsGamepadScrollingEnabled(bool bInIsGamepadScrollingEnabled);
/**
* Sets the list to refresh on the next tick.
*
* Note that refreshing, from a list perspective, is limited to accounting for discrepancies between items and entries.
* In other words, it will only release entries that no longer have items and generate entries for new items (or newly visible items).
*
* It does NOT account for changes within existing items - that is up to the item to announce and an entry to listen to as needed.
* This can be onerous to set up for simple cases, so it's also reasonable (though not ideal) to call RegenerateAllEntries when changes within N list items need to be reflected.
*/
UFUNCTION(BlueprintCallable, Category = ListViewBase)
UMG_API void RequestRefresh();
DECLARE_EVENT_OneParam(UListView, FOnListEntryGenerated, UUserWidget&);
FOnListEntryGenerated& OnEntryWidgetGenerated() { return OnListEntryGeneratedEvent; }
DECLARE_EVENT_OneParam(UListView, FOnEntryWidgetReleased, UUserWidget&);
FOnEntryWidgetReleased& OnEntryWidgetReleased() { return OnEntryWidgetReleasedEvent; }
protected:
UMG_API virtual TSharedRef<SWidget> RebuildWidget() override final;
UMG_API virtual void ReleaseSlateResources(bool bReleaseChildren) override;
UMG_API virtual void SynchronizeProperties() override;
/** Implement in child classes to construct the actual ListView Slate widget */
UMG_API virtual TSharedRef<STableViewBase> RebuildListWidget();
//@todo DanH: Should probably have the events for native & BP built in up here - need to update existing binds to UListView's version
virtual void HandleListEntryHovered(UUserWidget& EntryWidget) {}
virtual void HandleListEntryUnhovered(UUserWidget& EntryWidget) {}
UMG_API virtual void FinishGeneratingEntry(UUserWidget& GeneratedEntry);
/** Called when a row widget is generated for a list item */
UPROPERTY(BlueprintAssignable, Category = Events, meta = (DisplayName = "On Entry Generated"))
FOnListEntryGeneratedDynamic BP_OnEntryGenerated;
virtual void NativeOnEntryGenerated(UUserWidget* EntryWidget) {};
/** Called when all row widgets are generated for all list items */
UPROPERTY(BlueprintAssignable, Category = Events, meta = (DisplayName = "On Entries Generated"))
FOnListEntriesGeneratedDynamic BP_OnEntriesGenerated;
virtual void NativeOnEntriesGenerated() {};
/**
* Normally these are processed by UListViewBase::FinishGeneratingEntry which uses World->GetTimerManager() to generate entries next frame
* However when for example using a listview in editor utility widgets a world there is not reliable and an alternative is to use
* FTSTicker::GetCoreTicker()
*/
TArray<TWeakObjectPtr<UUserWidget>> GeneratedEntriesToAnnounce;
template <typename WidgetEntryT = UUserWidget, typename ObjectTableRowT = SObjectTableRow<UObject*>>
WidgetEntryT& GenerateTypedEntry(TSubclassOf<WidgetEntryT> WidgetClass, const TSharedRef<STableViewBase>& OwnerTable)
{
static_assert(TIsDerivedFrom<ObjectTableRowT, ITableRow>::IsDerived && TIsDerivedFrom<ObjectTableRowT, SObjectWidget>::IsDerived,
"GenerateObjectTableRow can only be used to create SObjectWidget types that also inherit from ITableRow. See SObjectTableRow.");
WidgetEntryT* ListEntryWidget = EntryWidgetPool.GetOrCreateInstance<WidgetEntryT>(*WidgetClass,
[this, &OwnerTable] (UUserWidget* WidgetObject, TSharedRef<SWidget> Content)
{
return SNew(ObjectTableRowT, OwnerTable, *WidgetObject, this)
.bAllowDragging(bAllowDragging)
.OnHovered_UObject(this, &UListViewBase::HandleListEntryHovered)
.OnUnhovered_UObject(this, &UListViewBase::HandleListEntryUnhovered)
[
Content
];
});
check(ListEntryWidget);
FinishGeneratingEntry(*ListEntryWidget);
return *ListEntryWidget;
}
#if WITH_EDITOR
/**
* Called during design time to allow lists to generate preview entries via dummy data.
* Since that data could be of any type, the child has to be the one to generate them.
*
* Expected to call RefreshDesignerItems<T> with the appropriate T for your underlying list
* @see UListView::OnRefreshDesignerItems for a usage example
*/
virtual void OnRefreshDesignerItems() {}
/**
* Helper intended to be called by overrides of OnRefreshDesignerItems.
* @see UListView::OnRefreshDesignerItems for a usage example
*/
template <typename PlaceholderItemT>
void RefreshDesignerItems(TArray<PlaceholderItemT>& ListItems, TFunctionRef<PlaceholderItemT()> CreateItemFunc)
{
bNeedsToCallRefreshDesignerItems = false;
bool bRefresh = false;
if (EntryWidgetClass && NumDesignerPreviewEntries > 0 && EntryWidgetClass->ImplementsInterface(UUserListEntry::StaticClass()))
{
if (ListItems.Num() < NumDesignerPreviewEntries)
{
while (ListItems.Num() < NumDesignerPreviewEntries)
{
ListItems.Add(CreateItemFunc());
}
bRefresh = true;
}
else if (ListItems.Num() > NumDesignerPreviewEntries)
{
const int32 NumExtras = ListItems.Num() - NumDesignerPreviewEntries;
ListItems.RemoveAtSwap(ListItems.Num() - (NumExtras + 1), NumExtras);
bRefresh = true;
}
}
else
{
ListItems.Reset();
bRefresh = true;
}
if (bRefresh)
{
RequestRefresh();
}
}
#endif
/** Expected to be bound to the actual ListView widget created by a child class (automatically taken care of via the construction helpers within ITypedUMGListView) */
UMG_API void HandleRowReleased(const TSharedRef<ITableRow>& Row);
// Note: Options for this property can be configured via class and property metadata. See class declaration comment above.
/** The type of widget to create for each entry displayed in the list. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ListEntries, meta = (DesignerRebuild, AllowPrivateAccess = true, MustImplement = "/Script/UMG.UserListEntry"))
TSubclassOf<UUserWidget> EntryWidgetClass;
/** The multiplier to apply when wheel scrolling */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Scrolling)
float WheelScrollMultiplier = 1.f;
/** True to enable lerped animation when scrolling through the list */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Scrolling)
bool bEnableScrollAnimation = false;
/** The speed to apply when lerping in the scroll animation. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Scrolling)
float ScrollingAnimationInterpolationSpeed = 12.f;
/** True to enable lerped animation when scrolling through the list with touch*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Scrolling, DisplayName = "Enable Touch Animated Scrolling")
bool bInEnableTouchAnimatedScrolling = false;
/** Disable to stop scrollbars from activating inertial overscrolling */
UPROPERTY(EditAnywhere, Category = Scrolling)
bool AllowOverscroll = true;
/** True to allow right click drag scrolling. */
UPROPERTY(EditAnywhere, Category = Scrolling)
bool bEnableRightClickScrolling = true;
/** True to allow scrolling using touch input. */
UPROPERTY(EditAnywhere, Category = Scrolling)
bool bEnableTouchScrolling = true;
/** Enable/Disable scrolling using Touch or Mouse. */
UPROPERTY(EditDefaultsOnly, Category = Scrolling)
bool bIsPointerScrollingEnabled = true;
/** Enable/Disable scrolling using Gamepad. */
UPROPERTY(EditDefaultsOnly, Category = Scrolling)
bool bIsGamepadScrollingEnabled = true;
UPROPERTY(EditAnywhere, Category = Scrolling)
bool bEnableFixedLineOffset = false;
/**
* Optional fixed offset (in lines) to always apply to the top/left (depending on orientation) of the list.
* If provided, all non-inertial means of scrolling will settle with exactly this offset of the topmost entry.
* Ex: A value of 0.25 would cause the topmost full entry to be offset down by a quarter length of the preceeding entry.
*/
UPROPERTY(EditAnywhere, Category = Scrolling, meta = (EditCondition = bEnableFixedLineOffset, ClampMin = 0.0f, ClampMax = 0.5f))
float FixedLineScrollOffset = 0.f;
/** True to allow dragging of row widgets in the list */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Input)
bool bAllowDragging = true;
/** If true, items will be "selected" (in addition to focused) when navigating to them. If false, they will only be focused. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = ListView)
bool bSelectItemOnNavigation = true;
/** Called when a row widget is released by the list (i.e. when it no longer represents a list item) */
UPROPERTY(BlueprintAssignable, Category = Events, meta = (DisplayName = "On Entry Released"))
FOnListEntryReleasedDynamic BP_OnEntryReleased;
virtual void NativeOnEntryReleased(UUserWidget* EntryWidget) {};
private:
UMG_API virtual void HandleAnnounceGeneratedEntries();
#if WITH_EDITORONLY_DATA
bool bNeedsToCallRefreshDesignerItems = false;;
/** The number of dummy item entry widgets to preview in the widget designer */
UPROPERTY(EditAnywhere, Category = ListEntries, meta = (ClampMin = 0, ClampMax = 20))
int32 NumDesignerPreviewEntries = 5;
#endif
UPROPERTY(Transient)
FUserWidgetPool EntryWidgetPool;
FTimerHandle EntryGenAnnouncementTimerHandle;
FOnListEntryGenerated OnListEntryGeneratedEvent;
FOnEntryWidgetReleased OnEntryWidgetReleasedEvent;
TSharedPtr<STableViewBase> MyTableViewBase;
friend class FListViewBaseDetails;
};
#define IMPLEMENT_TYPED_UMG_LIST(ItemType, ListPropertyName) \
protected: \
virtual SListView<ItemType>* GetMyListView() const override { return ListPropertyName.Get(); } \
virtual uint32 GetOwningUserIndex() const override \
{ \
const ULocalPlayer* LocalPlayer = GetOwningLocalPlayer(); \
int32 SlateUserIndex = LocalPlayer ? FSlateApplication::Get().GetUserIndexForController(LocalPlayer->GetControllerId()) : 0; \
return SlateUserIndex >= 0 ? SlateUserIndex : 0; \
} \
virtual bool IsDesignerPreview() const override { return IsDesignTime(); } \
private: \
friend class ITypedUMGListView<ItemType>; \
mutable FSimpleListItemEvent OnItemClickedEvent; \
mutable FSimpleListItemEvent OnItemDoubleClickedEvent; \
mutable FOnItemSelectionChanged OnItemSelectionChangedEvent; \
mutable FOnItemIsHoveredChanged OnItemIsHoveredChangedEvent; \
mutable FOnItemScrolledIntoView OnItemScrolledIntoViewEvent; \
mutable FOnListViewScrolled OnListViewScrolledEvent; \
mutable FOnFinishedScrolling OnFinishedScrollingEvent; \
mutable FOnItemExpansionChanged OnItemExpansionChangedEvent; \
mutable FOnGetEntryClassForItem OnGetEntryClassForItemDelegate; \
mutable FOnIsItemSelectableOrNavigable OnIsItemSelectableOrNavigableDelegate; \
public: \
virtual TSubclassOf<UUserWidget> GetDefaultEntryClass() const override { return EntryWidgetClass; } \
virtual FSimpleListItemEvent& OnItemClicked() const override { return OnItemClickedEvent; } \
virtual FSimpleListItemEvent& OnItemDoubleClicked() const override { return OnItemDoubleClickedEvent; } \
virtual FOnItemIsHoveredChanged& OnItemIsHoveredChanged() const override { return OnItemIsHoveredChangedEvent; } \
virtual FOnItemSelectionChanged& OnItemSelectionChanged() const override { return OnItemSelectionChangedEvent; } \
virtual FOnItemScrolledIntoView& OnItemScrolledIntoView() const override { return OnItemScrolledIntoViewEvent; } \
virtual FOnListViewScrolled& OnListViewScrolled() const override { return OnListViewScrolledEvent; } \
virtual FOnFinishedScrolling& OnFinishedScrolling() const override { return OnFinishedScrollingEvent; } \
virtual FOnItemExpansionChanged& OnItemExpansionChanged() const override { return OnItemExpansionChangedEvent; } \
virtual FOnGetEntryClassForItem& OnGetEntryClassForItem() const override { return OnGetEntryClassForItemDelegate; } \
virtual FOnIsItemSelectableOrNavigable& OnIsItemSelectableOrNavigable() const override { return OnIsItemSelectableOrNavigableDelegate; }