Files
UnrealEngineUWP/Engine/Source/Developer/TreeMap/STreeMap.cpp
Chris Wood 8a112d6140 PushMenu() now respects QueryPopupMethod(). All menus now support reusing windows.
UE-14641 - Fix PushMenu() to use QueryPopupMethod()

Pretty big refactor
Adds IMenu as way to identify menus. Replaces referring to menus as SWindows.
Lots of uses of PushMenu() fixed up to match new API

#codereview Nick.Atamas

[CL 2579277 by Chris Wood in Main branch]
2015-06-05 20:19:33 -04:00

1564 lines
52 KiB
C++

// 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
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,
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 );
}
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;
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, 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.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 != 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)
{
// deprecated
ShowOptionsMenuAtInternal(ScreenSpacePosition, FWidgetPath());
}
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, 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;
FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, WindowContent, ScreenSpacePosition, FPopupTransitionEffect::ContextMenu, bFocusImmediately);
}
}
#undef LOCTEXT_NAMESPACE