2015-04-22 15:58:21 -04:00
// Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
# include "TreeMapModule.h"
# include "STreeMap.h"
# include "ITreeMap.h"
# define LOCTEXT_NAMESPACE "STreeMap"
namespace STreeMapDefs
{
/** Minimum pixels the user must drag the cursor before a drag+drop starts on a visual */
const float MinCursorDistanceForDraggingVisual = 3.0f ;
}
void STreeMap : : Construct ( const FArguments & InArgs , const TSharedRef < FTreeMapNodeData > & InTreeMapNodeData , const TSharedPtr < ITreeMapCustomization > & InCustomization )
{
CurrentUndoLevel = INDEX_NONE ;
Customization = InCustomization ;
TreeMapNodeData = InTreeMapNodeData ;
AllowEditing = InArgs . _AllowEditing ;
BackgroundImage = InArgs . _BackgroundImage ;
NodeBackground = InArgs . _NodeBackground ;
HoveredNodeBackground = InArgs . _HoveredNodeBackground ;
BorderPadding = InArgs . _BorderPadding ;
RelativeDragStartMouseCursorPos = FVector2D : : ZeroVector ;
RelativeMouseCursorPos = FVector2D : : ZeroVector ;
{
FTreeMapOptions TreeMapOptions ;
NameFont = TreeMapOptions . NameFont ;
Name2Font = TreeMapOptions . Name2Font ;
CenterTextFont = TreeMapOptions . CenterTextFont ;
if ( InArgs . _NameFont . IsSet ( ) )
{
NameFont = InArgs . _NameFont ;
}
if ( InArgs . _Name2Font . IsSet ( ) )
{
Name2Font = InArgs . _Name2Font ;
}
if ( InArgs . _CenterTextFont . IsSet ( ) )
{
CenterTextFont = InArgs . _CenterTextFont ;
}
}
MinimumVisualTreeNodeSize = InArgs . _MinimumVisualTreeNodeSize ;
TopLevelContainerOuterPadding = InArgs . _TopLevelContainerOuterPadding ;
NestedContainerOuterPadding = InArgs . _NestedContainerOuterPadding ;
ContainerInnerPadding = InArgs . _ContainerInnerPadding ;
ChildContainerTextPadding = InArgs . _ChildContainerTextPadding ;
TreeMapSize = FVector2D ( 0 , 0 ) ;
NavigateAnimationCurve . AddCurve ( 0.0f , InArgs . _NavigationTransitionTime , ECurveEaseFunction : : CubicOut ) ;
MouseOverVisual = NULL ;
DraggingVisual = NULL ;
DragVisualDistance = 0.0f ;
bIsNamingNewNode = false ;
HighlightPulseStartTime = - 99999.0 ;
OnTreeMapNodeDoubleClicked = InArgs . _OnTreeMapNodeDoubleClicked ;
if ( Customization . IsValid ( ) )
{
SizeNodesByAttribute = Customization - > GetDefaultSizeByAttribute ( ) ;
ColorNodesByAttribute = Customization - > GetDefaultColorByAttribute ( ) ;
}
const bool bShouldPlayTransition = false ;
SetTreeRoot ( TreeMapNodeData . ToSharedRef ( ) , bShouldPlayTransition ) ;
}
void STreeMap : : Tick ( const FGeometry & AllottedGeometry , const double InCurrentTime , const float InDeltaTime )
{
SLeafWidget : : Tick ( AllottedGeometry , InCurrentTime , InDeltaTime ) ;
if ( AllottedGeometry . Size ! = TreeMapSize | | ! TreeMap . IsValid ( ) )
{
// Stop renaming a node if we were doing that
StopRenamingNode ( ) ;
// Make a tree map!
FTreeMapOptions TreeMapOptions ;
TreeMapOptions . TreeMapType = ETreeMapType : : Squarified ;
TreeMapOptions . DisplayWidth = AllottedGeometry . Size . X ;
TreeMapOptions . DisplayHeight = AllottedGeometry . Size . Y ;
TreeMapOptions . TopLevelContainerOuterPadding = TopLevelContainerOuterPadding ;
TreeMapOptions . NestedContainerOuterPadding = NestedContainerOuterPadding ;
TreeMapOptions . ContainerInnerPadding = ContainerInnerPadding ;
TreeMapOptions . NameFont = NameFont . Get ( ) ;
TreeMapOptions . Name2Font = Name2Font . Get ( ) ;
TreeMapOptions . CenterTextFont = CenterTextFont . Get ( ) ;
TreeMapOptions . FontSizeChangeBasedOnDepth = 2 ; // @todo treemap custom: Expose as a customization option to STreeMap
TreeMapOptions . MinimumInteractiveNodeSize = MinimumVisualTreeNodeSize ;
TreeMap = ITreeMap : : CreateTreeMap ( TreeMapOptions , ActiveRootTreeMapNode . ToSharedRef ( ) ) ;
TreeMapSize = AllottedGeometry . Size ;
CachedNodeVisuals = TreeMap - > GetVisuals ( ) ;
MouseOverVisual = NULL ;
DraggingVisual = NULL ;
// Map the new visuals back to the old ones!
NodeVisualIndicesToLastIndices . Reset ( ) ;
TSet < int32 > ValidLastIndices ;
for ( auto VisualIndex = 0 ; VisualIndex < CachedNodeVisuals . Num ( ) ; + + VisualIndex )
{
const auto & Visual = CachedNodeVisuals [ VisualIndex ] ;
for ( auto LastVisualIndex = 0 ; LastVisualIndex < LastCachedNodeVisuals . Num ( ) ; + + LastVisualIndex )
{
const auto & LastVisual = LastCachedNodeVisuals [ LastVisualIndex ] ;
if ( LastVisual . NodeData = = Visual . NodeData )
{
NodeVisualIndicesToLastIndices . Add ( VisualIndex , LastVisualIndex ) ;
ValidLastIndices . Add ( LastVisualIndex ) ;
break ;
}
}
}
// Find all of the orphans
OrphanedLastIndices . Reset ( ) ;
for ( auto LastVisualIndex = 0 ; LastVisualIndex < LastCachedNodeVisuals . Num ( ) ; + + LastVisualIndex )
{
if ( ! ValidLastIndices . Contains ( LastVisualIndex ) )
{
OrphanedLastIndices . Add ( LastVisualIndex ) ;
}
}
}
}
void STreeMap : : MakeBlendedNodeVisual ( const int32 VisualIndex , const float NavigationAlpha , FTreeMapNodeVisualInfo & OutVisual ) const
{
OutVisual = CachedNodeVisuals [ VisualIndex ] ; // NOTE: Copying visual
// Do we need to interp?
if ( NavigationAlpha < 1.0f )
{
// Did the visual exist before we navigated?
const int32 * LastVisualIndexPtr = NodeVisualIndicesToLastIndices . Find ( VisualIndex ) ;
if ( LastVisualIndexPtr ! = NULL )
{
// It did exist!
const auto LastVisualIndex = * LastVisualIndexPtr ;
const auto & LastVisual = LastCachedNodeVisuals [ LastVisualIndex ] ;
// Blend before "before" and "now"
OutVisual . Position = FMath : : Lerp ( LastVisual . Position , OutVisual . Position , NavigationAlpha ) ;
OutVisual . Size = FMath : : Lerp ( LastVisual . Size , OutVisual . Size , NavigationAlpha ) ;
// Do an HSV color lerp; it just looks more sensible!
OutVisual . Color = FLinearColor : : LerpUsingHSV ( LastVisual . Color , OutVisual . Color , NavigationAlpha ) ;
// The blended visual is considered interactive only if both the new version and the old version were interactive
OutVisual . bIsInteractive = LastVisual . bIsInteractive & & OutVisual . bIsInteractive ;
}
else
{
// Didn't exist before. Fade in from nothing!
OutVisual . Color . A * = NavigationAlpha ;
}
}
}
int32 STreeMap : : OnPaint ( const FPaintArgs & Args , const FGeometry & AllottedGeometry , const FSlateRect & WidgetClippingRect , FSlateWindowElementList & OutDrawElements , int32 LayerId , const FWidgetStyle & InWidgetStyle , bool bParentEnabled ) const
{
const bool bEnabled = ShouldBeEnabled ( bParentEnabled ) ;
const auto DrawEffects = bEnabled ? ESlateDrawEffect : : None : ESlateDrawEffect : : DisabledEffect ;
// Draw background border layer
{
const FSlateBrush * ThisBackgroundImage = BackgroundImage . Get ( ) ;
FSlateDrawElement : : MakeBox (
OutDrawElements ,
LayerId ,
AllottedGeometry . ToPaintGeometry ( ) ,
ThisBackgroundImage ,
WidgetClippingRect ,
DrawEffects ,
InWidgetStyle . GetColorAndOpacityTint ( ) * ThisBackgroundImage - > TintColor . GetColor ( InWidgetStyle )
) ;
}
float NavigationAlpha = NavigateAnimationCurve . GetLerp ( ) ;
// Draw tree map
if ( TreeMap . IsValid ( ) )
{
// Figure out all the nodes we need to draw in a big ordered list. This can sometimes include nodes from the previous tree map we were
// looking at before we "navigated", as those nodes will be briefly visible during the animated transition
static TArray < const FTreeMapNodeVisualInfo * > NodeVisualsToDraw ;
NodeVisualsToDraw . Reset ( ) ;
// Draw the orphan's first
// @todo treemap visuals: During transitions, nodes look "on top" should be drawn last. Right now they will underlap and overlap adjacents at the same time. Looks weird.
for ( auto OrphanedVisualIndex : OrphanedLastIndices )
{
NodeVisualsToDraw . Add ( & LastCachedNodeVisuals [ OrphanedVisualIndex ] ) ;
}
const int32 FirstNewVisualIndex = NodeVisualsToDraw . Num ( ) ;
for ( const auto & Visual : CachedNodeVisuals )
{
NodeVisualsToDraw . Add ( & Visual ) ;
}
const FTreeMapNodeData * RenamingNodeDataPtr = RenamingNodeData . Pin ( ) . Get ( ) ;
// Draw background boxes for all visuals
{
+ + LayerId ;
const FSlateBrush * ThisHoveredNodeBackground = HoveredNodeBackground . Get ( ) ;
const FSlateBrush * ThisNodeBackground = NodeBackground . Get ( ) ;
for ( auto DrawVisualIndex = 0 ; DrawVisualIndex < NodeVisualsToDraw . Num ( ) ; + + DrawVisualIndex )
{
FTreeMapNodeVisualInfo BlendedVisual ;
if ( DrawVisualIndex > = FirstNewVisualIndex )
{
MakeBlendedNodeVisual ( DrawVisualIndex - FirstNewVisualIndex , NavigationAlpha , BlendedVisual ) ;
}
else
{
// This is an orphan
BlendedVisual = * NodeVisualsToDraw [ DrawVisualIndex ] ;
BlendedVisual . Color . A * = 1.0f - NavigationAlpha ; // Fade orphans out when navigating
}
// Don't draw if completely faded out
if ( BlendedVisual . Color . A > KINDA_SMALL_NUMBER )
{
const bool bIsMouseOverNode = MouseOverVisual ! = NULL & & MouseOverVisual - > NodeData = = BlendedVisual . NodeData ;
// Draw the visual's background box
const FVector2D VisualPosition = BlendedVisual . Position ;
const FSlateRect VisualClippingRect = TransformRect ( AllottedGeometry . GetAccumulatedLayoutTransform ( ) , FSlateRect ( VisualPosition , VisualPosition + BlendedVisual . Size ) ) ;
const auto VisualPaintGeometry = AllottedGeometry . ToPaintGeometry ( VisualPosition , BlendedVisual . Size ) ;
auto DrawColor = InWidgetStyle . GetColorAndOpacityTint ( ) * ThisNodeBackground - > TintColor . GetColor ( InWidgetStyle ) * BlendedVisual . Color ;
FSlateDrawElement : : MakeBox (
OutDrawElements ,
LayerId ,
VisualPaintGeometry ,
bIsMouseOverNode ? ThisHoveredNodeBackground : ThisNodeBackground ,
VisualClippingRect ,
DrawEffects ,
DrawColor
) ;
if ( BlendedVisual . NodeData - > BackgroundBrush ! = nullptr )
{
// Preserve aspect ratio, but stretch to fill the whole rectangle, even if we have to crop the smaller edge
const float LargestSize = FMath : : Max ( BlendedVisual . Size . X , BlendedVisual . Size . Y ) ;
const FVector2D BackgroundSize ( LargestSize , LargestSize ) ;
FVector2D BackgroundPosition = VisualPosition ;
if ( BlendedVisual . Size . X > BlendedVisual . Size . Y ) // Is our box wider than tall?
{
const float SizeDifference = LargestSize - BlendedVisual . Size . Y ;
BackgroundPosition . Y - = SizeDifference * 0.5f ;
}
else
{
const float SizeDifference = LargestSize - BlendedVisual . Size . X ;
BackgroundPosition . X - = SizeDifference * 0.5f ;
}
const auto BackgroundPaintGeometry = AllottedGeometry . ToPaintGeometry ( BackgroundPosition , BackgroundSize ) ;
const FSlateRect BackgroundClippingRect = VisualClippingRect . InsetBy ( FMargin ( 1 ) ) ;
// Draw the background brush
FSlateDrawElement : : MakeBox (
OutDrawElements ,
LayerId ,
BackgroundPaintGeometry ,
BlendedVisual . NodeData - > BackgroundBrush ,
BackgroundClippingRect ,
DrawEffects ,
DrawColor ) ;
}
const bool bIsHighlightPulseNode = HighlightPulseNode . IsValid ( ) & & HighlightPulseNode . Pin ( ) . Get ( ) = = BlendedVisual . NodeData ;
if ( bIsHighlightPulseNode )
{
const float HighlightPulseAnimDuration = 1.5f ; // @todo treemap: Probably should be customizable in the widget's settings
const float HighlightPulseAnimProgress = ( FSlateApplication : : Get ( ) . GetCurrentTime ( ) - HighlightPulseStartTime ) / HighlightPulseAnimDuration ;
if ( HighlightPulseAnimProgress > = 0.0f & & HighlightPulseAnimProgress < = 1.0f )
{
DrawColor = FLinearColor ( 1.0f , 1.0f , 1.0f , FMath : : MakePulsatingValue ( HighlightPulseAnimProgress , 6.0f , 0.5f ) ) ;
FSlateDrawElement : : MakeBox (
OutDrawElements ,
LayerId ,
VisualPaintGeometry ,
ThisHoveredNodeBackground ,
VisualClippingRect ,
DrawEffects ,
DrawColor ) ;
}
}
}
}
}
// Draw text layers. We draw it twice for all visuals. Once for a drop shadow, and then again for the foreground text.
const TSharedRef < FSlateFontMeasure > & FontMeasureService = FSlateApplication : : Get ( ) . GetRenderer ( ) - > GetFontMeasureService ( ) ;
for ( auto TextLayerIndex = 0 ; TextLayerIndex < 2 ; + + TextLayerIndex )
{
+ + LayerId ;
// @todo treemap: Want ellipses for truncated text
// @todo treemap: Squash text based on box size rather than strictly depth?
const float ShadowOpacity = 0.5f ;
const auto ShadowOffset = ( TextLayerIndex = = 0 ) ? FVector2D ( - 1.0f , 1.0f ) : FVector2D : : ZeroVector ;
const auto TextColorScale = ( TextLayerIndex = = 0 ) ? FLinearColor ( 0.0f , 0.0f , 0.0f , ShadowOpacity ) : FLinearColor : : White ;
for ( auto DrawVisualIndex = 0 ; DrawVisualIndex < NodeVisualsToDraw . Num ( ) ; + + DrawVisualIndex )
{
FTreeMapNodeVisualInfo BlendedVisual ;
if ( DrawVisualIndex > = FirstNewVisualIndex )
{
MakeBlendedNodeVisual ( DrawVisualIndex - FirstNewVisualIndex , NavigationAlpha , BlendedVisual ) ;
}
else
{
// This is an orphan
BlendedVisual = * NodeVisualsToDraw [ DrawVisualIndex ] ;
BlendedVisual . Color . A * = 1.0f - NavigationAlpha ; // Fade orphans out when navigating
}
// Don't draw text if completely faded out, or if not interactive
if ( BlendedVisual . Color . A > KINDA_SMALL_NUMBER & & BlendedVisual . bIsInteractive )
{
// Don't draw a title for a node that is actively being renamed -- the text box is already visible, after all
if ( BlendedVisual . NodeData ! = RenamingNodeDataPtr )
{
const FVector2D VisualPosition = BlendedVisual . Position ;
// Allow text to fade out with its parent box
auto VisualTextColor = TextColorScale ;
VisualTextColor . A * = BlendedVisual . Color . A ;
const bool bIsMouseOverNode = MouseOverVisual ! = NULL & & MouseOverVisual - > NodeData = = BlendedVisual . NodeData ;
// If the visual is crushed down pretty small in screen space, we don't want to bother even try drawing text
const FVector2D ScreenSpaceVisualSize ( AllottedGeometry . Scale * BlendedVisual . Size ) ;
if ( ScreenSpaceVisualSize . X > 20 )
{
const auto VisualPaintGeometry = AllottedGeometry . ToPaintGeometry ( VisualPosition , BlendedVisual . Size ) ;
// Clip the text to the visual's rectangle, with some extra inner padding to avoid overlapping the visual's border
auto TextClippingRect = FSlateRect ( VisualPaintGeometry . DrawPosition , VisualPaintGeometry . DrawPosition + VisualPaintGeometry . GetLocalSize ( ) ) ;
TextClippingRect = TextClippingRect . IntersectionWith ( WidgetClippingRect ) ;
TextClippingRect = TextClippingRect . InsetBy ( FMargin ( ChildContainerTextPadding , 0 , ChildContainerTextPadding , 0 ) ) ;
if ( TextClippingRect . IsValid ( ) )
{
// Name (first line)
float NameTextHeight = 0.0f ;
if ( ! BlendedVisual . NodeData - > Name . IsEmpty ( ) )
{
const FVector2D TextSize = FontMeasureService - > Measure ( BlendedVisual . NodeData - > Name , BlendedVisual . NameFont ) ;
NameTextHeight = TextSize . Y ;
const auto TextX = VisualPosition . X + FMath : : Max ( ChildContainerTextPadding , ( float ) BlendedVisual . Size . X * 0.5f - TextSize . X * 0.5f ) ; // Clamp to left edge if cropped, so the user can at least read the beginning
const auto TextY = VisualPosition . Y + ChildContainerTextPadding ;
FSlateDrawElement : : MakeText (
OutDrawElements ,
LayerId ,
AllottedGeometry . ToOffsetPaintGeometry ( ShadowOffset + FVector2D ( TextX , TextY ) ) ,
BlendedVisual . NodeData - > Name ,
BlendedVisual . NameFont ,
TextClippingRect ,
DrawEffects ,
InWidgetStyle . GetColorAndOpacityTint ( ) * VisualTextColor ) ;
}
// Name (second line)
float Name2TextHeight = 0.0f ;
if ( ! BlendedVisual . NodeData - > Name2 . IsEmpty ( ) & & BlendedVisual . NodeData - > IsLeafNode ( ) )
{
const FVector2D TextSize = FontMeasureService - > Measure ( BlendedVisual . NodeData - > Name2 , BlendedVisual . Name2Font ) ;
Name2TextHeight = TextSize . Y ;
const auto TextX = VisualPosition . X + FMath : : Max ( ChildContainerTextPadding , ( float ) BlendedVisual . Size . X * 0.5f - TextSize . X * 0.5f ) ; // Clamp to left edge if cropped, so the user can at least read the beginning
const auto TextY = VisualPosition . Y + ChildContainerTextPadding + NameTextHeight ;
// Clip the text to the visual's rectangle, with some extra inner padding to avoid overlapping the visual's border
FSlateDrawElement : : MakeText (
OutDrawElements ,
LayerId ,
AllottedGeometry . ToOffsetPaintGeometry ( ShadowOffset + FVector2D ( TextX , TextY ) ) ,
BlendedVisual . NodeData - > Name2 ,
BlendedVisual . Name2Font ,
TextClippingRect ,
DrawEffects ,
InWidgetStyle . GetColorAndOpacityTint ( ) * VisualTextColor ) ;
}
// Center text
if ( ! BlendedVisual . NodeData - > CenterText . IsEmpty ( ) & & BlendedVisual . NodeData - > IsLeafNode ( ) )
{
// If the visual is smaller than the text we're going to draw, try using a smaller font
2015-04-22 18:33:17 -04:00
const FSlateFontInfo * BlendedVisualCenterTextFont = & BlendedVisual . CenterTextFont ;
const FVector2D FullTextSize = FontMeasureService - > Measure ( BlendedVisual . NodeData - > CenterText , * BlendedVisualCenterTextFont ) ;
2015-04-22 15:58:21 -04:00
if ( ScreenSpaceVisualSize . X < FullTextSize . X | | ScreenSpaceVisualSize . Y < FullTextSize . Y ) // @todo treemap: assumption that NameFont will be smaller than CenterText font
{
2015-04-22 18:33:17 -04:00
BlendedVisualCenterTextFont = & BlendedVisual . NameFont ;
2015-04-22 15:58:21 -04:00
}
2015-04-22 18:33:17 -04:00
const FVector2D TextSize = FontMeasureService - > Measure ( BlendedVisual . NodeData - > CenterText , * BlendedVisualCenterTextFont ) ;
2015-04-22 15:58:21 -04:00
const auto TextX = VisualPosition . X + FMath : : Max ( ChildContainerTextPadding , ( float ) BlendedVisual . Size . X * 0.5f - TextSize . X * 0.5f ) ; // Clamp to left edge if cropped, so the user can at least read the beginning
const auto TextY = VisualPosition . Y + FMath : : Max ( ChildContainerTextPadding + NameTextHeight + Name2TextHeight , ( float ) BlendedVisual . Size . Y * 0.5f - TextSize . Y * 0.5f ) ; // Clamp to bottom of first line of text if cropped
// Clip the text to the visual's rectangle, with some extra inner padding to avoid overlapping the visual's border
FSlateDrawElement : : MakeText (
OutDrawElements ,
LayerId ,
AllottedGeometry . ToOffsetPaintGeometry ( ShadowOffset + FVector2D ( TextX , TextY ) ) ,
BlendedVisual . NodeData - > CenterText ,
2015-04-22 18:33:17 -04:00
* BlendedVisualCenterTextFont ,
2015-04-22 15:58:21 -04:00
TextClippingRect ,
DrawEffects ,
InWidgetStyle . GetColorAndOpacityTint ( ) * VisualTextColor ) ;
}
}
}
}
}
}
}
}
if ( DraggingVisual ! = NULL & & DragVisualDistance > = STreeMapDefs : : MinCursorDistanceForDraggingVisual )
{
const FVector2D DragStartCursorPos = RelativeDragStartMouseCursorPos ;
const FVector2D NewCursorPos = RelativeMouseCursorPos ;
const FVector2D SplineStart = DragStartCursorPos ;
const FVector2D SplineEnd = NewCursorPos ;
const FVector2D SplineStartDir = ( DragStartCursorPos - ( DraggingVisual - > Position + DraggingVisual - > Size * 0.5f ) ) ; // @todo treemap: Point away from the center of the dragged visual
const FVector2D SplineEndDir = FVector2D ( 0.0f , 200.0f ) ; // @todo treemap: Probably needs better customization support
// @todo treemap: Draw line in red if drop won't work?
// Draw two passes, the first one is an drop shadow
for ( auto SplineLayerIndex = 0 ; SplineLayerIndex < 2 ; + + SplineLayerIndex )
{
+ + LayerId ;
const float ShadowOpacity = 0.5f ;
const float SplineThickness = ( SplineLayerIndex = = 0 ) ? 5.0f : 4.0f ;
const auto ShadowOffset = ( SplineLayerIndex = = 0 ) ? FVector2D ( - 1.0f , 1.0f ) : FVector2D : : ZeroVector ;
const auto SplineColorScale = ( SplineLayerIndex = = 0 ) ? FLinearColor ( 0.0f , 0.0f , 0.0f , ShadowOpacity ) : FLinearColor : : White ;
FSlateDrawElement : : MakeSpline (
OutDrawElements ,
LayerId ,
AllottedGeometry . ToPaintGeometry ( ShadowOffset , FVector2D ( 1.0 , 1.0f ) ) ,
SplineStart ,
SplineStartDir ,
SplineEnd ,
SplineEndDir ,
WidgetClippingRect ,
SplineThickness ,
DrawEffects ,
InWidgetStyle . GetColorAndOpacityTint ( ) * DraggingVisual - > Color * SplineColorScale ) ;
}
}
return LayerId ;
}
FVector2D STreeMap : : ComputeDesiredSize ( float LayoutScaleMultiplier ) const
{
// TreeMap widgets have no desired size -- their size is always determined by their container
return FVector2D : : ZeroVector ;
}
FReply STreeMap : : OnMouseMove ( const FGeometry & InMyGeometry , const FPointerEvent & InMouseEvent )
{
FReply Reply = FReply : : Unhandled ( ) ;
// Update window-relative cursor position
RelativeMouseCursorPos = InMyGeometry . AbsoluteToLocal ( InMouseEvent . GetScreenSpacePosition ( ) ) ;
// Clear current hover
MouseOverVisual = NULL ;
// Don't hover while transitioning. It looks bad!
if ( ! IsNavigationTransitionActive ( ) )
{
// Figure out which visual the cursor is over
FTreeMapNodeVisualInfo * NodeVisualUnderCursor = FindNodeVisualUnderCursor ( InMyGeometry , InMouseEvent . GetScreenSpacePosition ( ) ) ;
if ( NodeVisualUnderCursor ! = NULL )
{
// Mouse is over a visual
MouseOverVisual = NodeVisualUnderCursor ;
}
Reply = FReply : : Handled ( ) ;
}
if ( DraggingVisual ! = NULL )
{
DragVisualDistance + = InMouseEvent . GetCursorDelta ( ) . Size ( ) ;
}
return Reply ;
}
void STreeMap : : OnMouseLeave ( const FPointerEvent & InMouseEvent )
{
MouseOverVisual = NULL ;
}
FReply STreeMap : : OnMouseButtonDown ( const FGeometry & InMyGeometry , const FPointerEvent & InMouseEvent )
{
FReply Reply = FReply : : Unhandled ( ) ;
if ( InMouseEvent . GetEffectingButton ( ) = = EKeys : : LeftMouseButton )
{
// Wait until we've finished animating a transition
if ( ! IsNavigationTransitionActive ( ) )
{
if ( AllowEditing . Get ( ) )
{
// Figure out what was clicked on
FTreeMapNodeVisualInfo * NodeVisualUnderCursor = FindNodeVisualUnderCursor ( InMyGeometry , InMouseEvent . GetScreenSpacePosition ( ) ) ;
if ( NodeVisualUnderCursor ! = NULL )
{
// Mouse was pressed on a node
DraggingVisual = NodeVisualUnderCursor ;
DragVisualDistance = 0.0f ;
RelativeDragStartMouseCursorPos = InMyGeometry . AbsoluteToLocal ( InMouseEvent . GetScreenSpacePosition ( ) ) ;
Reply = FReply : : Handled ( ) ;
}
}
}
}
return Reply ;
}
FReply STreeMap : : OnMouseButtonUp ( const FGeometry & InMyGeometry , const FPointerEvent & InMouseEvent )
{
FReply Reply = FReply : : Unhandled ( ) ;
if ( InMouseEvent . GetEffectingButton ( ) = = EKeys : : LeftMouseButton )
{
auto * DroppedVisual = DraggingVisual ;
DraggingVisual = NULL ;
if ( DroppedVisual ! = NULL & & DragVisualDistance > = STreeMapDefs : : MinCursorDistanceForDraggingVisual )
{
// Wait until we've finished animating a transition
if ( ! IsNavigationTransitionActive ( ) )
{
// Find the node under the cursor
FTreeMapNodeVisualInfo * NodeVisualUnderCursor = FindNodeVisualUnderCursor ( InMyGeometry , InMouseEvent . GetScreenSpacePosition ( ) ) ;
if ( NodeVisualUnderCursor ! = NULL )
{
// The dropped node will become a child of the node we dropped onto
auto NewParentNode = NodeVisualUnderCursor - > NodeData - > AsShared ( ) ;
auto DroppedNode = DroppedVisual - > NodeData - > AsShared ( ) ;
// Reparent it!
ReparentNode ( DroppedNode , NewParentNode ) ;
Reply = FReply : : Handled ( ) ;
}
}
}
else
{
FTreeMapNodeVisualInfo * NodeVisualUnderCursor = FindNodeVisualUnderCursor ( InMyGeometry , InMouseEvent . GetScreenSpacePosition ( ) ) ;
if ( NodeVisualUnderCursor ! = NULL )
{
if ( AllowEditing . Get ( ) )
{
// Start renaming!
const bool bIsNewNode = false ;
StartRenamingNode ( InMyGeometry , NodeVisualUnderCursor - > NodeData - > AsShared ( ) , NodeVisualUnderCursor - > Position , bIsNewNode ) ;
Reply = FReply : : Handled ( ) ;
}
}
}
DragVisualDistance = 0.0f ;
}
else if ( InMouseEvent . GetEffectingButton ( ) = = EKeys : : RightMouseButton )
{
// Show a pop-up menu!
ShowOptionsMenuAt ( InMouseEvent . GetScreenSpacePosition ( ) ) ;
}
return Reply ;
}
FReply STreeMap : : OnMouseButtonDoubleClick ( const FGeometry & InMyGeometry , const FPointerEvent & InMouseEvent )
{
FReply Reply = FReply : : Unhandled ( ) ;
if ( InMouseEvent . GetEffectingButton ( ) = = EKeys : : LeftMouseButton )
{
// Wait until we're done transitioning before allowing another transition
if ( ! IsNavigationTransitionActive ( ) )
{
// Figure out what was clicked on
const FTreeMapNodeVisualInfo * NodeVisualUnderCursor = FindNodeVisualUnderCursor ( InMyGeometry , InMouseEvent . GetScreenSpacePosition ( ) ) ;
if ( NodeVisualUnderCursor ! = NULL )
{
// Double-clicked on a tree map visual! Check to see if we were asked to customize how double-click is handled.
if ( OnTreeMapNodeDoubleClicked . IsBound ( ) )
{
OnTreeMapNodeDoubleClicked . Execute ( * NodeVisualUnderCursor - > NodeData ) ;
}
else
{
// Double-click was not overridden, so just do our default thing and re-root the tree directly on the node that was double-clicked on
// Re-root the tree
const bool bShouldPlayTransition = true ;
SetTreeRoot ( NodeVisualUnderCursor - > NodeData - > AsShared ( ) , bShouldPlayTransition ) ;
}
Reply = FReply : : Handled ( ) ;
}
}
}
return Reply ;
}
FReply STreeMap : : OnMouseWheel ( const FGeometry & MyGeometry , const FPointerEvent & MouseEvent )
{
FReply Reply = FReply : : Unhandled ( ) ;
// Don't zoom in or out while already transitioning. It feels too frenetic.
if ( ! IsNavigationTransitionActive ( ) )
{
if ( MouseEvent . GetWheelDelta ( ) > 0 )
{
// Figure out what was clicked on
const FTreeMapNodeVisualInfo * NodeVisualUnderCursor = FindNodeVisualUnderCursor ( MyGeometry , MouseEvent . GetScreenSpacePosition ( ) ) ;
if ( NodeVisualUnderCursor ! = NULL )
{
// From the node that was scrolled over, visits nodes upward until we find one whose parent is our current active root
FTreeMapNodeDataPtr NextNode = NodeVisualUnderCursor - > NodeData - > AsShared ( ) ;
do
{
FTreeMapNodeDataPtr NodeParent ;
if ( NextNode - > Parent ! = NULL )
{
NodeParent = NextNode - > Parent - > AsShared ( ) ;
}
if ( NodeParent = = ActiveRootTreeMapNode )
{
break ;
}
NextNode = NodeParent ;
}
while ( NextNode . IsValid ( ) ) ;
// Zoom in one level
if ( NextNode . IsValid ( ) )
{
const bool bShouldPlayTransition = true ;
SetTreeRoot ( NextNode . ToSharedRef ( ) , bShouldPlayTransition ) ;
if ( ! Reply . IsEventHandled ( ) )
{
Reply = FReply : : Handled ( ) ;
}
}
}
}
else if ( MouseEvent . GetWheelDelta ( ) < 0 )
{
// Zoom out one level
if ( ActiveRootTreeMapNode - > Parent ! = NULL )
{
const bool bShouldPlayTransition = true ;
SetTreeRoot ( ActiveRootTreeMapNode - > Parent - > AsShared ( ) , bShouldPlayTransition ) ;
if ( ! Reply . IsEventHandled ( ) )
{
Reply = FReply : : Handled ( ) ;
}
}
}
}
return Reply ;
}
void STreeMap : : SetTreeRoot ( const FTreeMapNodeDataRef & NewRoot , const bool bShouldPlayTransition )
{
if ( ActiveRootTreeMapNode ! = NewRoot )
{
ActiveRootTreeMapNode = NewRoot ;
// Freshen the visualization data for the node since it may be stale after being copied off the undo stack
RebuildTreeMap ( bShouldPlayTransition ) ;
}
}
void STreeMap : : RebuildTreeMap ( const bool bShouldPlayTransition )
{
// Stop renaming anything. We don't want pop-up windows persisting during the transition
StopRenamingNode ( ) ;
// Keep track of the last tree
LastTreeMap = TreeMap ;
LastCachedNodeVisuals = CachedNodeVisuals ;
if ( bShouldPlayTransition )
{
NavigateAnimationCurve . Play ( AsShared ( ) ) ;
}
else
{
NavigateAnimationCurve . JumpToEnd ( ) ;
}
// Kill the tree map so that it will be regenerated
TreeMap . Reset ( ) ;
CachedNodeVisuals . Reset ( ) ;
DraggingVisual = NULL ;
MouseOverVisual = NULL ;
// Customize the size and coloring of the source nodes before we rebuild it
ApplyVisualizationToNodes ( ActiveRootTreeMapNode . ToSharedRef ( ) ) ;
}
FTreeMapNodeVisualInfo * STreeMap : : FindNodeVisualUnderCursor ( const FGeometry & MyGeometry , const FVector2D & ScreenSpaceCursorPosition )
{
if ( TreeMap . IsValid ( ) )
{
const FVector2D LocalCursorPosition = MyGeometry . AbsoluteToLocal ( ScreenSpaceCursorPosition ) ;
// NOTE: Iterating backwards so that child-most nodes are checked first
for ( auto VisualIndex = CachedNodeVisuals . Num ( ) - 1 ; VisualIndex > = 0 ; - - VisualIndex )
{
auto & Visual = CachedNodeVisuals [ VisualIndex ] ;
// We only allow interactive visuals to be moused over
if ( Visual . bIsInteractive )
{
const auto VisualRect = FSlateRect ( Visual . Position , Visual . Position + Visual . Size ) ;
if ( VisualRect . ContainsPoint ( LocalCursorPosition ) )
{
return & Visual ;
}
}
}
}
return NULL ;
}
bool STreeMap : : IsNavigationTransitionActive ( ) const
{
return NavigateAnimationCurve . IsPlaying ( ) ;
}
void STreeMap : : TakeUndoSnapshot ( )
{
// If we've already undone some state, then we'll remove any undo state beyond the level that
// we've already undone up to.
if ( CurrentUndoLevel ! = INDEX_NONE )
{
UndoStates . RemoveAt ( CurrentUndoLevel , UndoStates . Num ( ) - CurrentUndoLevel ) ;
// Reset undo level as we haven't undone anything since this latest undo
CurrentUndoLevel = INDEX_NONE ;
}
// Take an Undo snapshot before we change anything
UndoStates . Add ( this - > TreeMapNodeData - > CopyNodeRecursively ( ) ) ;
// If we've hit the undo limit, then delete previous entries
const int32 MaxUndoLevels = 200 ; // @todo treemap custom: Make customizable in the settings of the widget?
if ( UndoStates . Num ( ) > MaxUndoLevels )
{
UndoStates . RemoveAt ( 0 ) ;
}
}
FTreeMapNodeDataPtr STreeMap : : FindNodeInCopiedTree ( const FTreeMapNodeDataRef & NodeToFind , const FTreeMapNodeDataRef & OriginalNode , const FTreeMapNodeDataRef & CopiedNode ) const
{
if ( NodeToFind = = OriginalNode )
{
return CopiedNode ;
}
for ( int32 ChildNodeIndex = 0 ; ChildNodeIndex < OriginalNode - > Children . Num ( ) ; + + ChildNodeIndex )
{
const auto & OriginalChildNode = OriginalNode - > Children [ ChildNodeIndex ] ;
const auto & CopiedChildNode = CopiedNode - > Children [ ChildNodeIndex ] ;
const auto & FoundMatchingCopy = FindNodeInCopiedTree ( NodeToFind , OriginalChildNode . ToSharedRef ( ) , CopiedChildNode . ToSharedRef ( ) ) ;
if ( FoundMatchingCopy . IsValid ( ) )
{
return FoundMatchingCopy ;
}
}
return NULL ;
}
void STreeMap : : Undo ( )
{
if ( UndoStates . Num ( ) > 0 )
{
// Restore from undo state
int32 UndoStateIndex ;
if ( CurrentUndoLevel = = INDEX_NONE )
{
// We haven't undone anything since the last time a new undo state was added
UndoStateIndex = UndoStates . Num ( ) - 1 ;
// Store an undo state for the current state (before undo was pressed)
TakeUndoSnapshot ( ) ;
}
else
{
// Move down to the next undo level
UndoStateIndex = CurrentUndoLevel - 1 ;
}
// Is there anything else to undo?
if ( UndoStateIndex > = 0 )
{
// Undo from history
{
const FTreeMapNodeDataRef NewTreeMapNodeData = UndoStates [ UndoStateIndex ] - > CopyNodeRecursively ( ) ;
const FTreeMapNodeDataRef NewActiveRoot = FindNodeInCopiedTree ( ActiveRootTreeMapNode . ToSharedRef ( ) , TreeMapNodeData . ToSharedRef ( ) , NewTreeMapNodeData ) . ToSharedRef ( ) ;
TreeMapNodeData = NewTreeMapNodeData ;
const bool bShouldPlayTransition = true ; // @todo treemap: A bit worried about volatility of LastTreeMap node refs here
SetTreeRoot ( NewActiveRoot , bShouldPlayTransition ) ;
}
CurrentUndoLevel = UndoStateIndex ;
}
}
}
void STreeMap : : Redo ( )
{
// Is there anything to redo? If we've haven't tried to undo since the last time new
// undo state was added, then CurrentUndoLevel will be INDEX_NONE
if ( CurrentUndoLevel ! = INDEX_NONE )
{
const int32 NextUndoLevel = CurrentUndoLevel + 1 ;
if ( UndoStates . Num ( ) > NextUndoLevel )
{
// Restore from undo state
{
const FTreeMapNodeDataRef NewTreeMapNodeData = UndoStates [ NextUndoLevel ] - > CopyNodeRecursively ( ) ;
const FTreeMapNodeDataRef NewActiveRoot = FindNodeInCopiedTree ( ActiveRootTreeMapNode . ToSharedRef ( ) , TreeMapNodeData . ToSharedRef ( ) , NewTreeMapNodeData ) . ToSharedRef ( ) ;
TreeMapNodeData = NewTreeMapNodeData ;
const bool bShouldPlayTransition = true ; // @todo treemap: A bit worried about volatility of LastTreeMap node refs here
SetTreeRoot ( NewActiveRoot , bShouldPlayTransition ) ;
}
CurrentUndoLevel = NextUndoLevel ;
if ( UndoStates . Num ( ) < = CurrentUndoLevel + 1 )
{
// We've redone all available undo states
CurrentUndoLevel = INDEX_NONE ;
// Pop last undo state that we created on initial undo
UndoStates . RemoveAt ( UndoStates . Num ( ) - 1 ) ;
}
}
}
}
void STreeMap : : ReparentNode ( const FTreeMapNodeDataRef DroppedNode , const FTreeMapNodeDataRef NewParentNode )
{
// Can't reparent the tree root node
if ( DroppedNode ! = TreeMapNodeData )
{
struct Local
{
/** Checks to see if the specified Node is a child of 'PossibleParent' at any level */
static bool IsMyParent ( const FTreeMapNodeDataRef & Node , const FTreeMapNodeDataRef & PossibleParent )
{
if ( Node = = PossibleParent )
{
return true ;
}
if ( PossibleParent - > Parent ! = NULL )
{
return IsMyParent ( Node , PossibleParent - > Parent - > AsShared ( ) ) ;
}
return false ;
}
} ;
// Can't parent owning nodes to their children
if ( ! Local : : IsMyParent ( DroppedNode , NewParentNode ) )
{
// Can't reparent to self
if ( NewParentNode ! = DroppedNode )
{
// Only if parent has changed
if ( & NewParentNode . Get ( ) ! = DroppedNode - > Parent )
{
// Take an Undo snapshot before we change anything
TakeUndoSnapshot ( ) ;
if ( DroppedNode - > Parent ! = NULL )
{
DroppedNode - > Parent - > Children . RemoveSingle ( DroppedNode ) ;
}
NewParentNode - > Children . Add ( DroppedNode ) ;
DroppedNode - > Parent = & NewParentNode . Get ( ) ;
// If we container node became a leaf node, we need to make sure it has a valid size set
// @todo treemap: This seems a bit... weird. It won't reverse either, if you put the node back (save with overridden size!) Maybe we need a bool for "auto size"=true. OR, have a separate size for Node vs. Leaf?
if ( DroppedNode - > Size = = 0.0f & & DroppedNode - > IsLeafNode ( ) )
{
DroppedNode - > Size = 1.0f ;
}
// Invalidate the tree
const bool bShouldPlayTransition = true ;
RebuildTreeMap ( bShouldPlayTransition ) ;
// After we have a new tree, play a highlight effect on the reparented node so the user can see where it ended
// up in the tree. Tree map layout can be fairly volatile after a parenting change.
HighlightPulseNode = DroppedNode ;
HighlightPulseStartTime = FSlateApplication : : Get ( ) . GetCurrentTime ( ) ;
}
}
}
}
}
FReply STreeMap : : DeleteHoveredNode ( )
{
FReply Reply = FReply : : Unhandled ( ) ;
// Wait until we've finished animating a transition
if ( ! IsNavigationTransitionActive ( ) )
{
if ( MouseOverVisual ! = NULL )
{
// Only non-root nodes can be deleted
auto NodeToDelete = MouseOverVisual - > NodeData - > AsShared ( ) ;
if ( NodeToDelete - > Parent ! = NULL )
{
// Don't allow the current active tree root to be deleted. The user should zoom out first!
if ( NodeToDelete ! = ActiveRootTreeMapNode )
{
TakeUndoSnapshot ( ) ;
// Delete the node
{
NodeToDelete - > Parent - > Children . RemoveSingle ( NodeToDelete ) ;
NodeToDelete - > Parent = NULL ;
// Note: NodeToDelete will actually be deleted when it goes out of scope later in this function (shared pointer), but it is already removed from our tree
}
const bool bShouldPlayTransition = true ;
RebuildTreeMap ( bShouldPlayTransition ) ;
Reply = FReply : : Handled ( ) ;
}
}
}
}
return Reply ;
}
FReply STreeMap : : InsertNewNodeAsChildOfHoveredNode ( const FGeometry & MyGeometry )
{
FReply Reply = FReply : : Unhandled ( ) ;
// Wait until we've finished animating a transition
if ( ! IsNavigationTransitionActive ( ) )
{
if ( MouseOverVisual ! = NULL )
{
auto ParentNode = MouseOverVisual - > NodeData - > AsShared ( ) ;
// Allocate the new node but don't add it to the tree yet. We want the user to give the node a name first!
// NOTE: Ownership of this node will be transferred to StartRenamingNode, where it will be referenced
// in the delegate callback for the editable text change commit handler. If the user opts to not type anything, the
// node will be deleted instead of being added to the tree.
FTreeMapNodeDataRef NewNode ( new FTreeMapNodeData ( ) ) ;
NewNode - > Name = TEXT ( " " ) ;
NewNode - > Size = 1.0f ; // Leaf nodes always get a size of 1.0 for now
NewNode - > Parent = MouseOverVisual - > NodeData ;
// Start editing the new node before we insert it
const bool bIsNewNode = true ;
StartRenamingNode ( MyGeometry , NewNode , MouseOverVisual - > Position , bIsNewNode ) ;
Reply = FReply : : Handled ( ) ;
}
}
return Reply ;
}
bool STreeMap : : SupportsKeyboardFocus ( ) const
{
return true ;
}
FReply STreeMap : : OnKeyDown ( const FGeometry & MyGeometry , const FKeyEvent & InKeyboardEvent )
{
FReply Reply = FReply : : Unhandled ( ) ;
const FKey Key = InKeyboardEvent . GetKey ( ) ;
if ( AllowEditing . Get ( ) )
{
if ( Key = = EKeys : : Delete )
{
// If the cursor is over a node, delete it!
DeleteHoveredNode ( ) ;
Reply = FReply : : Handled ( ) ;
}
else if ( Key = = EKeys : : Insert )
{
// If the cursor is over a node, insert a new node as a child!
InsertNewNodeAsChildOfHoveredNode ( MyGeometry ) ;
Reply = FReply : : Handled ( ) ;
}
else if ( Key = = EKeys : : Z & & InKeyboardEvent . IsControlDown ( ) )
{
// Undo
Undo ( ) ;
Reply = FReply : : Handled ( ) ;
}
else if ( Key = = EKeys : : Y & & InKeyboardEvent . IsControlDown ( ) )
{
// Redo
Redo ( ) ;
Reply = FReply : : Handled ( ) ;
}
}
return Reply ;
}
void STreeMap : : RenamingNode_OnTextCommitted ( const FText & NewText , ETextCommit : : Type , TSharedRef < FTreeMapNodeData > NodeToRename )
{
TSharedPtr < SWidget > RenamingNodeWidgetPinned ( RenamingNodeWidget . Pin ( ) ) ;
if ( RenamingNodeWidgetPinned . IsValid ( ) )
{
// Kill the window after the text has been committed
TSharedRef < SWindow > ParentWindow = FSlateApplication : : Get ( ) . FindWidgetWindow ( RenamingNodeWidgetPinned . ToSharedRef ( ) ) . ToSharedRef ( ) ;
RenamingNodeWidget . Reset ( ) ; // Avoid reentrancy
RenamingNodeData . Reset ( ) ;
FSlateApplication : : Get ( ) . RequestDestroyWindow ( ParentWindow ) ;
// Make sure new name is OK
if ( NewText . ToString ( ) ! = NodeToRename - > Name & &
! NewText . IsEmpty ( ) )
{
// Store undo state
TakeUndoSnapshot ( ) ;
// Rename it!
NodeToRename - > Name = NewText . ToString ( ) ;
if ( bIsNamingNewNode )
{
// Add the new node to the tree
TakeUndoSnapshot ( ) ;
// Insert the new node
{
auto ParentNode = NodeToRename - > Parent - > AsShared ( ) ;
ParentNode - > Children . Add ( NodeToRename ) ;
}
const bool bShouldPlayTransition = true ;
RebuildTreeMap ( bShouldPlayTransition ) ;
}
else
{
// NOTE: No refresh needed, as node labels are pulled directly from nodes
}
}
}
}
void STreeMap : : StartRenamingNode ( const FGeometry & MyGeometry , const FTreeMapNodeDataRef & NodeData , const FVector2D & RelativePosition , const bool bIsNewNode )
{
TSharedRef < SBorder > RenamerWidget = SNew ( SBorder ) ;
TSharedRef < SEditableTextBox > EditableText =
SNew ( SEditableTextBox )
. Text ( FText : : FromString ( NodeData - > Name ) )
. SelectAllTextWhenFocused ( true )
. RevertTextOnEscape ( true )
. MinDesiredWidth ( 100 )
. OnTextCommitted ( this , & STreeMap : : RenamingNode_OnTextCommitted , NodeData - > AsShared ( ) )
;
RenamerWidget - > SetContent ( EditableText ) ;
RenamingNodeWidget = RenamerWidget ;
RenamingNodeData = NodeData ;
bIsNamingNewNode = bIsNewNode ;
const bool bFocusImmediately = true ;
const bool bShouldAutoSize = true ;
FSlateApplication : : Get ( ) . PushMenu ( AsShared ( ) , RenamerWidget , MyGeometry . LocalToAbsolute ( RelativePosition ) , FPopupTransitionEffect : : None , bFocusImmediately , bShouldAutoSize ) ;
// Focus the text box right after we spawn it so that the user can start typing
FSlateApplication : : Get ( ) . SetKeyboardFocus ( EditableText , EKeyboardFocusCause : : SetDirectly ) ;
}
void STreeMap : : StopRenamingNode ( )
{
TSharedPtr < SWidget > RenamingNodeWidgetPinned ( RenamingNodeWidget . Pin ( ) ) ;
if ( RenamingNodeWidgetPinned . IsValid ( ) )
{
// Kill the window after the text has been committed
TSharedRef < SWindow > ParentWindow = FSlateApplication : : Get ( ) . FindWidgetWindow ( RenamingNodeWidgetPinned . ToSharedRef ( ) ) . ToSharedRef ( ) ;
FSlateApplication : : Get ( ) . RequestDestroyWindow ( ParentWindow ) ;
}
}
void STreeMap : : ApplyVisualizationToNodes ( const FTreeMapNodeDataRef & Node )
{
const FLinearColor RootDefaultColor = FLinearColor ( 0.125f , 0.125f , 0.125f ) ; // @todo treemap custom: Make configurable in the STreeMap settings?
int32 TreeDepth = 0 ;
ApplyVisualizationToNodesRecursively ( Node , RootDefaultColor , TreeDepth ) ;
}
void STreeMap : : ApplyVisualizationToNodesRecursively ( const FTreeMapNodeDataRef & Node , const FLinearColor & DefaultColor , const int32 TreeDepth )
{
// Select default color saturation based on tree depth
FLinearColor MyDefaultColor = DefaultColor ;
auto HSV = MyDefaultColor . LinearRGBToHSV ( ) ;
const float SaturationReductionPerDepthLevel = 0.2f ; // @todo treemap custom: Make configurable in the tree map settings? Calculate tree depth?
const float MinAllowedSaturation = 0.1f ;
HSV . G = FMath : : Max ( MinAllowedSaturation , HSV . G - TreeDepth * SaturationReductionPerDepthLevel ) ;
MyDefaultColor = HSV . HSVToLinearRGB ( ) ;
// Size
{
if ( SizeNodesByAttribute . IsValid ( ) )
{
FTreeMapAttributeDataPtr * AttributeDataPtr = Node - > Attributes . Find ( SizeNodesByAttribute - > Name ) ;
if ( AttributeDataPtr ! = NULL )
{
const FTreeMapAttributeData & AttributeData = * AttributeDataPtr - > Get ( ) ;
// Make sure the data value is in range
if ( ensure ( SizeNodesByAttribute - > Values . Contains ( AttributeData . Value ) ) )
{
// @todo treemap: This doesn't work well with container nodes. Our range of sizes doesn't go big enough to compete with cumulative sizes!
Node - > Size = SizeNodesByAttribute - > Values [ AttributeData . Value ] - > NodeSize ;
}
else
{
// Invalid attribute data!
Node - > Size = Node - > IsLeafNode ( ) ? SizeNodesByAttribute - > DefaultValue - > NodeSize : 0.0f ; // Leaf nodes get a default size, but container nodes use auto-sizing
}
}
else
{
// The node doesn't have this attribute on it. Use default.
Node - > Size = Node - > IsLeafNode ( ) ? SizeNodesByAttribute - > DefaultValue - > NodeSize : 0.0f ; // Leaf nodes get a default size, but container nodes use auto-sizing
}
}
else
{
// @todo treemap: Really we want this to "restore the original size set by the user", not make up new defaults. Disabled for now.
// Default size
// Node->Size = Node->IsLeafNode() ? 1.0f : 0.0f; // Leaf nodes get a default size, but container nodes use auto-sizing
}
}
// Color
{
if ( ColorNodesByAttribute . IsValid ( ) )
{
FTreeMapAttributeDataPtr * AttributeDataPtr = Node - > Attributes . Find ( ColorNodesByAttribute - > Name ) ;
if ( AttributeDataPtr ! = NULL )
{
const FTreeMapAttributeData & AttributeData = * AttributeDataPtr - > Get ( ) ;
// Make sure the data value is in range
if ( ensure ( ColorNodesByAttribute - > Values . Contains ( AttributeData . Value ) ) )
{
Node - > Color = ColorNodesByAttribute - > Values [ AttributeData . Value ] - > NodeColor ;
}
else
{
// Invalid attribute data!
Node - > Color = ColorNodesByAttribute - > DefaultValue - > NodeColor ;
}
}
else
{
// The node doesn't have this attribute on it. Use default.
Node - > Color = ColorNodesByAttribute - > DefaultValue - > NodeColor ;
}
}
else
{
// Default color
Node - > Color = MyDefaultColor ;
}
}
for ( int32 ChildIndex = 0 ; ChildIndex < Node - > Children . Num ( ) ; + + ChildIndex )
{
const auto & ChildNode = Node - > Children [ ChildIndex ] ;
// Make up a distinct color for all of the root's top level nodes
FLinearColor ChildColor = MyDefaultColor ;
if ( TreeDepth = = 0 )
{
// Choose a hue evenly spaced across the spectrum
float ColorHue = 360.0f * ( float ) ( ChildIndex + 1 ) / ( float ) Node - > Children . Num ( ) ;
auto ChildColorHSV = FLinearColor : : White ;
ChildColorHSV . R = ColorHue ;
ChildColorHSV . G = 1.0f ; // Full saturation!
ChildColor = ChildColorHSV . HSVToLinearRGB ( ) ;
}
ApplyVisualizationToNodesRecursively ( ChildNode . ToSharedRef ( ) , ChildColor , TreeDepth + 1 ) ;
}
}
void STreeMap : : ShowOptionsMenuAt ( const FVector2D & ScreenSpacePosition )
{
struct Local
{
static void MakeEditNodeAttributeMenu ( FMenuBuilder & MenuBuilder , FTreeMapNodeDataPtr EditingNode , TSharedPtr < FTreeMapAttribute > Attribute , STreeMap * Self )
{
MenuBuilder . AddMenuEntry (
LOCTEXT ( " RemoveAttribute " , " Not Set " ) ,
FText ( ) , // No tooltip (intentional)
FSlateIcon ( ) , // Icon
FUIAction (
FExecuteAction : : CreateStatic ( & Local : : EditNodeAttribute_Execute , EditingNode , Attribute , TSharedPtr < FTreeMapAttributeValue > ( ) , Self ) ,
FCanExecuteAction ( ) ,
FIsActionChecked : : CreateStatic ( & Local : : EditNodeAttribute_IsChecked , EditingNode , Attribute , TSharedPtr < FTreeMapAttributeValue > ( ) , Self ) ) ,
NAME_None , // Extension point
EUserInterfaceActionType : : ToggleButton ) ;
MenuBuilder . AddMenuSeparator ( ) ;
// @todo treemap: These probably should be sorted rather than hash order
for ( auto HashIter ( Attribute - > Values . CreateConstIterator ( ) ) ; HashIter ; + + HashIter )
{
FTreeMapAttributeValuePtr Value = HashIter . Value ( ) ;
MenuBuilder . AddMenuEntry (
FText : : FromName ( Value - > Name ) ,
FText ( ) , // No tooltip (intentional)
FSlateIcon ( ) , // Icon
FUIAction (
FExecuteAction : : CreateStatic ( & Local : : EditNodeAttribute_Execute , EditingNode , Attribute , Value , Self ) ,
FCanExecuteAction ( ) ,
FIsActionChecked : : CreateStatic ( & Local : : EditNodeAttribute_IsChecked , EditingNode , Attribute , Value , Self ) ) ,
NAME_None , // Extension point
EUserInterfaceActionType : : ToggleButton ) ;
}
}
static void EditNodeAttribute_Execute ( FTreeMapNodeDataPtr EditingNode , TSharedPtr < FTreeMapAttribute > Attribute , TSharedPtr < FTreeMapAttributeValue > Value , STreeMap * Self )
{
bool bAnyChanges = false ;
if ( Value . IsValid ( ) )
{
if ( ! EditingNode - > Attributes . Contains ( Attribute - > Name ) )
{
EditingNode - > Attributes . Add ( Attribute - > Name , MakeShareable ( new FTreeMapAttributeData ( Value - > Name ) ) ) ;
bAnyChanges = true ;
}
else if ( EditingNode - > Attributes [ Attribute - > Name ] - > Value ! = Value - > Name )
{
EditingNode - > Attributes [ Attribute - > Name ] - > Value = Value - > Name ;
bAnyChanges = true ;
}
else
{
// Nothing changed
}
}
else
{
// Clearing attribute
if ( EditingNode - > Attributes . Contains ( Attribute - > Name ) )
{
EditingNode - > Attributes . Remove ( Attribute - > Name ) ;
bAnyChanges = true ;
}
else
{
// Nothing changed
}
}
// Has anything changed?
if ( bAnyChanges )
{
const bool bShouldPlayTransition = true ;
Self - > RebuildTreeMap ( bShouldPlayTransition ) ;
}
}
static bool EditNodeAttribute_IsChecked ( FTreeMapNodeDataPtr EditingNode , TSharedPtr < FTreeMapAttribute > Attribute , TSharedPtr < FTreeMapAttributeValue > Value , STreeMap * Self )
{
if ( Value . IsValid ( ) )
{
return EditingNode - > Attributes . Contains ( Attribute - > Name ) & & EditingNode - > Attributes [ Attribute - > Name ] - > Value = = Value - > Name ;
}
else
{
return ! EditingNode - > Attributes . Contains ( Attribute - > Name ) ;
}
}
static void SizeByAttribute_Execute ( TSharedPtr < FTreeMapAttribute > Attribute , STreeMap * Self )
{
// Has anything changed?
if ( Self - > SizeNodesByAttribute ! = Attribute )
{
// Apply the new visualization!
Self - > SizeNodesByAttribute = Attribute ;
const bool bShouldPlayTransition = true ;
Self - > RebuildTreeMap ( bShouldPlayTransition ) ;
}
}
static bool SizeByAttribute_IsChecked ( TSharedPtr < FTreeMapAttribute > Attribute , STreeMap * Self )
{
return Self - > SizeNodesByAttribute = = Attribute ;
}
static void MakeSizeByAttributeMenu ( FMenuBuilder & MenuBuilder , STreeMap * Self )
{
MenuBuilder . AddMenuEntry (
LOCTEXT ( " NoSizeByAttribute " , " Off " ) ,
FText ( ) , // No tooltip (intentional)
FSlateIcon ( ) , // Icon
FUIAction (
FExecuteAction : : CreateStatic ( & Local : : SizeByAttribute_Execute , TSharedPtr < FTreeMapAttribute > ( ) , Self ) ,
FCanExecuteAction ( ) ,
FIsActionChecked : : CreateStatic ( & Local : : SizeByAttribute_IsChecked , TSharedPtr < FTreeMapAttribute > ( ) , Self ) ) ,
NAME_None , // Extension point
EUserInterfaceActionType : : ToggleButton ) ;
if ( Self - > Customization . IsValid ( ) )
{
MenuBuilder . AddMenuSeparator ( ) ;
// @todo treemap: These probably should be sorted rather than hash order
for ( auto HashIter ( Self - > Customization - > GetAttributes ( ) . CreateConstIterator ( ) ) ; HashIter ; + + HashIter )
{
FTreeMapAttributePtr Attribute = HashIter . Value ( ) ;
MenuBuilder . AddMenuEntry (
FText : : FromName ( Attribute - > Name ) ,
FText ( ) , // No tooltip (intentional)
FSlateIcon ( ) , // Icon
FUIAction (
FExecuteAction : : CreateStatic ( & Local : : SizeByAttribute_Execute , Attribute , Self ) ,
FCanExecuteAction ( ) ,
FIsActionChecked : : CreateStatic ( & Local : : SizeByAttribute_IsChecked , Attribute , Self ) ) ,
NAME_None , // Extension point
EUserInterfaceActionType : : ToggleButton ) ;
}
}
}
static void ColorByAttribute_Execute ( TSharedPtr < FTreeMapAttribute > Attribute , STreeMap * Self )
{
// Has anything changed?
if ( Self - > ColorNodesByAttribute ! = Attribute )
{
// Apply the new visualization!
Self - > ColorNodesByAttribute = Attribute ;
const bool bShouldPlayTransition = true ;
Self - > RebuildTreeMap ( bShouldPlayTransition ) ;
}
}
static bool ColorByAttribute_IsChecked ( TSharedPtr < FTreeMapAttribute > Attribute , STreeMap * Self )
{
return Self - > ColorNodesByAttribute = = Attribute ;
}
static void MakeColorByAttributeMenu ( FMenuBuilder & MenuBuilder , STreeMap * Self )
{
MenuBuilder . AddMenuEntry (
LOCTEXT ( " NoColorByAttribute " , " Off " ) ,
FText ( ) , // No tooltip (intentional)
FSlateIcon ( ) , // Icon
FUIAction (
FExecuteAction : : CreateStatic ( & Local : : ColorByAttribute_Execute , TSharedPtr < FTreeMapAttribute > ( ) , Self ) ,
FCanExecuteAction ( ) ,
FIsActionChecked : : CreateStatic ( & Local : : ColorByAttribute_IsChecked , TSharedPtr < FTreeMapAttribute > ( ) , Self ) ) ,
NAME_None , // Extension point
EUserInterfaceActionType : : ToggleButton ) ;
if ( Self - > Customization . IsValid ( ) )
{
MenuBuilder . AddMenuSeparator ( ) ;
// @todo treemap: These probably should be sorted rather than hash order
for ( auto HashIter ( Self - > Customization - > GetAttributes ( ) . CreateConstIterator ( ) ) ; HashIter ; + + HashIter )
{
FTreeMapAttributePtr Attribute = HashIter . Value ( ) ;
MenuBuilder . AddMenuEntry (
FText : : FromName ( Attribute - > Name ) ,
FText ( ) , // No tooltip (intentional)
FSlateIcon ( ) , // Icon
FUIAction (
FExecuteAction : : CreateStatic ( & Local : : ColorByAttribute_Execute , Attribute , Self ) ,
FCanExecuteAction ( ) ,
FIsActionChecked : : CreateStatic ( & Local : : ColorByAttribute_IsChecked , Attribute , Self ) ) ,
NAME_None , // Extension point
EUserInterfaceActionType : : ToggleButton ) ;
}
}
}
} ;
// Only present a context menu if the tree has been customized
if ( Customization . IsValid ( ) )
{
const bool bShouldCloseMenuAfterSelection = true ;
FMenuBuilder OptionsMenuBuilder ( bShouldCloseMenuAfterSelection , NULL ) ;
if ( AllowEditing . Get ( ) & & Customization . IsValid ( ) & & MouseOverVisual ! = NULL )
{
// Node editing options
OptionsMenuBuilder . BeginSection ( NAME_None , LOCTEXT ( " EditNodeAttributesSection " , " Edit Node " ) ) ;
{
const auto & EditingNode = MouseOverVisual - > NodeData - > AsShared ( ) ;
for ( auto HashIter ( Customization - > GetAttributes ( ) . CreateConstIterator ( ) ) ; HashIter ; + + HashIter )
{
FTreeMapAttributePtr Attribute = HashIter . Value ( ) ;
OptionsMenuBuilder . AddSubMenu (
FText : : FromName ( Attribute - > Name ) ,
LOCTEXT ( " EditAttribute_ToolTip " , " Edits this attribute on the node under the curosr. " ) ,
FNewMenuDelegate : : CreateStatic ( & Local : : MakeEditNodeAttributeMenu , FTreeMapNodeDataPtr ( EditingNode ) , Attribute , this ) ) ;
}
}
OptionsMenuBuilder . EndSection ( ) ;
}
OptionsMenuBuilder . BeginSection ( NAME_None , LOCTEXT ( " ChangeLayoutSection " , " Layout " ) ) ;
{
OptionsMenuBuilder . AddSubMenu ( LOCTEXT ( " SizeBy " , " Size by " ) , LOCTEXT ( " SizeBy_ToolTip " , " Sets which criteria to base the size of the nodes in the tree map on. " ) , FNewMenuDelegate : : CreateStatic ( & Local : : MakeSizeByAttributeMenu , this ) ) ;
OptionsMenuBuilder . AddSubMenu ( LOCTEXT ( " ColorBy " , " Color by " ) , LOCTEXT ( " ColorBy_ToolTip " , " Sets which criteria to base the color of the nodes in the tree map on. " ) , FNewMenuDelegate : : CreateStatic ( & Local : : MakeColorByAttributeMenu , this ) ) ;
}
OptionsMenuBuilder . EndSection ( ) ;
TSharedRef < SWidget > WindowContent =
SNew ( SBorder )
[
OptionsMenuBuilder . MakeWidget ( )
] ;
const bool bFocusImmediately = false ;
const bool bShouldAutoSize = true ;
FSlateApplication : : Get ( ) . PushMenu ( AsShared ( ) , WindowContent , ScreenSpacePosition , FPopupTransitionEffect : : ContextMenu , bFocusImmediately , bShouldAutoSize ) ;
}
}
# undef LOCTEXT_NAMESPACE