Files
UnrealEngineUWP/Engine/Source/Developer/TreeMap/TreeMap.cpp
Stanley Hayes f9da779043 Copying WEX-Staging @ (WEX/Main @ 3740665) to //UE4/Main
#lockdown Nick.Penwarden
#rb none

Copying //UE4/WEX-Staging to //UE4/Dev-Main (Source: //WEX/Main/Engine @ 3740665)
#lockdown Nick.Penwarden

Change 3739326 by Ben.Zeigler

	Change iteration order of depends nodes so it lists hard management references before soft management references, this is better for the UI when lots of references exist
	Update text for loading custom asset registry bin to be clearer

Change 3739000 by John.Opila

	Caching optimization for text widget desired size.

Change 3713551 by David.Nikdel

	Allow Set Properties to recognize Json array values as importable.

Change 3712485 by Josh.May

	Added Pete's fix for the PLATFORM_TVOS/PLATFORM_IOS #define conflict introduced by mach-o/loader.h

Change 3700174 by Chris.Babcock

	Fix setFilters crash on some Android devices

Change 3691531 by Josh.May

	Fixed an intermittent crash that occurred when opening the AssetAuditBrower. AssetManagerEditorModule's CurrentRegistrySource was getting set too early, becoming invalid in the event that RegistrySourceMap is resized.

Change 3688409 by Gil.Gribb

	Critical fix for an extremely rare race condition on async IO.

Change 3687529 by josh.may

	Force layout recalculations for single-pass layout SScaleBoxes when their final scale is zero. This tends to occur in calls to SearchForWidgetRecursively before a SScaleBox's AllottedGeometry has been calculated.

Change 3684788 by Peter.Sauerbrei

	fix for archive generation on the build machines

Change 3684320 by john.opila

	Workaround for widgets disappering. Ensuring scale is never 0 so we don't get divide by zero.

Change 3684042 by Peter.Sauerbrei

	more logging to figure out why there is not data in the Applicaiton diretory of the archive

Change 3678620 by Ben.Zeigler

	Minor text changes to size map

Change 3678314 by Ben.Zeigler

	Add Make Collection With References and Audit References to Size Map to easily allow inspecting the specific set of filtered packages in other tools

Change 3677875 by Ben.Zeigler

	Fix crash in size map from keeping reference to node after map was resized, and undo the Name->DisplayName rename as it could affect licensees

Change 3676899 by Peter.Sauerbrei

	narrowed down to the plist data, trying to figure out if it is missing or not

Change 3676570 by Peter.Sauerbrei

	more logging to track down the archive error

Change 3676293 by Peter.Sauerbrei

	fix for compile failure on IOS

Change 3676172 by Peter.Sauerbrei

	potential fix for missing icons in the ipa when run through the build machines

Change 3673544 by Ben.Zeigler

	Sort AllChunksInfo alphabetically so the order is consistent accross build and platforms to facillitate diffing

Change 3671597 by Peter.Sauerbrei

	Merging
	//UE4/Dev-Mobile/Engine/...
	to //WEX/Main/Engine/...

Change 3670932 by Ben.Zeigler

	Change it so cooking with the AssetManager writes out AllChunksInfo.csv next to the DevelopmentAssetRegistry, but not the per-chunk csv files as those are not useful. Also made the size counts platform accurate

Change 3670906 by Peter.Sauerbrei

	update WEX for building with Xcode 9

Change 3660026 by Josh.May

	Moved SWebBrowserView's parent window "searches" to OnPaint. There's definitely something wrong with FindWidgetWindow... Even after deferring SWebBrowserView's calls to FindWidgetWindow until first Tick, the same widget layout artifacts could occur after opening multiple SWebBrowserViews. And, as Nick pointed out in the related email thread, this approach is also more efficient.

Change 3655411 by Josh.May

	Ensure SWebBrowserView's parent window searches are deferred until after Construct. We haven't puzzled through it yet, but calling FindWidgetWindow during Construct seems to corrupt some Slate state. Deferring this search until later gets around the issue and makes sense anyway, given the widget isn't added to the hierarchy until after Construct.

Change 3655407 by John.Opila

	Sneaking in some stats for SpawnActor.

Change 3654649 by Ben.Zeigler

	Refactor SizeMap and ReferenceViewer into the AssetManagerEditor plugin, and delete the old modules.
	Fix SizeMap crash that I temporarily added. TreeMap is initialized weirdly

Change 3648912 by Ben.Zeigler

	First half of changes to refactor sizemap/reference viewer into the asset manager editor plugin
	Add GetAllContentBrowserCommandExtenders to ContentBrowserModule that allows registering commands/keybinds to extend the content browser via plugins
	Add GetSharedMenu/ToolbarExtensibilityManager to AssetEditorToolkit that allows extending the generic asset editor via plugin
	Move the code to spawn the Reference Viewer and SizeMap into the AssetManagerEditor plugin so these UIs can be tightly bound and share data. This also enables keybinds for Size Map and Audit.
	Remove size map from the save as dialog, it created a special modal size map window that will not work after my refactor

Change 3639419 by Ben.Marsh

	Use DirectoryInfo instead of DirectoryReference to enumerate projects. Tracking down UHT compile failures on Mac.

Change 3638619 by David.Nikdel

	AsyncLoading: Suggested change by Gil to add lock prior to changing LoadPhase to WaitingForHeader (presumably to make FArchiveAsync2::StartReadingHeader's assumption about locking true)

Change 3633562 by Chris.Babcock

	Update Android virtual keyboard support

Change 3630564 by Peter.Sauerbrei

	fix for the manifest stage problem

Change 3629577 by Chris.Babcock

	Fix merge errors in GameActivity.java

Change 3629154 by David.Nikdel

	Disable debug device output in shipping builds (even if logs are enabled)

Change 3626542 by John.Opila

	Back out changelist 3603452
	Undoing the OpenGL load changes as the initial load time was just too damn high!

Change 3620472 by David.Nikdel

	Fix from Nick to fix a BP that crashes on Compile

Change 3618090 by Josh.May

	Reset inertial scrolling for SScrollboxes and STableView-based Slate widgets when scrolling to specific scroll offsets.

Change 3613980 by Chris.Babcock

	Fix issue with Android password keyboard input

Change 3603825 by John.Opila

	Shader change doesn't seem to like standalone PC.

Change 3603452 by John.Opila

	Moving openGL shader compilation into loading instead of at the last minute.

Change 3593008 by David.Nikdel

	Merging CL 3504471
	from //Fortnite/Dev-Cinematics/Engine/...
	to //WEX/Main/Engine/...
	----------------------------------------
	Sequencer: Delay mouse capture until drag for sequencer time slider
	  - Fixes context menus not opening as a result of mouse capture being taken on mouse down

=================================================================================================
THESE CHANGES TOUCH MULTIPLE PLATFORMS
=================================================================================================

Change 3739931 by Ben.Zeigler

	changes to some asset manager code modified on WEX, and fix several FStringAssetReference->FSoftObjectPath

Change 3723451 by Josh.May

	Exposed OnBeforePopup to UMG and Blueprint for UWebBrowser. This is triggered by the CEFBrowserHandler when attempting to open hyperlinks targeting  _blank and, when not handled, would result in the page never loading.
	Added OnBeforePopup handling for the HTMLNewsWidget, ensuring the URLs are opened in an external browser.

Change 3711256 by Dmitriy.Dyomin

	Fixed: Friend list invalidation panel relative transform caching issues
	Also fixed issues with and set slate.cacherenderdata=0 for better batching

Change 3698695 by Josh.May

	Made the UMG default font overridable via config, allowing us to replace it with a game-scope localized Font asset. If there's a better place for this mechanism/accessor to live, please let me know.
	Added a new 'Default' font that replicates '/Engine/EngineFonts/Roboto'. This also has a localized Font asset variant for zh-Hans.

Change 3676085 by Josh.May

	Implemented MulticastBroadcastReceiver, a BroadcastReceiver capable of "multicasting" intents to other receivers. AppsFlyer defines a similar MultiInstallBroadcastReceiver class specific to the INSTALL_REFERRER intent, but it MUST be the very first one defined (cannot be guaranteed in our build pipeline AFAIK).
	Added MulticastBroadcastReceiver (for INSTALL_REFERRER) to the AndroidManifest generation logic, allowing BOTH Adjust and AppsFlyer to receive the intent.
	Added dev channel support for AndroidAppsFlyer, enabled conditionally based on shipping/distribution and whether or not a valid AppsFlyerDevChannel name is specified. For WEX, our dev channel is WEX_Dev.
	Fixed AppsFlyer_EventAttribute's Java class lookups and constructor signature.

Change 3670860 by Ben.Zeigler

	First version of improvements to tools to analyze chunks
	Size Map and Reference Viewer now support reading cooked  asset data and displaying chunks. Changing the platform dropdown in the Asset Audit window switches the other windows as well
	Asset Audit window now has "Add Chunks" button, and selecting AllTypes in the Primary Asset drop down will add all primary assets
	Size Map now shows Disk Size by default, and supports a right click context menu
	Significant UI improvements to all 3 tools, including keybind support
	Split Manage references into Hard and Soft, where Hard are set explicitly and soft are inherited. This allows determining why an asset was included in a chunk/primary asset
	When the AssetManager builds management information for the audit browser/cooker, it now precomputes a chunk mapping for relevant assets. PackageChunkType is used to refer to these virtual primary assets
	Add callback to content browser delegates to handle adding arbitrary FAssetData to an asset view, used to show chunks
	Several changes to the ITreeMap UI used by size map

Change 3670290 by Josh.May

	Added AppleAppID configs for AppsFlyer.
	Added AdSupport and iAd frameworks for IOSAppsFlyer. According to the AppsFlyer documentation, these are required for IDFA and Apple Search Ads tracking.

Change 3643531 by Peter.Sauerbrei

	fix for save game location and certain data backed up to the cloud when it shouldn't

Change 3629303 by Ben.Zeigler

	Merge fix for shared ptr corruption in async loading thread from Main, and enable asnyc loading thread for WEX
	Copy of CL #3623261 and 3625806

Change 3629219 by Peter.Sauerbrei

	Merging using WEX_Main_to_UE4_WEX_Staging
	bringing over the files that Stan didn't have access to

Change 3629063 by Stanley.Hayes

	Engine Merge: Merging using WEX_Main_to_UE4_WEX_Staging(flipped)

Change 3618988 by Josh.May

	Reimplemented DevicePerformanceBucket-based WorldMap class selection to account for the WorldMap actor being pre-serialized into the UMAP.
	On a related note, ChildActorComponents marked as "editor only" now mark their spawned Actors as Transient to prevent them from getting serialized at cook-time.

Change 3597981 by Josh.May

	Converted WExpCampaignDefinition's RegionDefinition refs back to hard references and, to compensate, converted WExpZoneDefinition's ZoneBoss refs to soft references. This moves the RegionDefinitions and ZoneDefinitions from chunk 2 to chunk 1 without pulling in assets for the ZoneBosses. This also allows us to grab the ZoneBoss refs during UWExpAssetManager::GetMainMenuAssetList.
	Reworked UWExpAssetManager::GetMainMenuAssetList and UWExpAssetManager::GetLevelAssetList to build more "complete" asset lists by expanding lists of PrimaryAssetIds.
	Tweaked the WorldMap's ZoneBoss spawning to account for the switch to AssetPtrs.

Change 3581214 by Josh.Markiewicz

	added cookie deletion for Google on logout

[CL 3750870 by Stanley Hayes in Main branch]
2017-11-10 17:20:53 -05:00

726 lines
23 KiB
C++

// Copyright 1998-2017 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 auto HashTag = ChildNodeData->Name.Mid( HashTagCharIndex + 1, HashTagLength - 1 );
ChildNodeData->HashTags.Add( HashTag );
// Strip the hash tag ofg of the original string
ChildNodeData->Name = ChildNodeData->Name.Mid( 0, HashTagCharIndex );
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;
}