1628 lines
54 KiB
C++
1628 lines
54 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "STreeMap.h"
|
|
#include "Rendering/DrawElements.h"
|
|
#include "Widgets/SWindow.h"
|
|
#include "Layout/WidgetPath.h"
|
|
#include "Framework/Application/MenuStack.h"
|
|
#include "Fonts/FontMeasure.h"
|
|
#include "Framework/Application/SlateApplication.h"
|
|
#include "Textures/SlateIcon.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "Widgets/Layout/SBorder.h"
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "Widgets/Input/SEditableTextBox.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;
|
|
}
|
|
}
|
|
|
|
MinimumInteractiveTreeNodeSize = InArgs._MinimumInteractiveTreeNodeSize;
|
|
MinimumVisibleTreeNodeSize = InArgs._MinimumVisibleTreeNodeSize;
|
|
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 = nullptr;
|
|
DraggingVisual = nullptr;
|
|
DragVisualDistance = 0.0f;
|
|
|
|
bIsNamingNewNode = false;
|
|
|
|
HighlightPulseStartTime = -99999.0;
|
|
|
|
OnTreeMapNodeDoubleClicked = InArgs._OnTreeMapNodeDoubleClicked;
|
|
OnTreeMapNodeRightClicked = InArgs._OnTreeMapNodeRightClicked;
|
|
|
|
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.GetLocalSize().X;
|
|
TreeMapOptions.DisplayHeight = AllottedGeometry.GetLocalSize().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 = MinimumInteractiveTreeNodeSize;
|
|
TreeMapOptions.MinimumVisibleNodeSize = MinimumVisibleTreeNodeSize;
|
|
TreeMap = ITreeMap::CreateTreeMap( TreeMapOptions, ActiveRootTreeMapNode.ToSharedRef() );
|
|
|
|
TreeMapSize = AllottedGeometry.Size;
|
|
|
|
CachedNodeVisuals = TreeMap->GetVisuals();
|
|
MouseOverVisual = nullptr;
|
|
DraggingVisual = nullptr;
|
|
|
|
// 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 != nullptr )
|
|
{
|
|
// 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& MyCullingRect, 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,
|
|
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 != nullptr && 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( BlendedVisual.Size, FSlateLayoutTransform(VisualPosition) );
|
|
auto DrawColor = InWidgetStyle.GetColorAndOpacityTint() * ThisNodeBackground->TintColor.GetColor( InWidgetStyle ) * BlendedVisual.Color;
|
|
|
|
OutDrawElements.PushClip(FSlateClippingZone(VisualClippingRect));
|
|
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements,
|
|
LayerId,
|
|
VisualPaintGeometry,
|
|
bIsMouseOverNode ? ThisHoveredNodeBackground : ThisNodeBackground,
|
|
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( BackgroundSize, FSlateLayoutTransform(BackgroundPosition) );
|
|
|
|
const FSlateRect BackgroundClippingRect = VisualClippingRect.InsetBy( FMargin( 1 ) );
|
|
|
|
OutDrawElements.PushClip(FSlateClippingZone(BackgroundClippingRect));
|
|
|
|
// Draw the background brush
|
|
FSlateDrawElement::MakeBox(
|
|
OutDrawElements,
|
|
LayerId,
|
|
BackgroundPaintGeometry,
|
|
BlendedVisual.NodeData->BackgroundBrush,
|
|
DrawEffects,
|
|
DrawColor );
|
|
|
|
OutDrawElements.PopClip();
|
|
}
|
|
|
|
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,
|
|
DrawEffects,
|
|
DrawColor );
|
|
}
|
|
}
|
|
|
|
OutDrawElements.PopClip();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// 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 != nullptr && 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 )
|
|
{
|
|
FGeometry VisualGeometry = AllottedGeometry.MakeChild(BlendedVisual.Size, FSlateLayoutTransform(VisualPosition));
|
|
const FPaintGeometry VisualPaintGeometry = VisualGeometry.ToPaintGeometry();
|
|
|
|
|
|
// Clip the text to the visual's rectangle, with some extra inner padding to avoid overlapping the visual's border
|
|
FSlateRect TextClippingRect = VisualGeometry.GetLayoutBoundingRect();
|
|
TextClippingRect = TextClippingRect.InsetBy( FMargin( ChildContainerTextPadding, 0, ChildContainerTextPadding, 0 ) );
|
|
if( TextClippingRect.IsValid() )
|
|
{
|
|
OutDrawElements.PushClip(FSlateClippingZone(TextClippingRect));
|
|
|
|
// 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,
|
|
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,
|
|
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
|
|
const FSlateFontInfo* BlendedVisualCenterTextFont = &BlendedVisual.CenterTextFont;
|
|
const FVector2D FullTextSize = FontMeasureService->Measure( BlendedVisual.NodeData->CenterText, *BlendedVisualCenterTextFont );
|
|
if( ScreenSpaceVisualSize.X < FullTextSize.X || ScreenSpaceVisualSize.Y < FullTextSize.Y ) // @todo treemap: assumption that NameFont will be smaller than CenterText font
|
|
{
|
|
BlendedVisualCenterTextFont = &BlendedVisual.NameFont;
|
|
}
|
|
|
|
const FVector2D TextSize = FontMeasureService->Measure( BlendedVisual.NodeData->CenterText, *BlendedVisualCenterTextFont );
|
|
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,
|
|
*BlendedVisualCenterTextFont,
|
|
DrawEffects,
|
|
InWidgetStyle.GetColorAndOpacityTint() * VisualTextColor );
|
|
}
|
|
|
|
OutDrawElements.PopClip();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
if( DraggingVisual != nullptr && 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( FVector2f( 1.0, 1.0f ), FSlateLayoutTransform(ShadowOffset) ),
|
|
SplineStart,
|
|
SplineStartDir,
|
|
SplineEnd,
|
|
SplineEndDir,
|
|
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 = nullptr;
|
|
|
|
// 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 != nullptr )
|
|
{
|
|
// Mouse is over a visual
|
|
MouseOverVisual = NodeVisualUnderCursor;
|
|
}
|
|
|
|
Reply = FReply::Handled();
|
|
}
|
|
|
|
if( DraggingVisual != nullptr )
|
|
{
|
|
DragVisualDistance += InMouseEvent.GetCursorDelta().Size();
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
|
|
void STreeMap::OnMouseLeave( const FPointerEvent& InMouseEvent )
|
|
{
|
|
MouseOverVisual = nullptr;
|
|
}
|
|
|
|
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 != nullptr )
|
|
{
|
|
// 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();
|
|
FTreeMapNodeVisualInfo* NodeVisualUnderCursor = FindNodeVisualUnderCursor(InMyGeometry, InMouseEvent.GetScreenSpacePosition());
|
|
if( InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton )
|
|
{
|
|
auto* DroppedVisual = DraggingVisual;
|
|
DraggingVisual = nullptr;
|
|
|
|
if( DroppedVisual != nullptr && DragVisualDistance >= STreeMapDefs::MinCursorDistanceForDraggingVisual )
|
|
{
|
|
// Wait until we've finished animating a transition
|
|
if( !IsNavigationTransitionActive() && NodeVisualUnderCursor != nullptr )
|
|
{
|
|
// 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 if( NodeVisualUnderCursor != nullptr )
|
|
{
|
|
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 )
|
|
{
|
|
if (OnTreeMapNodeRightClicked.IsBound())
|
|
{
|
|
if (NodeVisualUnderCursor != nullptr)
|
|
{
|
|
OnTreeMapNodeRightClicked.Execute(*NodeVisualUnderCursor->NodeData, InMouseEvent);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Show a pop-up menu!
|
|
ShowOptionsMenuAt(InMouseEvent);
|
|
}
|
|
}
|
|
else if ( InMouseEvent.GetEffectingButton() == EKeys::ThumbMouseButton )
|
|
{
|
|
// Back button
|
|
if (ZoomOut() && !Reply.IsEventHandled())
|
|
{
|
|
Reply = FReply::Handled();
|
|
}
|
|
}
|
|
else if ( InMouseEvent.GetEffectingButton() == EKeys::ThumbMouseButton2 )
|
|
{
|
|
if (NodeVisualUnderCursor != nullptr)
|
|
{
|
|
// Do zoom behavior
|
|
const bool bShouldPlayTransition = true;
|
|
SetTreeRoot(NodeVisualUnderCursor->NodeData->AsShared(), bShouldPlayTransition);
|
|
}
|
|
}
|
|
|
|
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 != nullptr )
|
|
{
|
|
// 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, InMouseEvent );
|
|
}
|
|
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 != nullptr )
|
|
{
|
|
// 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 != nullptr )
|
|
{
|
|
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 != nullptr )
|
|
{
|
|
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 );
|
|
}
|
|
}
|
|
|
|
|
|
FTreeMapNodeDataPtr STreeMap::GetTreeRoot() const
|
|
{
|
|
return ActiveRootTreeMapNode;
|
|
}
|
|
|
|
|
|
bool STreeMap::CanZoomOut() const
|
|
{
|
|
if (ActiveRootTreeMapNode.IsValid() && ActiveRootTreeMapNode->Parent != nullptr)
|
|
{
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool STreeMap::ZoomOut()
|
|
{
|
|
if (!CanZoomOut())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const bool bShouldPlayTransition = true;
|
|
SetTreeRoot(ActiveRootTreeMapNode->Parent->AsShared(), bShouldPlayTransition);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
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 = nullptr;
|
|
MouseOverVisual = nullptr;
|
|
|
|
// Update ActiveRootTreeMapNode
|
|
FTreeMapNodeDataPtr NewActiveRoot = FindNodeInCopiedTree(ActiveRootTreeMapNode.ToSharedRef(), TreeMapNodeData.ToSharedRef(), TreeMapNodeData.ToSharedRef());
|
|
|
|
if (NewActiveRoot.IsValid())
|
|
{
|
|
ActiveRootTreeMapNode = NewActiveRoot;
|
|
}
|
|
else
|
|
{
|
|
ActiveRootTreeMapNode = TreeMapNodeData;
|
|
}
|
|
|
|
// 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 nullptr;
|
|
}
|
|
|
|
|
|
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.Get() == OriginalNode.Get() )
|
|
{
|
|
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 nullptr;
|
|
}
|
|
|
|
|
|
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 != nullptr )
|
|
{
|
|
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 != nullptr )
|
|
{
|
|
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 != nullptr )
|
|
{
|
|
// Only non-root nodes can be deleted
|
|
auto NodeToDelete = MouseOverVisual->NodeData->AsShared();
|
|
if( NodeToDelete->Parent != nullptr )
|
|
{
|
|
// 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 = nullptr;
|
|
|
|
// 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 != nullptr )
|
|
{
|
|
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->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;
|
|
FSlateApplication::Get().PushMenu( AsShared(), FWidgetPath(), RenamerWidget, MyGeometry.LocalToAbsolute( RelativePosition ), FPopupTransitionEffect::None, bFocusImmediately );
|
|
|
|
// Focus the text box right after we spawn it so that the user can start typing
|
|
FSlateApplication::Get().SetKeyboardFocus( EditableText, EFocusCause::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.05f; // @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 != nullptr )
|
|
{
|
|
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 != nullptr )
|
|
{
|
|
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 FPointerEvent& InMouseEvent)
|
|
{
|
|
FWidgetPath WidgetPath = InMouseEvent.GetEventPath() != nullptr ? *InMouseEvent.GetEventPath() : FWidgetPath();
|
|
const FVector2D& ScreenSpacePosition = InMouseEvent.GetScreenSpacePosition();
|
|
|
|
ShowOptionsMenuAtInternal(ScreenSpacePosition, WidgetPath);
|
|
}
|
|
|
|
void STreeMap::ShowOptionsMenuAtInternal(const FVector2D& ScreenSpacePosition, const FWidgetPath& WidgetPath)
|
|
{
|
|
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, nullptr );
|
|
|
|
if( AllowEditing.Get() && Customization.IsValid() && MouseOverVisual != nullptr )
|
|
{
|
|
// 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;
|
|
FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, WindowContent, ScreenSpacePosition, FPopupTransitionEffect::ContextMenu, bFocusImmediately);
|
|
}
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|