726 lines
23 KiB
C++
726 lines
23 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Fonts/SlateFontInfo.h"
|
|
#include "Fonts/FontMeasure.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "ITreeMap.h"
|
|
#include "XmlFile.h"
|
|
|
|
|
|
typedef TSharedPtr<class FTreeMapNode> FTreeMapNodePtr;
|
|
typedef TSharedRef<class FTreeMapNode> FTreeMapNodeRef;
|
|
|
|
|
|
/**
|
|
* Rectangle used for tree maps
|
|
*/
|
|
struct FTreeMapRect
|
|
{
|
|
/** Position of the rectangle */
|
|
FVector2D Position;
|
|
|
|
/** Dimensions of the rectangle */
|
|
FVector2D Size;
|
|
|
|
|
|
/** Default constructor */
|
|
FTreeMapRect()
|
|
: Position( FVector2D::ZeroVector ),
|
|
Size( FVector2D::ZeroVector )
|
|
{
|
|
}
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Single node in a tree map, which may have any number of child nodes, each with their own children and so on
|
|
*/
|
|
class FTreeMapNode
|
|
{
|
|
|
|
public:
|
|
|
|
/** Pointer to the source data for this node */
|
|
FTreeMapNodeDataPtr Data;
|
|
|
|
/** List of child nodes */
|
|
TArray< FTreeMapNodePtr > Children;
|
|
|
|
/** For leaf nodes, the size of this node. For non-leaf nodes, the size of all of my child nodes. */
|
|
float Size;
|
|
|
|
/** Node rectangle */
|
|
FTreeMapRect Rect;
|
|
|
|
/** Node rectangle, with padding applied */
|
|
FTreeMapRect PaddedRect;
|
|
|
|
/** Font to use for this node's title */
|
|
FSlateFontInfo NameFont;
|
|
|
|
/** Font to use for this node's second line title */
|
|
FSlateFontInfo Name2Font;
|
|
|
|
/** Font to use for this node's centered text */
|
|
FSlateFontInfo CenterTextFont;
|
|
|
|
/** True if the node is 'interactive'. That is, we have enough room for a title area and padding for the node to be clicked on */
|
|
bool bIsInteractive;
|
|
|
|
/** True if the node is visible at all, if false drawing will skip this entirely */
|
|
bool bIsVisible;
|
|
|
|
public:
|
|
|
|
/** Default constructor for FTreeMapNode */
|
|
FTreeMapNode( const FTreeMapNodeDataRef& InitNodeData );
|
|
|
|
/** @return Returns true if this is a leaf node */
|
|
bool IsLeafNode() const
|
|
{
|
|
return Children.Num() == 0;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Tree map object
|
|
*/
|
|
class FTreeMap : public ITreeMap
|
|
{
|
|
|
|
public:
|
|
|
|
/** Default constructor for FTreeMap */
|
|
FTreeMap( const FTreeMapOptions& Options, const FTreeMapNodeDataRef& RootNodeData );
|
|
|
|
/** ITreeMap interface */
|
|
virtual TArray<FTreeMapNodeVisualInfo> GetVisuals() override;
|
|
|
|
|
|
private:
|
|
|
|
enum class ESplitDirection
|
|
{
|
|
Horizontal,
|
|
Vertical
|
|
};
|
|
|
|
/** Recursively sets up nodes in the tree */
|
|
void AddNodesRecursively( FTreeMapNodePtr& Node, const FTreeMapNodeDataRef& NodeData );
|
|
|
|
/** Recursively caches the size of each node. Leaf nodes get the size of their source node, while non-leaf nodes are set to the total size of all of their children */
|
|
void CalculateNodeSizesRecursively( const FTreeMapNodeRef& Node, float& MaxNodeSize );
|
|
|
|
/** Scales the specified node and all sub-nodes by the specified amount */
|
|
void ScaleNodesRecursively( const FTreeMapNodeRef& NodeToScale, const float ScaleFactor );
|
|
|
|
/** Sets up a node using the standard tree mapping algorithm */
|
|
void MakeStandardTreeNode( const FTreeMapOptions& Options, FTreeMap::ESplitDirection SplitDirection, const FTreeMapNodeRef& Node );
|
|
|
|
/** Sets up a node using a squarification method */
|
|
void MakeSquarifiedTreeNode( const FTreeMapOptions& Options, const FTreeMapNodeRef& Node );
|
|
|
|
/** Partitions nodes recursively */
|
|
void PartitionNodesRecursively( const FTreeMapOptions& Options, FTreeMap::ESplitDirection SplitDirection, const FTreeMapNodeRef& Node );
|
|
|
|
/** Pad all of the nodes in to make room for titles and border */
|
|
void PadNodesRecursively( const FTreeMapOptions& Options, const FTreeMapNodeRef& Node, const int32 TreeDepth );
|
|
|
|
|
|
private:
|
|
|
|
/** Root node in the tree map */
|
|
FTreeMapNodePtr RootNode;
|
|
};
|
|
|
|
|
|
|
|
|
|
FTreeMapNode::FTreeMapNode( const FTreeMapNodeDataRef& InitNodeData )
|
|
: Data( InitNodeData ),
|
|
Children(),
|
|
Size( 0.0f ),
|
|
Rect(),
|
|
PaddedRect(),
|
|
NameFont(),
|
|
Name2Font(),
|
|
CenterTextFont(),
|
|
bIsInteractive( true )
|
|
{
|
|
}
|
|
|
|
|
|
|
|
FTreeMap::FTreeMap( const FTreeMapOptions& Options, const FTreeMapNodeDataRef& RootNodeData )
|
|
{
|
|
AddNodesRecursively( RootNode, RootNodeData );
|
|
|
|
// Cache the size of every node
|
|
float MaxNodeSize = 0.0f;
|
|
CalculateNodeSizesRecursively( RootNode.ToSharedRef(), MaxNodeSize );
|
|
|
|
// Also fix up the node sizes as we go. We want the sizes to be proportional to the total display size
|
|
const float DisplaySize = Options.DisplayWidth * Options.DisplayHeight;
|
|
ScaleNodesRecursively( RootNode.ToSharedRef(), DisplaySize / MaxNodeSize ); // @todo treemap perf: Could use a scale factor w/ accessor instead of recursing here
|
|
|
|
// The root node has a fixed position and size
|
|
RootNode->Rect.Position = FVector2D::ZeroVector;
|
|
RootNode->Rect.Size = FVector2D( Options.DisplayWidth, Options.DisplayHeight );
|
|
|
|
// For regular tree types, we'll choose a "next split direction" that matches the aspect of the display area
|
|
const float DisplayAspect = Options.DisplayWidth / Options.DisplayHeight;
|
|
const bool bIsWiderThanTall = DisplayAspect >= 1.0f;
|
|
const auto SplitDirection = bIsWiderThanTall ? ESplitDirection::Horizontal : ESplitDirection::Vertical;
|
|
PartitionNodesRecursively( Options, SplitDirection, RootNode.ToSharedRef() );
|
|
|
|
// Now add space for titles and borders
|
|
const int32 TreeDepth = 0;
|
|
PadNodesRecursively( Options, RootNode.ToSharedRef(), TreeDepth );
|
|
}
|
|
|
|
|
|
void FTreeMap::AddNodesRecursively( FTreeMapNodePtr& OutNode, const FTreeMapNodeDataRef& NodeData )
|
|
{
|
|
// Setup this node
|
|
OutNode = MakeShareable( new FTreeMapNode( NodeData ) );
|
|
|
|
// Add children
|
|
for( const auto& ChildNodeData : NodeData->Children )
|
|
{
|
|
FTreeMapNodePtr ChildNode;
|
|
AddNodesRecursively( ChildNode, ChildNodeData.ToSharedRef() );
|
|
|
|
OutNode->Children.Add( ChildNode );
|
|
}
|
|
}
|
|
|
|
|
|
void FTreeMap::CalculateNodeSizesRecursively( const FTreeMapNodeRef& Node, float& MaxNodeSize )
|
|
{
|
|
// Is this a leaf node? Leaf nodes will actually determine the size of non-leaf nodes.
|
|
if( Node->IsLeafNode() )
|
|
{
|
|
// NOTE: Size should really always be greater than zero here to get good results, but we don't want to assert.
|
|
Node->Size = Node->Data->Size;
|
|
}
|
|
else
|
|
{
|
|
// Update child node sizes
|
|
float TotalSizeOfChildren = 0.0f;
|
|
float MaxSizeOfChildren = 0.0f;
|
|
for( const auto& ChildNode : Node->Children )
|
|
{
|
|
CalculateNodeSizesRecursively( ChildNode.ToSharedRef(), MaxSizeOfChildren );
|
|
TotalSizeOfChildren += ChildNode->Size;
|
|
}
|
|
|
|
// Container node. If a size was explicitly set, then we'll use that size.
|
|
if( Node->Data->Size > 0.0f )
|
|
{
|
|
Node->Size = Node->Data->Size;
|
|
|
|
// Scale the child nodes to fit into the forced container size
|
|
const float ScaleFactor = Node->Size / TotalSizeOfChildren;
|
|
{
|
|
for( const auto& ChildNode : Node->Children )
|
|
{
|
|
ScaleNodesRecursively( ChildNode.ToSharedRef(), ScaleFactor );
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Create a size for the node by summing it's child node sizes
|
|
Node->Size = TotalSizeOfChildren;
|
|
}
|
|
|
|
// Sort our children, largest to smallest
|
|
Node->Children.Sort( []( const FTreeMapNodePtr& A, const FTreeMapNodePtr& B ) { return A->Size > B->Size; } );
|
|
}
|
|
|
|
MaxNodeSize = FMath::Max( MaxNodeSize, Node->Size );
|
|
}
|
|
|
|
|
|
void FTreeMap::ScaleNodesRecursively( const FTreeMapNodeRef& NodeToScale, const float ScaleFactor )
|
|
{
|
|
NodeToScale->Size *= ScaleFactor;
|
|
|
|
for( const auto& ChildNode : NodeToScale->Children )
|
|
{
|
|
ScaleNodesRecursively( ChildNode.ToSharedRef(), ScaleFactor );
|
|
}
|
|
}
|
|
|
|
|
|
void FTreeMap::MakeStandardTreeNode( const FTreeMapOptions& Options, FTreeMap::ESplitDirection SplitDirection, const FTreeMapNodeRef& Node )
|
|
{
|
|
// Standard tree map algorithm. We alternate between horizontal and vertical packing of children. All children are packed
|
|
// into a single row or column. This makes it fairly easy to see the hierarchical structure of the tree, but yields really long rectangles!
|
|
FVector2D Offset( FVector2D::ZeroVector );
|
|
for( const auto& ChildNode : Node->Children )
|
|
{
|
|
ChildNode->Rect.Position = Node->Rect.Position + Offset;
|
|
|
|
const float ChildFractionOfParent = ChildNode->Size / Node->Size;
|
|
if( SplitDirection == ESplitDirection::Horizontal )
|
|
{
|
|
ChildNode->Rect.Size.X = Node->Rect.Size.X * ChildFractionOfParent;
|
|
ChildNode->Rect.Size.Y = Node->Rect.Size.Y;
|
|
Offset.X += ChildNode->Rect.Size.X;
|
|
}
|
|
else
|
|
{
|
|
ChildNode->Rect.Size.X = Node->Rect.Size.X;
|
|
ChildNode->Rect.Size.Y = Node->Rect.Size.Y * ChildFractionOfParent;
|
|
Offset.Y += ChildNode->Rect.Size.Y;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FTreeMap::MakeSquarifiedTreeNode( const FTreeMapOptions& Options, const FTreeMapNodeRef& InNode )
|
|
{
|
|
// NOTE: This algorithm is explained in the paper titled, "Squarified Treemaps", by Mark Bruls, Kees Huizing, and Jarke J.van Wijk
|
|
|
|
// For squarification, we'll always choose the wider aspect direction at every split (ignoring incoming NextSplitDirection!)
|
|
|
|
struct Local
|
|
{
|
|
/** Figure out the highest aspect ratio of all of the blocks, given the length of the rectangle that we want to place these blocks into */
|
|
static float GetWorstAspectInRow( const TArray< FTreeMapNodePtr >& Nodes, const float SubRectShortestSide )
|
|
{
|
|
float MinSize = MAX_FLT;
|
|
float MaxSize = 0.0f;
|
|
float TotalSize = 0.0f;
|
|
for( const auto& Node : Nodes )
|
|
{
|
|
MinSize = FMath::Min( MinSize, Node->Size );
|
|
MaxSize = FMath::Max( MaxSize, Node->Size );
|
|
TotalSize += Node->Size;
|
|
}
|
|
|
|
float TotalSizeSquared = TotalSize * TotalSize;
|
|
float SubRectShortestSideSquared = SubRectShortestSide * SubRectShortestSide;
|
|
|
|
float WorstAspect = FMath::Max( ( SubRectShortestSideSquared * MaxSize ) / TotalSizeSquared, TotalSizeSquared / ( SubRectShortestSideSquared * MinSize ) );
|
|
return WorstAspect;
|
|
}
|
|
|
|
|
|
/** Incoming nodes should be sorted, largest to smallest */
|
|
static TArray<FTreeMapNodePtr> BuildRowFromNodes( TArray<FTreeMapNodePtr>& Nodes, const float SubRectShortestSide )
|
|
{
|
|
TArray<FTreeMapNodePtr> Row;
|
|
|
|
// Add the first child node to our row
|
|
Row.Add( Nodes[ 0 ] );
|
|
Nodes.RemoveAt( 0 );
|
|
|
|
// If there are no more nodes to sort, then we're finished for now
|
|
if( Nodes.Num() > 0 )
|
|
{
|
|
auto NewRow = Row;
|
|
do
|
|
{
|
|
NewRow.Add( Nodes[ 0 ] );
|
|
if( GetWorstAspectInRow( Row, SubRectShortestSide ) > GetWorstAspectInRow( NewRow, SubRectShortestSide ) )
|
|
{
|
|
Row = NewRow;
|
|
|
|
// Claim the node from the original list
|
|
Nodes.RemoveAt( 0 );
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
while( Nodes.Num() > 0 );
|
|
}
|
|
|
|
return Row;
|
|
}
|
|
|
|
|
|
/** Figures out which nodes will fit */
|
|
static void PlaceNodes( TArray<FTreeMapNodePtr>& Row, FTreeMapRect& SubRect )
|
|
{
|
|
float TotalRowSize = 0.0f;
|
|
for( const auto& Node : Row )
|
|
{
|
|
TotalRowSize += Node->Size;
|
|
}
|
|
|
|
const FVector2D SubRectMax = SubRect.Position + SubRect.Size;
|
|
|
|
FTreeMapRect PlacementRect = SubRect;
|
|
if( SubRect.Size.X < SubRect.Size.Y )
|
|
{
|
|
// Taller than wide
|
|
float RowHeight = TotalRowSize / SubRect.Size.X;
|
|
if( PlacementRect.Position.Y + RowHeight >= SubRectMax.Y )
|
|
{
|
|
RowHeight = SubRectMax.Y - PlacementRect.Position.Y;
|
|
}
|
|
|
|
for( int32 ColumnIndex = 0; ColumnIndex < Row.Num(); ++ColumnIndex )
|
|
{
|
|
auto& Node = Row[ ColumnIndex ];
|
|
|
|
float Width = Node->Size / RowHeight;
|
|
if( PlacementRect.Position.X + Width > SubRectMax.X || ( ColumnIndex + 1 ) == Row.Num() )
|
|
{
|
|
Width = SubRectMax.X - PlacementRect.Position.X;
|
|
}
|
|
Node->Rect.Position = PlacementRect.Position;
|
|
Node->Rect.Size.X = Width;
|
|
Node->Rect.Size.Y = RowHeight;
|
|
|
|
PlacementRect.Position.X += Width;
|
|
}
|
|
|
|
const float NewY = SubRect.Position.Y + RowHeight;
|
|
SubRect.Size.Y -= NewY - SubRect.Position.Y;
|
|
SubRect.Position.Y = NewY;
|
|
}
|
|
else
|
|
{
|
|
// Wider than tall
|
|
float RowWidth = TotalRowSize / SubRect.Size.Y;
|
|
if( PlacementRect.Position.X + RowWidth >= SubRectMax.X )
|
|
{
|
|
RowWidth = SubRectMax.X - PlacementRect.Position.X;
|
|
}
|
|
|
|
for( int32 ColumnIndex = 0; ColumnIndex < Row.Num(); ++ColumnIndex )
|
|
{
|
|
auto& Node = Row[ ColumnIndex ];
|
|
|
|
float Height = Node->Size / RowWidth;
|
|
if( PlacementRect.Position.Y + Height > SubRectMax.Y || ( ColumnIndex + 1 ) == Row.Num() )
|
|
{
|
|
Height = SubRectMax.Y - PlacementRect.Position.Y;
|
|
}
|
|
Node->Rect.Position = PlacementRect.Position;
|
|
Node->Rect.Size.X = RowWidth;
|
|
Node->Rect.Size.Y = Height;
|
|
|
|
PlacementRect.Position.Y += Height;
|
|
}
|
|
|
|
const float NewX = SubRect.Position.X + RowWidth;
|
|
SubRect.Size.X -= NewX - SubRect.Position.X;
|
|
SubRect.Position.X = NewX;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Squarify it!
|
|
auto ChildrenCopy = InNode->Children;
|
|
FTreeMapRect SubRect = InNode->Rect;
|
|
do
|
|
{
|
|
const auto SubRectShortestSide = FMath::Min( SubRect.Size.X, SubRect.Size.Y );
|
|
auto Row = Local::BuildRowFromNodes( ChildrenCopy, SubRectShortestSide );
|
|
Local::PlaceNodes( Row, SubRect );
|
|
}
|
|
while( ChildrenCopy.Num() > 0 );
|
|
}
|
|
|
|
|
|
void FTreeMap::PartitionNodesRecursively( const FTreeMapOptions& Options, FTreeMap::ESplitDirection SplitDirection, const FTreeMapNodeRef& Node )
|
|
{
|
|
// Store off our padded copy of the rectangle. We'll actually do the padding later on.
|
|
Node->PaddedRect = Node->Rect;
|
|
|
|
if( !Node->IsLeafNode() )
|
|
{
|
|
if( Options.TreeMapType == ETreeMapType::Standard )
|
|
{
|
|
MakeStandardTreeNode( Options, SplitDirection, Node );
|
|
}
|
|
else if( Options.TreeMapType == ETreeMapType::Squarified )
|
|
{
|
|
MakeSquarifiedTreeNode( Options, Node );
|
|
}
|
|
|
|
// The default algorithm just alternates between horizontal and vertical. The squarification algorithm ignores this.
|
|
auto NextSplitDirection = ( SplitDirection == ESplitDirection::Horizontal ) ? ESplitDirection::Vertical : ESplitDirection::Horizontal;
|
|
|
|
// Process children
|
|
for( const auto& ChildNode : Node->Children )
|
|
{
|
|
PartitionNodesRecursively( Options, NextSplitDirection, ChildNode.ToSharedRef() );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FTreeMap::PadNodesRecursively( const FTreeMapOptions& Options, const FTreeMapNodeRef& Node, const int32 TreeDepth )
|
|
{
|
|
// Inset the container node to leave room for a border, if needed
|
|
// Don't inset the root node
|
|
const auto OriginalNodeRect = Node->Rect;
|
|
|
|
// Choose a height for this node's font
|
|
const uint16 MinAllowedFontSize = 8; // @todo treemap custom: Don't hardcode and instead make this a customizable option?
|
|
Node->NameFont = Options.NameFont;
|
|
Node->NameFont.Size = FMath::Max< int32 >( MinAllowedFontSize, Options.NameFont.Size - ( TreeDepth * Options.FontSizeChangeBasedOnDepth ) );
|
|
Node->Name2Font = Options.Name2Font;
|
|
Node->Name2Font.Size = FMath::Max< int32 >( MinAllowedFontSize, Options.Name2Font.Size - ( TreeDepth * Options.FontSizeChangeBasedOnDepth ) );
|
|
Node->CenterTextFont = Options.CenterTextFont;
|
|
Node->CenterTextFont.Size = FMath::Max< int32 >( MinAllowedFontSize, Options.CenterTextFont.Size - ( TreeDepth * Options.FontSizeChangeBasedOnDepth ) );
|
|
|
|
if( Node != RootNode )
|
|
{
|
|
// Make sure we don't pad beyond our node's size
|
|
const float ContainerOuterPadding = TreeDepth == 1 ? Options.TopLevelContainerOuterPadding : Options.NestedContainerOuterPadding;
|
|
const FVector2D MaxPadding( Node->PaddedRect.Size * 0.5f );
|
|
const FVector2D Padding( FMath::Min( ContainerOuterPadding, MaxPadding.X ), FMath::Min( ContainerOuterPadding, MaxPadding.Y ) );
|
|
|
|
Node->PaddedRect.Position += Padding;
|
|
Node->PaddedRect.Size -= Padding * 2.0f;
|
|
}
|
|
|
|
{
|
|
// The 'child area' is the area within this node that we will fit all child nodes into
|
|
auto ChildAreaRect = Node->PaddedRect;
|
|
|
|
// Unless this is a top level node, make sure the node is big enough to bother reporting to our caller. They may not want to visualize tiny nodes!
|
|
Node->bIsVisible = ( ChildAreaRect.Size.X * ChildAreaRect.Size.Y >= Options.MinimumVisibleNodeSize );
|
|
Node->bIsInteractive = ( TreeDepth <= 1 || ChildAreaRect.Size.X * ChildAreaRect.Size.Y >= Options.MinimumInteractiveNodeSize );
|
|
|
|
if( Node->bIsInteractive && Node->bIsVisible )
|
|
{
|
|
// Figure out how much space we need for the title text
|
|
const TSharedRef< FSlateFontMeasure >& FontMeasureService = FSlateApplication::Get().GetRenderer()->GetFontMeasureService();
|
|
const float MaxCharacterHeight = FontMeasureService->GetMaxCharacterHeight( Node->NameFont ); // @todo treemap perf: Cache this for various heights to reduce calls to FSlateFontMeasure
|
|
const float ContainerTitleAreaHeight = MaxCharacterHeight;
|
|
|
|
// Leave room for a title if we were asked to do that
|
|
{
|
|
const float Padding = FMath::Min( ChildAreaRect.Size.Y, ContainerTitleAreaHeight );
|
|
ChildAreaRect.Position.Y += Padding;
|
|
ChildAreaRect.Size.Y -= Padding;
|
|
}
|
|
|
|
// Apply inner padding before our child nodes, if needed
|
|
{
|
|
// Make sure we don't pad beyond our node's size
|
|
const FVector2D MaxPadding( ChildAreaRect.Size * 0.5f );
|
|
const FVector2D Padding( FMath::Min( Options.ContainerInnerPadding, MaxPadding.X ), FMath::Min( Options.ContainerInnerPadding, MaxPadding.Y ) );
|
|
|
|
ChildAreaRect.Position += Padding;
|
|
ChildAreaRect.Size -= Padding * 2.0f;
|
|
}
|
|
}
|
|
|
|
// Offset and scale all of the child node rects to fit into the child area. This is where some squashing might happen,
|
|
// and the sizes are no longer 1:1 with what they originally represented. But for our purposes this is OK! If you need
|
|
// the sizes to be perfectly accurate, then disable all padding options.
|
|
for( const auto& ChildNode : Node->Children )
|
|
{
|
|
ChildNode->PaddedRect.Position = ChildAreaRect.Position + ( ChildNode->PaddedRect.Position - OriginalNodeRect.Position ) / OriginalNodeRect.Size * ChildAreaRect.Size;
|
|
ChildNode->PaddedRect.Size = ChildNode->PaddedRect.Size / OriginalNodeRect.Size * ChildAreaRect.Size;
|
|
}
|
|
}
|
|
|
|
// Process children
|
|
for( const auto& ChildNode : Node->Children )
|
|
{
|
|
PadNodesRecursively( Options, ChildNode.ToSharedRef(), TreeDepth + 1 );
|
|
}
|
|
}
|
|
|
|
|
|
TSharedRef<ITreeMap> ITreeMap::CreateTreeMap( const FTreeMapOptions& Options, const FTreeMapNodeDataRef& RootNodeData )
|
|
{
|
|
return MakeShareable( new FTreeMap( Options, RootNodeData ) );
|
|
}
|
|
|
|
|
|
|
|
FTreeMapNodeDataPtr ITreeMap::ParseOPMLToTreeMapData( const FString& OPMLFilePath, FString& OutErrorMessage )
|
|
{
|
|
// Use the file name as the root node name
|
|
const FString RootNodeName = FPaths::GetBaseFilename( OPMLFilePath );
|
|
|
|
FXmlFile OPML;
|
|
bool bLoadResult = OPML.LoadFile( OPMLFilePath );
|
|
|
|
FTreeMapNodeDataPtr RootNodeData;
|
|
if( bLoadResult && OPML.IsValid() )
|
|
{
|
|
// Get the working Xml Node
|
|
const FXmlNode* XmlRoot = OPML.GetRootNode();
|
|
if( XmlRoot != nullptr )
|
|
{
|
|
const auto& RootName = XmlRoot->GetTag();
|
|
if( RootName.Equals( TEXT( "opml" ), ESearchCase::IgnoreCase ) )
|
|
{
|
|
for( const auto& OuterXmlNode : XmlRoot->GetChildrenNodes() )
|
|
{
|
|
const auto& OuterNodeName = OuterXmlNode->GetTag();
|
|
if( OuterNodeName.Equals( TEXT( "body" ), ESearchCase::IgnoreCase ) )
|
|
{
|
|
struct Local
|
|
{
|
|
static void RecursivelyCreateNodes( const FTreeMapNodeDataRef& NodeData, const FXmlNode& XmlNode )
|
|
{
|
|
for( const auto& ChildXmlNode : XmlNode.GetChildrenNodes() )
|
|
{
|
|
const auto& NodeName = ChildXmlNode->GetTag();
|
|
if( NodeName.Equals( TEXT( "outline" ), ESearchCase::IgnoreCase ) )
|
|
{
|
|
FTreeMapNodeDataRef ChildNodeData = MakeShareable( new FTreeMapNodeData() );
|
|
ChildNodeData->Parent = &NodeData.Get();
|
|
|
|
// All outline nodes MUST have a text attribute (required as part of OPML spec)
|
|
const auto& OutlineText = ChildXmlNode->GetAttribute( TEXT( "text" ) );
|
|
ChildNodeData->Name = OutlineText;
|
|
NodeData->Children.Add( ChildNodeData );
|
|
|
|
// Recurse into children
|
|
RecursivelyCreateNodes( ChildNodeData, *ChildXmlNode );
|
|
|
|
|
|
// Setup attributes of this node
|
|
{
|
|
const float DefaultLeafNodeSize = 1.0f; // Leaf nodes must always have a non-zero size!
|
|
const float DefaultContainerNodeSize = 0.0f; // 0.0 for container nodes, means "compute my size using my children"
|
|
ChildNodeData->Size = ChildNodeData->IsLeafNode() ? DefaultLeafNodeSize : DefaultContainerNodeSize;
|
|
|
|
// Parse out any hash tags
|
|
int32 HashTagCharIndex;
|
|
while( ChildNodeData->Name.FindChar( '#', HashTagCharIndex ) )
|
|
{
|
|
// Parse hash tag string
|
|
int32 HashTagLength = 1;
|
|
while( ( ChildNodeData->Name.Len() > HashTagCharIndex + HashTagLength ) &&
|
|
!FChar::IsWhitespace( ChildNodeData->Name[ HashTagCharIndex + HashTagLength ] ) &&
|
|
ChildNodeData->Name[ HashTagCharIndex + HashTagLength ] != '#' )
|
|
{
|
|
++HashTagLength;
|
|
}
|
|
|
|
|
|
if( HashTagLength > 1 )
|
|
{
|
|
const FString HashTag = ChildNodeData->Name.Mid( HashTagCharIndex + 1, HashTagLength - 1 );
|
|
|
|
ChildNodeData->HashTags.Add( HashTag );
|
|
|
|
// Strip the hash tag ofg of the original string
|
|
ChildNodeData->Name.MidInline( 0, HashTagCharIndex, EAllowShrinking::No );
|
|
if( HashTagCharIndex + HashTagLength < ChildNodeData->Name.Len() )
|
|
{
|
|
ChildNodeData->Name += ChildNodeData->Name.Mid( HashTagCharIndex + HashTagLength );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clean up any leftover whitespace in the node name, after stripping out hash tags
|
|
ChildNodeData->Name.TrimStartAndEndInline();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Node that we're not interested in
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
RootNodeData = MakeShareable( new FTreeMapNodeData() );
|
|
RootNodeData->Parent = NULL;
|
|
RootNodeData->Name = RootNodeName;
|
|
Local::RecursivelyCreateNodes( RootNodeData.ToSharedRef(), *OuterXmlNode );
|
|
}
|
|
else
|
|
{
|
|
// Top level node that we're not interested in
|
|
}
|
|
}
|
|
|
|
if( !RootNodeData.IsValid() )
|
|
{
|
|
OutErrorMessage = TEXT( "Couldn't find a 'body' node in the XML document" );
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
OutErrorMessage = TEXT( "File does not appear to be an OPML-formatted XML document" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OutErrorMessage = TEXT( "No root node found in XML document" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Couldn't load file
|
|
OutErrorMessage = OPML.GetLastError();
|
|
}
|
|
|
|
return RootNodeData;
|
|
}
|
|
|
|
|
|
TArray<FTreeMapNodeVisualInfo> FTreeMap::GetVisuals()
|
|
{
|
|
TArray<FTreeMapNodeVisualInfo> Visuals;
|
|
|
|
struct Local
|
|
{
|
|
static void RecursivelyGatherVisuals( TArray<FTreeMapNodeVisualInfo>& VisualsList, const FTreeMapNodeRef& Node )
|
|
{
|
|
if (!Node->bIsVisible)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Add a visual for the node that was passed in. We'll recurse down into children afterwards.
|
|
FTreeMapNodeVisualInfo Visual;
|
|
Visual.NodeData = Node->Data.Get();
|
|
Visual.Position = Node->PaddedRect.Position;
|
|
Visual.Size = Node->PaddedRect.Size;
|
|
Visual.Color = Node->Data->Color;
|
|
Visual.NameFont = Node->NameFont;
|
|
Visual.Name2Font = Node->Name2Font;
|
|
Visual.CenterTextFont = Node->CenterTextFont;
|
|
Visual.bIsInteractive = Node->bIsInteractive;
|
|
|
|
// If the node is non-interactive, then ghost it
|
|
if( !Visual.bIsInteractive )
|
|
{
|
|
Visual.Color.A *= 0.25f;
|
|
}
|
|
|
|
VisualsList.Add( Visual );
|
|
|
|
|
|
// Process children
|
|
for( auto ChildNodeIndex = 0; ChildNodeIndex < Node->Children.Num(); ++ChildNodeIndex )
|
|
{
|
|
const auto& ChildNode = Node->Children[ ChildNodeIndex ];
|
|
|
|
// Make up a distinct color for all of the root's top level nodes
|
|
RecursivelyGatherVisuals( VisualsList, ChildNode.ToSharedRef() );
|
|
}
|
|
}
|
|
};
|
|
|
|
Local::RecursivelyGatherVisuals( Visuals, RootNode.ToSharedRef() );
|
|
|
|
return Visuals;
|
|
}
|