// Copyright Epic Games, Inc. All Rights Reserved #pragma once #include "CoreMinimal.h" #include "InputCoreTypes.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "Input/Reply.h" #include "Framework/SlateDelegates.h" #include "Widgets/Views/STableViewBase.h" #include "Styling/SlateTypes.h" #include "Framework/Views/TableViewTypeTraits.h" #include "Widgets/Views/STableRow.h" #include "Widgets/Text/STextBlock.h" #include "Framework/Layout/Overscroll.h" #include "Widgets/Views/SListView.h" #include "Algo/Reverse.h" #include "Math/NumericLimits.h" /** Info needed by a (relatively) small fraction of the tree items; some of them may not be visible. */ struct FSparseItemInfo { /** * Construct a new FTreeItem. * * @param InItemToVisualize The DateItem pointer being wrapped by this FTreeItem * @param InHasChildren Does this item have children? True if yes. */ FSparseItemInfo( bool InIsExpanded, bool InHasExpandedChildren ) : bIsExpanded( InIsExpanded ) , bHasExpandedChildren( InHasExpandedChildren ) { } /** Is this tree item expanded? */ bool bIsExpanded; /** Does this tree item have any expanded children? */ bool bHasExpandedChildren; }; /** Info needed by every visible item in the tree */ struct FItemInfo { FItemInfo() { } FItemInfo(TBitArray<> InNeedsVerticalWire, bool InHasChildren, bool InIsLastChild, int32 InParentIndex ) : NeedsVerticalWire(InNeedsVerticalWire) , bHasChildren( InHasChildren ) , bIsLastChild( InIsLastChild ) , ParentIndex( InParentIndex ) { } /** * Flags for whether we need a wire drawn for this level of the tree. * * NeedsVerticalWrite.Num() is the nesting level within the tree. e.g. 0 is root-level, 1 is children of root, etc. */ TBitArray<> NeedsVerticalWire; int32 GetNestingLevel() const { return NeedsVerticalWire.Num()-1; } /** Does this tree item have children? */ uint32 bHasChildren : 1; /** Is this the last child of its parent? If so, it gets a special kind of wire/connector. */ uint32 bIsLastChild : 1; /** Index into the linearized tree of the parent for this item, if any, otherwise INDEX_NONE. */ int32 ParentIndex; }; /** * This assumes you are familiar with SListView; see SListView. * * TreeView setup is virtually identical to that of ListView. * Additionally, TreeView introduces a new delegate: OnGetChildren(). * OnGetChildren() takes some DataItem being observed by the tree * and returns that item's children. Like ListView, TreeView operates * exclusively with pointers to DataItems. * */ template class STreeView : public SListView< ItemType > { public: using NullableItemType = typename TListTypeTraits< ItemType >::NullableType; using MapKeyFuncs = typename TListTypeTraits< ItemType >::MapKeyFuncs; using MapKeyFuncsSparse = typename TListTypeTraits< ItemType >::MapKeyFuncsSparse; using TSparseItemMap = TMap< ItemType, FSparseItemInfo, FDefaultSetAllocator, MapKeyFuncsSparse >; using TItemSet = TSet< TObjectPtrWrapTypeOf, typename TListTypeTraits< TObjectPtrWrapTypeOf >::SetKeyFuncs >; using FOnGetChildren = typename TSlateDelegates< ItemType >::FOnGetChildren; using FOnGenerateRow = typename TSlateDelegates< ItemType >::FOnGenerateRow; using FOnRefreshRow = typename TSlateDelegates< ItemType >::FOnRefreshRow; using FOnSetExpansionRecursive = typename TSlateDelegates< ItemType >::FOnSetExpansionRecursive; using FOnItemScrolledIntoView = typename TSlateDelegates< ItemType >::FOnItemScrolledIntoView; using FOnSelectionChanged = typename TSlateDelegates< NullableItemType >::FOnSelectionChanged; using FIsSelectableOrNavigable = typename TSlateDelegates< ItemType >::FIsSelectableOrNavigable; using FOnMouseButtonClick = typename TSlateDelegates< ItemType >::FOnMouseButtonClick; using FOnMouseButtonDoubleClick = typename TSlateDelegates< ItemType >::FOnMouseButtonDoubleClick; using FOnExpansionChanged = typename TSlateDelegates< ItemType >::FOnExpansionChanged; using FOnItemToString_Debug = typename TSlateDelegates< ItemType >::FOnItemToString_Debug; using FOnWidgetToBeRemoved = typename SListView< ItemType >::FOnWidgetToBeRemoved; public: SLATE_BEGIN_ARGS( STreeView ) : _TreeViewStyle(&FAppStyle::Get().GetWidgetStyle("TreeView")) , _OnGenerateRow() , _OnGeneratePinnedRow() , _OnRefreshRow() , _OnGetChildren() , _OnSetExpansionRecursive() , _MaxPinnedItems(6) // Having more than the max amount of items leads to the extra items in the middle being collapsed into ellipses, and the last item is fully shown , _OnContextMenuOpening() , _OnItemsRebuilt() , _OnMouseButtonClick() , _OnMouseButtonDoubleClick() , _OnSelectionChanged() , _OnExpansionChanged() , _OnIsSelectableOrNavigable() , _SelectionMode(ESelectionMode::Multi) , _ClearSelectionOnClick(true) , _ExternalScrollbar() , _EnableAnimatedScrolling(false) , _ScrollbarDragFocusCause(EFocusCause::Mouse) , _ConsumeMouseWheel( EConsumeMouseWheel::WhenScrollingPossible ) , _AllowOverscroll(EAllowOverscroll::Yes) , _ScrollBarStyle(&FAppStyle::Get().GetWidgetStyle("ScrollBar")) , _PreventThrottling(false) , _WheelScrollMultiplier(GetGlobalScrollAmount()) , _OnItemToString_Debug() , _OnEnteredBadState() , _HandleGamepadEvents(true) , _HandleDirectionalNavigation(true) , _AllowInvisibleItemSelection(false) , _HighlightParentNodesForSelection(false) , _ReturnFocusToSelection() , _ShouldStackHierarchyHeaders(false) { this->_Clipping = EWidgetClipping::ClipToBounds; } SLATE_STYLE_ARGUMENT( FTableViewStyle, TreeViewStyle ) SLATE_EVENT( FOnGenerateRow, OnGenerateRow ) SLATE_EVENT( FOnGenerateRow, OnGeneratePinnedRow ) SLATE_EVENT( FOnRefreshRow, OnRefreshRow ) SLATE_EVENT( FOnWidgetToBeRemoved, OnRowReleased ) SLATE_EVENT( FOnTableViewScrolled, OnTreeViewScrolled ) SLATE_EVENT( FOnFinishedScrolling, OnFinishedScrolling ) SLATE_EVENT( FOnItemScrolledIntoView, OnItemScrolledIntoView ) SLATE_EVENT( FOnGetChildren, OnGetChildren ) SLATE_EVENT( FOnSetExpansionRecursive, OnSetExpansionRecursive ) SLATE_ITEMS_SOURCE_ARGUMENT( ItemType, TreeItemsSource ) SLATE_ATTRIBUTE_DEPRECATED(float, ItemHeight, 5.5, "The ItemHeight is only used for Tile. See ShouldArrangeAsTiles") SLATE_ATTRIBUTE( int32, MaxPinnedItems ); SLATE_EVENT( FOnContextMenuOpening, OnContextMenuOpening ) SLATE_EVENT( FSimpleDelegate, OnItemsRebuilt ) SLATE_EVENT( FOnMouseButtonClick, OnMouseButtonClick) SLATE_EVENT( FOnMouseButtonDoubleClick, OnMouseButtonDoubleClick ) SLATE_EVENT( FOnSelectionChanged, OnSelectionChanged ) SLATE_EVENT( FOnExpansionChanged, OnExpansionChanged ) SLATE_EVENT( FIsSelectableOrNavigable, OnIsSelectableOrNavigable) SLATE_ATTRIBUTE( ESelectionMode::Type, SelectionMode ) SLATE_ARGUMENT( TSharedPtr, HeaderRow ) SLATE_ARGUMENT ( bool, ClearSelectionOnClick ) SLATE_ARGUMENT( TSharedPtr, ExternalScrollbar ) SLATE_ARGUMENT( bool, EnableAnimatedScrolling) SLATE_ARGUMENT( TOptional, FixedLineScrollOffset ) SLATE_ARGUMENT( EFocusCause, ScrollbarDragFocusCause ) SLATE_ARGUMENT( EConsumeMouseWheel, ConsumeMouseWheel ); SLATE_ARGUMENT( EAllowOverscroll, AllowOverscroll ); SLATE_STYLE_ARGUMENT( FScrollBarStyle, ScrollBarStyle ); SLATE_ARGUMENT( bool, PreventThrottling ) SLATE_ARGUMENT( float, WheelScrollMultiplier ); /** Assign this to get more diagnostics from the list view. */ SLATE_EVENT(FOnItemToString_Debug, OnItemToString_Debug) SLATE_EVENT(FOnTableViewBadState, OnEnteredBadState); SLATE_ARGUMENT(bool, HandleGamepadEvents); SLATE_ARGUMENT(bool, HandleDirectionalNavigation); SLATE_ARGUMENT(bool, AllowInvisibleItemSelection); SLATE_ARGUMENT(bool, HighlightParentNodesForSelection); SLATE_ARGUMENT(bool, ReturnFocusToSelection) /** If true, Show the current hierarchy of items pinned at the top of the Tree View */ SLATE_ATTRIBUTE(bool, ShouldStackHierarchyHeaders) /** Callback delegate to have first chance handling of the OnKeyDown event */ SLATE_EVENT(FOnKeyDown, OnKeyDownHandler) SLATE_END_ARGS() /** * Construct this widget * * @param InArgs The declaration data for this widget. */ void Construct( const FArguments& InArgs ) { this->Clipping = InArgs._Clipping; this->OnGenerateRow = InArgs._OnGenerateRow; this->OnGeneratePinnedRow = InArgs._OnGeneratePinnedRow; this->OnRefreshRow = InArgs._OnRefreshRow; this->OnRowReleased = InArgs._OnRowReleased; this->OnItemScrolledIntoView = InArgs._OnItemScrolledIntoView; this->OnGetChildren = InArgs._OnGetChildren; this->OnSetExpansionRecursive = InArgs._OnSetExpansionRecursive; this->SetRootItemsSource(InArgs.MakeTreeItemsSource(this->SharedThis(this))); this->OnKeyDownHandler = InArgs._OnKeyDownHandler; this->OnContextMenuOpening = InArgs._OnContextMenuOpening; this->OnItemsRebuilt = InArgs._OnItemsRebuilt; this->OnClick = InArgs._OnMouseButtonClick; this->OnDoubleClick = InArgs._OnMouseButtonDoubleClick; this->OnSelectionChanged = InArgs._OnSelectionChanged; this->OnExpansionChanged = InArgs._OnExpansionChanged; this->OnIsSelectableOrNavigable = InArgs._OnIsSelectableOrNavigable; this->SelectionMode = InArgs._SelectionMode; this->bClearSelectionOnClick = InArgs._ClearSelectionOnClick; this->ConsumeMouseWheel = InArgs._ConsumeMouseWheel; this->AllowOverscroll = InArgs._AllowOverscroll; this->WheelScrollMultiplier = InArgs._WheelScrollMultiplier; 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->bHandleGamepadEvents = InArgs._HandleGamepadEvents; this->bHandleDirectionalNavigation = InArgs._HandleDirectionalNavigation; this->bAllowInvisibleItemSelection = InArgs._AllowInvisibleItemSelection; this->bHighlightParentNodesForSelection = InArgs._HighlightParentNodesForSelection; this->bReturnFocusToSelection = InArgs._ReturnFocusToSelection; this->bShouldStackHierarchyHeaders = InArgs._ShouldStackHierarchyHeaders; this->SetStyle(InArgs._TreeViewStyle); this->MaxPinnedItems = InArgs._MaxPinnedItems; this->DefaultMaxPinnedItems = InArgs._MaxPinnedItems; // Check for any parameters that the coder forgot to specify. FString ErrorString; { if ( !this->OnGenerateRow.IsBound() ) { ErrorString += TEXT("Please specify an OnGenerateRow. \n"); } if (!this->HasValidRootItemsSource()) { ErrorString += TEXT("Please specify a TreeItemsSource. \n"); } if ( !this->OnGetChildren.IsBound() ) { ErrorString += TEXT("Please specify an OnGetChildren. \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( 0.0f, 0.0f, EListItemAlignment::LeftAligned, InArgs._HeaderRow, InArgs._ExternalScrollbar, Orient_Vertical, InArgs._OnTreeViewScrolled, InArgs._ScrollBarStyle, InArgs._PreventThrottling ); if (this->ScrollBar.IsValid()) { this->ScrollBar->SetDragFocusCause(InArgs._ScrollbarDragFocusCause); } this->AddMetadata(MakeShared>(this->SharedThis(this))); } } /** Default constructor. */ STreeView() : SListView< ItemType >( ETableViewMode::Tree ) , bTreeItemsAreDirty( true ) { SListView::SetItemsSource(&LinearizedItems); } public: //~ SWidget overrides virtual FReply OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent ) override { if (this->OnKeyDownHandler.IsBound()) { FReply Reply = this->OnKeyDownHandler.Execute(MyGeometry, InKeyEvent); if (Reply.IsEventHandled()) { return Reply; } } // Check for selection/expansion toggling keys (Left, Right) // SelectorItem represents the keyboard selection. If it isn't valid then we don't know what to expand. // Don't respond to key-presses containing "Alt" as a modifier if ( TListTypeTraits::IsPtrValid(this->SelectorItem) && !InKeyEvent.IsAltDown() ) { if ( InKeyEvent.GetKey() == EKeys::Left ) { if( TListTypeTraits::IsPtrValid(this->SelectorItem) ) { ItemType RangeSelectionEndItem = TListTypeTraits::NullableItemTypeConvertToItemType( this->SelectorItem ); int32 SelectionIndex = this->LinearizedItems.Find( RangeSelectionEndItem ); if ( Private_DoesItemHaveChildren(SelectionIndex) && Private_IsItemExpanded( RangeSelectionEndItem ) ) { // Collapse the selected item Private_SetItemExpansion(RangeSelectionEndItem, false); } else { // Select the parent, who should be a previous item in the list whose nesting level is less than the selected one int32 SelectedNestingDepth = Private_GetNestingDepth(SelectionIndex); for (SelectionIndex--; SelectionIndex >= 0; --SelectionIndex) { if ( Private_GetNestingDepth(SelectionIndex) < SelectedNestingDepth ) { // Found the parent this->NavigationSelect(this->LinearizedItems[SelectionIndex], InKeyEvent); break; } } } } return FReply::Handled(); } else if ( InKeyEvent.GetKey() == EKeys::Right ) { if( TListTypeTraits::IsPtrValid(this->SelectorItem) ) { ItemType RangeSelectionEndItem = TListTypeTraits::NullableItemTypeConvertToItemType( this->SelectorItem ); int32 SelectionIndex = this->LinearizedItems.Find( RangeSelectionEndItem ); // Right only applies to items with children if ( Private_DoesItemHaveChildren(SelectionIndex) ) { if ( !Private_IsItemExpanded(RangeSelectionEndItem) ) { // Expand the selected item Private_SetItemExpansion(RangeSelectionEndItem, true); } else { // Select the first child, who should be the next item in the list // Make sure we aren't the last item on the list if ( SelectionIndex < this->LinearizedItems.Num() - 1 ) { this->NavigationSelect(this->LinearizedItems[SelectionIndex + 1], InKeyEvent); } } } } return FReply::Handled(); } } return SListView::OnKeyDown_Internal(MyGeometry, InKeyEvent); } private: //~ Tree View adds the ability to expand/collapse items. //~ All the selection functionality is inherited from ListView. virtual bool Private_IsItemExpanded( const ItemType& TheItem ) const override { const FSparseItemInfo* ItemInfo = SparseItemInfos.Find(TheItem); return ItemInfo != nullptr && ItemInfo->bIsExpanded; } virtual void Private_SetItemExpansion( ItemType TheItem, bool bShouldBeExpanded ) override { const FSparseItemInfo* const SparseItemInfo = SparseItemInfos.Find(TheItem); bool bWasExpanded = false; if(SparseItemInfo) { bWasExpanded = SparseItemInfo->bIsExpanded; SparseItemInfos.Add(TheItem, FSparseItemInfo(bShouldBeExpanded, SparseItemInfo->bHasExpandedChildren)); } else if(bShouldBeExpanded) { SparseItemInfos.Add(TheItem, FSparseItemInfo(bShouldBeExpanded, false)); } if(bWasExpanded != bShouldBeExpanded) { OnExpansionChanged.ExecuteIfBound(TheItem, bShouldBeExpanded); // We must rebuild the linearized version of the tree because // either some children became visible or some got removed. RequestTreeRefresh(); } } virtual void Private_OnExpanderArrowShiftClicked( ItemType TheItem, bool bShouldBeExpanded ) override { if(OnSetExpansionRecursive.IsBound()) { OnSetExpansionRecursive.Execute(TheItem, bShouldBeExpanded); // We must rebuild the linearized version of the tree because // either some children became visible or some got removed. RequestTreeRefresh(); } } virtual bool Private_DoesItemHaveChildren( int32 ItemIndexInList ) const override { bool bHasChildren = false; if (DenseItemInfos.IsValidIndex(ItemIndexInList)) { bHasChildren = DenseItemInfos[ItemIndexInList].bHasChildren; } return bHasChildren; } virtual int32 Private_GetNestingDepth( int32 ItemIndexInList ) const override { int32 NestingLevel = 0; if (DenseItemInfos.IsValidIndex(ItemIndexInList)) { NestingLevel = DenseItemInfos[ItemIndexInList].GetNestingLevel(); } return NestingLevel; } virtual const TBitArray<>& Private_GetWiresNeededByDepth(int32 ItemIndexInList) const override { return (DenseItemInfos.IsValidIndex(ItemIndexInList)) ? DenseItemInfos[ItemIndexInList].NeedsVerticalWire : TableViewHelpers::GetEmptyBitArray(); } virtual bool Private_IsLastChild(int32 ItemIndexInList) const override { return (DenseItemInfos.IsValidIndex(ItemIndexInList)) ? DenseItemInfos[ItemIndexInList].bIsLastChild : false; } /** * This clears the highlight state from all nodes and then traverses the parents of each selected node to add it to the highlighted set. */ virtual void Private_UpdateParentHighlights() { this->Private_ClearHighlightedItems(); for( typename TItemSet::TConstIterator SelectedItemIt(this->SelectedItems); SelectedItemIt; ++SelectedItemIt ) { // Sometimes selection events can come through before the Linearized List is built, so the item may not exist yet. int32 ItemIndex = LinearizedItems.Find(*SelectedItemIt); if (ItemIndex == INDEX_NONE) { continue; } if(DenseItemInfos.IsValidIndex(ItemIndex)) { const FItemInfo& ItemInfo = DenseItemInfos[ItemIndex]; int32 ParentIndex = ItemInfo.ParentIndex; while (ParentIndex != INDEX_NONE) { const ItemType& ParentItem = this->LinearizedItems[ParentIndex]; this->Private_SetItemHighlighted(ParentItem, true); const FItemInfo& ParentItemInfo = DenseItemInfos[ParentIndex]; ParentIndex = ParentItemInfo.ParentIndex; } } } } public: virtual void Private_SignalSelectionChanged(ESelectInfo::Type SelectInfo) override { SListView< ItemType >::Private_SignalSelectionChanged(SelectInfo); if (bHighlightParentNodesForSelection) { this->Private_UpdateParentHighlights(); } } /** * See SWidget::Tick() * * @param AllottedGeometry The space allotted for this widget. * @param InCurrentTime Current absolute real time. * @param InDeltaTime Real time passed since last tick. */ virtual void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override { if ( bTreeItemsAreDirty ) { // Check that ItemsPanel was made; we never make it if the user failed to specify all the parameters necessary to make the tree work. if ( this->ItemsPanel.IsValid() ) { // We are about to repopulate linearized items; the ListView that TreeView is built on top of will also need to refresh. bTreeItemsAreDirty = false; if ( OnGetChildren.IsBound() && HasValidRootItemsSource() ) { // We make copies of the old expansion and selection sets so that we can remove // any items that are no longer seen by the tree. TItemSet TempSelectedItemsMap; TSparseItemMap TempSparseItemInfo; TArray TempDenseItemInfos; // Rebuild the linearized view of the tree data. LinearizedItems.Empty(); PopulateLinearizedItems(GetRootItems(), LinearizedItems, TempDenseItemInfos, TBitArray<>(), TempSelectedItemsMap, TempSparseItemInfo, true, INDEX_NONE); if( !bAllowInvisibleItemSelection && (this->SelectedItems.Num() != TempSelectedItemsMap.Num() || this->SelectedItems.Difference(TempSelectedItemsMap).Num() > 0 || TempSelectedItemsMap.Difference(this->SelectedItems).Num() > 0 )) { this->SelectedItems = TempSelectedItemsMap; if ( !TListTypeTraits::IsPtrValid( this->RangeSelectionStart ) || !this->SelectedItems.Contains( TListTypeTraits::NullableItemTypeConvertToItemType( this->RangeSelectionStart ) )) { TListTypeTraits< ItemType >::ResetPtr( this->RangeSelectionStart ); TListTypeTraits< ItemType >::ResetPtr( this->SelectorItem ); } else if ( !TListTypeTraits::IsPtrValid( this->SelectorItem ) || !this->SelectedItems.Contains( TListTypeTraits::NullableItemTypeConvertToItemType( this->SelectorItem ) ) ) { this->SelectorItem = this->RangeSelectionStart; } this->Private_SignalSelectionChanged(ESelectInfo::Direct); } // these should come after Private_SignalSelectionChanged(); because through a // series of calls, Private_SignalSelectionChanged() could end up in a child // that indexes into either of these arrays (the index wouldn't be updated yet, // and could be invalid) SparseItemInfos = MoveTemp(TempSparseItemInfo); DenseItemInfos = MoveTemp(TempDenseItemInfos); // Once the selection changed events have gone through we can update the parent highlight statuses, which are based on your current selection. if (bHighlightParentNodesForSelection) { this->Private_UpdateParentHighlights(); } } } } // Tick the TreeView superclass so that it can refresh. // This may be due to TreeView requesting a refresh or because new items became visible due to resizing or scrolling. SListView< ItemType >::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); } /** * Given: an array of items (ItemsSource) each of which potentially has a child. * Task: populate the LinearizedItems array with a flattened version of the visible data items. * In the process, remove any items that are not visible while maintaining any collapsed * items that may have expanded children. * * @param ItemsSource An array of data items each of which may have 0 or more children. * @param LinearizedItems Array to populate with items based on expanded/collapsed state. * @param NewDenseItemInfos Array representing how nested each item in the Linearized items is, and whether it has children. * @param TreeLevel The current level of indentation. * @param OutNewSelectedItems Selected items minus any items that are no longer observed by the list. * @param NewSparseItemInfo Expanded items and items that have expanded children minus any items that are no longer observed by the list. * @param bAddingItems Are we adding encountered items to the linearized items list or just testing them for existence. * @param ParentIndex The index in the resulting linearized item list of the parent node for the currently processed level. * * @return true if we encountered expanded children; false otherwise. */ bool PopulateLinearizedItems( TArrayView InItemsSource, TArray& InLinearizedItems, TArray& NewDenseItemInfos, TBitArray<> NeedsParentWire, TItemSet& OutNewSelectedItems, TSparseItemMap& NewSparseItemInfo, bool bAddingItems, int32 ParentIndex) { NeedsParentWire.Add(false); const int32 NestingDepthIndex = NeedsParentWire.Num()-1; bool bSawExpandedItems = false; for ( int32 ItemIndex = 0; ItemIndex < InItemsSource.Num(); ++ItemIndex ) { const ItemType& CurItem = InItemsSource[ItemIndex]; // Find this items children. TArray ChildItems; OnGetChildren.Execute(InItemsSource[ItemIndex], ChildItems ); const bool bHasChildren = ChildItems.Num() > 0; // Child items will need a parent wire at this depth if the item we are inserting now is // not the last item in its immediate parent's list. const bool bIsLastChild = (ItemIndex == InItemsSource.Num() - 1); NeedsParentWire[NestingDepthIndex] = !bIsLastChild; // Is this item expanded, does it have expanded children? const FSparseItemInfo* CurItemInfo = SparseItemInfos.Find( CurItem ); const bool bIsExpanded = (CurItemInfo == nullptr) ? false : CurItemInfo->bIsExpanded; bool bHasExpandedChildren = (CurItemInfo == nullptr) ? false : CurItemInfo->bHasExpandedChildren; // Add this item to the linearized list and update the selection set. if (bAddingItems) { InLinearizedItems.Add( CurItem ); NewDenseItemInfos.Add( FItemInfo(NeedsParentWire, bHasChildren, bIsLastChild, ParentIndex) ); const bool bIsSelected = this->IsItemSelected( CurItem ); if (bIsSelected) { OutNewSelectedItems.Add( CurItem ); } } if ( bIsExpanded || bHasExpandedChildren ) { // If this item is expanded, we should process all of its children at the next indentation level. // If it is collapsed, process its children but do not add them to the linearized list. const bool bAddChildItems = bAddingItems && bIsExpanded; bHasExpandedChildren = PopulateLinearizedItems( ChildItems, InLinearizedItems, NewDenseItemInfos, NeedsParentWire, OutNewSelectedItems, NewSparseItemInfo, bAddChildItems, InLinearizedItems.Num() - 1); } if ( bIsExpanded || bHasExpandedChildren ) { // Update the item info for this tree item. NewSparseItemInfo.Add( CurItem, FSparseItemInfo( bIsExpanded, bHasExpandedChildren) ); } // If we encountered any expanded nodes along the way, the parent has expanded children. bSawExpandedItems = bSawExpandedItems || bIsExpanded || bHasExpandedChildren; } return bSawExpandedItems; } int32 PopulatePinnedItems(const TArray& InItemsSource, TArray< ItemType >& InPinnedItems, const STableViewBase::FReGenerateResults& Results) { // The value we return, to signify if we want the hierarchy to be collapsed even if it doesn't reach the max amount int32 MaxPinnedItemsOverride = -1; if (InItemsSource.IsEmpty()) { return MaxPinnedItemsOverride; } // Calculate the index of the first item in view int32 StartIndex = FMath::Clamp((int32)(FMath::FloorToDouble(Results.NewScrollOffset)), 0, InItemsSource.Num() - 1); int32 CurrentItemIndex = StartIndex; auto GetNonVisibleParents = [this, &InItemsSource, StartIndex](TArray& OutParents, int32 ItemIndex) { if (!DenseItemInfos.IsValidIndex(ItemIndex)) { return; } int32 ParentIndex = ItemIndex; // Walk through the list of parents of the current item until you reach the root do { ParentIndex = DenseItemInfos[ItemIndex].ParentIndex; // If the current item has a parent, and the parent is not visible, add the parent to the list of pinned items if (InItemsSource.IsValidIndex(ParentIndex) && ParentIndex < StartIndex) { OutParents.Add(InItemsSource[ParentIndex]); } ItemIndex = ParentIndex; } while (ParentIndex != INDEX_NONE); }; /* Special Case for if we are at the end of the list. When there is no space to scroll down in a list, changing the pinned hierarchy could also change the first visible item * which is used to calculate the pinned hierarchy. This leads to an infinite loop, so we solve this by finding a first visible item that has a hierachy large enough to hide * itself, and then collapse the hierarchy until the item remains the first visible item (so there are no infinite loops since the first visible item doesn't change) * */ if (Results.bGeneratedPastLastItem && Results.NewScrollOffset > 0) { int32 LastItem = InItemsSource.Num() - 1; int32 CurrentMaxPinnedItems = this->MaxPinnedItems.Get(); // Could be different than reported by STableViewBase if some items are collapsed int32 NumPinnedItems = (this->GetNumPinnedItems() < CurrentMaxPinnedItems) ? this->GetNumPinnedItems() : CurrentMaxPinnedItems; // This is the first item that would be visible, if there were no pinned rows int32 FirstItem = FMath::TruncToInt32(Results.NewScrollOffset - NumPinnedItems); // We find items that have a hierarchy big enough to cover themselves, but select the smallest among them int32 MinSpaceOccupied = TNumericLimits::Max(); // The index of the item we select int32 MinIndex = -1; for (int32 ItemIndex = FirstItem; ItemIndex <= LastItem; ItemIndex++) { // Get all parents of the current item that are not visible, to calculate the number of items in its hierarchy TArray NonVisibleParents; GetNonVisibleParents(NonVisibleParents, ItemIndex); int32 NumParents = NonVisibleParents.Num(); // How many items would be required in the hierarchy to cover the item itself int32 IndexOffset = ItemIndex - FirstItem; // If the hierarchy is too small, ignore it if (NumParents < IndexOffset) { continue; } // If hierarchy is the smallest we have found so far, AND the number of pinned items it will require is < the allowed max if (NumParents - IndexOffset < MinSpaceOccupied && IndexOffset <= CurrentMaxPinnedItems) { MinSpaceOccupied = NumParents - IndexOffset; MinIndex = ItemIndex; } } // If we found no such items, we are in the middle of generating the list so pinned rows are not required if (MinIndex == -1) { return MaxPinnedItemsOverride; } CurrentItemIndex = MinIndex; MaxPinnedItemsOverride = MinIndex - FirstItem; } // Get all the parents of the item that are not visible, which is the hierarchy to stack GetNonVisibleParents(InPinnedItems, CurrentItemIndex); // Reverse the list so the root is at the front Algo::Reverse(InPinnedItems); return MaxPinnedItemsOverride; } /** * Given a TreeItem, create a Widget to represent it in the tree view. * * @param InItem Item to visualize. * @return A widget that represents this item. */ virtual TSharedRef GenerateNewWidget( ItemType InItem ) override { if ( this->OnGenerateRow.IsBound() ) { return this->OnGenerateRow.Execute( InItem, this->SharedThis(this) ); } else { TSharedRef< STreeView > This = StaticCastSharedRef< STreeView >(this->AsShared()); // The programmer did not provide an OnGenerateRow() handler; let them know. TSharedRef< STableRow > NewTreeItemWidget = SNew( STableRow, This ) .Content() [ SNew(STextBlock) .Text( NSLOCTEXT("STreeView", "BrokenSetupMessage", "OnGenerateWidget() not assigned.") ) ]; return NewTreeItemWidget; } } /** Queue up a regeneration of the linearized items on the next tick. */ virtual void RequestListRefresh() override { if (!bTreeItemsAreDirty) { bTreeItemsAreDirty = true; SListView::RequestListRefresh(); } } void RequestTreeRefresh() { RequestListRefresh(); } virtual void RebuildList() override { LinearizedItems.Empty(); SListView::RebuildList(); } void SetStyle(const FTableViewStyle* InStyle) { Style = InStyle; STableViewBase::SetBackgroundBrush( Style != nullptr ? &Style->BackgroundBrush : FStyleDefaults::GetNoBrush() ); } /** * Set whether some data item is expanded or not. * * @param InItem The item whose expansion state to control. * @param InExpandItem If true the item should be expanded; otherwise collapsed. */ void SetItemExpansion( const ItemType& InItem, bool InShouldExpandItem ) { Private_SetItemExpansion(InItem, InShouldExpandItem); } /** Collapse all the items in the tree and expand InItem */ void SetSingleExpandedItem( const ItemType& InItem ) { // Will we have to do any work? const bool bItemAlreadyLoneExpanded = (this->SparseItemInfos.Num() == 1) && this->IsItemExpanded(InItem); if (!bItemAlreadyLoneExpanded) { this->SparseItemInfos.Empty(); Private_SetItemExpansion(InItem, true); } } /** * @param InItem The data item whose expansion state to query. * * @return true if the item is expanded; false otherwise. */ bool IsItemExpanded( const ItemType& InItem ) const { return Private_IsItemExpanded( InItem ); } public: //~ Hide the base function from SListView UE_DEPRECATED(5.3, "SetItemsSource is deprecated. You probably want to use SetTreeItemsSource.") void SetItemsSource(const TArray* InListItemsSource) { SListView::SetItemsSource(InListItemsSource); } UE_DEPRECATED(5.3, "SetItemsSource is deprecated. You probably want to use SetTreeItemsSource.") void SetItemsSource(TSharedRef<::UE::Slate::Containers::TObservableArray> InListItemsSource) { SListView::SetItemsSource(InListItemsSource); } UE_DEPRECATED(5.3, "SetItemsSource is deprecated. You probably want to use SetTreeItemsSource.") void SetItemsSource(TUniquePtr> Provider) { SListView::SetItemsSource(MoveTemp(Provider)); } UE_DEPRECATED(5.3, "ClearItemsSource is deprecated. You probably want to use ClearRootItemsSource.") void ClearItemsSource() { SListView::ClearItemsSource(); } UE_DEPRECATED(5.3, "HasValidItemsSource is deprecated. You probably want to use HasValidRootItemsSource.") bool HasValidItemsSource() const { return SListView::HasValidItemsSource(); } UE_DEPRECATED(5.3, "GetItems is deprecated. You probably want to use GetRootItems.") TArrayView GetItems() const { return SListView::GetItems(); } public: /** * Set the TreeItemsSource. The Tree will generate widgets to represent these items. * @param InItemsSource A pointer to the array of items that should be observed by this TreeView. */ void SetTreeItemsSource( const TArray* InItemsSource) { SetRootItemsSource(InItemsSource); } /** * Set the Root items. The tree will generate widgets to represent these items. * @param InItemsSource A pointer to the array of items that should be observed by this TreeView. */ void SetRootItemsSource(const TArray* InItemsSource) { ensureMsgf(InItemsSource, TEXT("The TreeItemsSource is invalid.")); if (TreeViewSource == nullptr || !TreeViewSource->IsSame(reinterpret_cast(InItemsSource))) { if (InItemsSource) { SetRootItemsSource(MakeUnique>(InItemsSource)); } else { ClearRootItemsSource(); } } } /** * Set the RootItemsSource. The tree will generate widgets to represent these items. * @param InItemsSource A pointer to the array of items that should be observed by this TreeView. */ void SetRootItemsSource(TSharedRef> InItemsSource) { if (TreeViewSource == nullptr || !TreeViewSource->IsSame(reinterpret_cast(&InItemsSource.Get()))) { SetRootItemsSource(MakeUnique>(this->SharedThis(this), MoveTemp(InItemsSource))); } } /** * Establishes a new list of root items being observed by the list. * Wipes all existing state and requests and will fully rebuild on the next tick. */ void SetRootItemsSource(TUniquePtr> Provider) { TreeViewSource = MoveTemp(Provider); RequestTreeRefresh(); } void ClearRootItemsSource() { SetRootItemsSource(TUniquePtr>()); } bool HasValidRootItemsSource() const { return TreeViewSource != nullptr; } TArrayView GetRootItems() const { return TreeViewSource ? TreeViewSource->GetItems() : TArrayView(); } /** * Generates a set of items that are currently expanded. * * @param ExpandedItems The generated set of expanded items. */ void GetExpandedItems( TItemSet& ExpandedItems ) const { for( typename TSparseItemMap::TConstIterator InfoIterator(SparseItemInfos); InfoIterator; ++InfoIterator ) { if ( InfoIterator.Value().bIsExpanded ) { ExpandedItems.Add( InfoIterator.Key() ); } } } /** Clears the entire set of expanded items. */ void ClearExpandedItems() { SparseItemInfos.Empty(); RequestTreeRefresh(); } virtual STableViewBase::FReGenerateResults ReGenerateItems(const FGeometry& MyGeometry) override { // We need to call the parent function first to know if we reached the end of the list STableViewBase::FReGenerateResults Results = SListView::ReGenerateItems(MyGeometry); if (bShouldStackHierarchyHeaders.Get()) { TArray PinnedItems; // If we reached the end of the list and there is space, a special case requires the hierarchy to be collapsed forcefully int32 MaxPinnedItemsOverride = PopulatePinnedItems(LinearizedItems, PinnedItems, Results); this->ReGeneratePinnedItems(PinnedItems, MyGeometry, MaxPinnedItemsOverride); } else { this->ClearPinnedWidgets(); } return Results; } protected: /** The delegate that is invoked whenever we need to gather an item's children. */ FOnGetChildren OnGetChildren; /** The delegate that is invoked to recursively expand/collapse a tree items children. */ FOnSetExpansionRecursive OnSetExpansionRecursive; UE_DEPRECATED(5.3, "Protected access to TreeItemsSource is deprecated. Please use GetTreeItems, SetTreeItemsSource or HasValidTreeItemsSource.") /** A pointer to the items being observed by the tree view. */ const TArray* TreeItemsSource; /** Info needed by a small fraction of tree items; some of these are not visible to the user. */ TSparseItemMap SparseItemInfos; /** Info needed by every item in the linearized version of the tree. */ TArray DenseItemInfos; /** * A linearized version of the items being observed by the tree view. * Note that we inherit from a ListView, which we point at this linearized version of the tree. */ TArray< ItemType > LinearizedItems; /** The delegate that is invoked whenever an item in the tree is expanded or collapsed. */ FOnExpansionChanged OnExpansionChanged; /** Style resource for the tree */ const FTableViewStyle* Style; private: /** Pointer to the source data that we are observing */ TUniquePtr> TreeViewSource; /** true when the LinearizedItems need to be regenerated. */ bool bTreeItemsAreDirty = false; /** true if we allow invisible items to stay selected. */ bool bAllowInvisibleItemSelection = false; /** true if we should highlight all parents for each of the currently selected items */ bool bHighlightParentNodesForSelection = false; /** true if we want to show the hierarchy of items pinned at the top */ TAttribute bShouldStackHierarchyHeaders = false; };