You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#lockdown Nick.Penwarden #rb none Copying //UE4/WEX-Staging to //UE4/Dev-Main (Source: //WEX/Main/Engine @ 3740665) #lockdown Nick.Penwarden Change 3739326 by Ben.Zeigler Change iteration order of depends nodes so it lists hard management references before soft management references, this is better for the UI when lots of references exist Update text for loading custom asset registry bin to be clearer Change 3739000 by John.Opila Caching optimization for text widget desired size. Change 3713551 by David.Nikdel Allow Set Properties to recognize Json array values as importable. Change 3712485 by Josh.May Added Pete's fix for the PLATFORM_TVOS/PLATFORM_IOS #define conflict introduced by mach-o/loader.h Change 3700174 by Chris.Babcock Fix setFilters crash on some Android devices Change 3691531 by Josh.May Fixed an intermittent crash that occurred when opening the AssetAuditBrower. AssetManagerEditorModule's CurrentRegistrySource was getting set too early, becoming invalid in the event that RegistrySourceMap is resized. Change 3688409 by Gil.Gribb Critical fix for an extremely rare race condition on async IO. Change 3687529 by josh.may Force layout recalculations for single-pass layout SScaleBoxes when their final scale is zero. This tends to occur in calls to SearchForWidgetRecursively before a SScaleBox's AllottedGeometry has been calculated. Change3684788by Peter.Sauerbrei fix for archive generation on the build machines Change 3684320 by john.opila Workaround for widgets disappering. Ensuring scale is never 0 so we don't get divide by zero. Change 3684042 by Peter.Sauerbrei more logging to figure out why there is not data in the Applicaiton diretory of the archive Change 3678620 by Ben.Zeigler Minor text changes to size map Change 3678314 by Ben.Zeigler Add Make Collection With References and Audit References to Size Map to easily allow inspecting the specific set of filtered packages in other tools Change 3677875 by Ben.Zeigler Fix crash in size map from keeping reference to node after map was resized, and undo the Name->DisplayName rename as it could affect licensees Change 3676899 by Peter.Sauerbrei narrowed down to the plist data, trying to figure out if it is missing or not Change 3676570 by Peter.Sauerbrei more logging to track down the archive error Change 3676293 by Peter.Sauerbrei fix for compile failure on IOS Change 3676172 by Peter.Sauerbrei potential fix for missing icons in the ipa when run through the build machines Change 3673544 by Ben.Zeigler Sort AllChunksInfo alphabetically so the order is consistent accross build and platforms to facillitate diffing Change 3671597 by Peter.Sauerbrei Merging //UE4/Dev-Mobile/Engine/... to //WEX/Main/Engine/... Change 3670932 by Ben.Zeigler Change it so cooking with the AssetManager writes out AllChunksInfo.csv next to the DevelopmentAssetRegistry, but not the per-chunk csv files as those are not useful. Also made the size counts platform accurate Change 3670906 by Peter.Sauerbrei update WEX for building with Xcode 9 Change 3660026 by Josh.May Moved SWebBrowserView's parent window "searches" to OnPaint. There's definitely something wrong with FindWidgetWindow... Even after deferring SWebBrowserView's calls to FindWidgetWindow until first Tick, the same widget layout artifacts could occur after opening multiple SWebBrowserViews. And, as Nick pointed out in the related email thread, this approach is also more efficient. Change 3655411 by Josh.May Ensure SWebBrowserView's parent window searches are deferred until after Construct. We haven't puzzled through it yet, but calling FindWidgetWindow during Construct seems to corrupt some Slate state. Deferring this search until later gets around the issue and makes sense anyway, given the widget isn't added to the hierarchy until after Construct. Change 3655407 by John.Opila Sneaking in some stats for SpawnActor. Change 3654649 by Ben.Zeigler Refactor SizeMap and ReferenceViewer into the AssetManagerEditor plugin, and delete the old modules. Fix SizeMap crash that I temporarily added. TreeMap is initialized weirdly Change 3648912 by Ben.Zeigler First half of changes to refactor sizemap/reference viewer into the asset manager editor plugin Add GetAllContentBrowserCommandExtenders to ContentBrowserModule that allows registering commands/keybinds to extend the content browser via plugins Add GetSharedMenu/ToolbarExtensibilityManager to AssetEditorToolkit that allows extending the generic asset editor via plugin Move the code to spawn the Reference Viewer and SizeMap into the AssetManagerEditor plugin so these UIs can be tightly bound and share data. This also enables keybinds for Size Map and Audit. Remove size map from the save as dialog, it created a special modal size map window that will not work after my refactor Change 3639419 by Ben.Marsh Use DirectoryInfo instead of DirectoryReference to enumerate projects. Tracking down UHT compile failures on Mac. Change 3638619 by David.Nikdel AsyncLoading: Suggested change by Gil to add lock prior to changing LoadPhase to WaitingForHeader (presumably to make FArchiveAsync2::StartReadingHeader's assumption about locking true) Change 3633562 by Chris.Babcock Update Android virtual keyboard support Change 3630564 by Peter.Sauerbrei fix for the manifest stage problem Change 3629577 by Chris.Babcock Fix merge errors in GameActivity.java Change3629154by David.Nikdel Disable debug device output in shipping builds (even if logs are enabled) Change 3626542 by John.Opila Back out changelist 3603452 Undoing the OpenGL load changes as the initial load time was just too damn high! Change 3620472 by David.Nikdel Fix from Nick to fix a BP that crashes on Compile Change 3618090 by Josh.May Reset inertial scrolling for SScrollboxes and STableView-based Slate widgets when scrolling to specific scroll offsets. Change 3613980 by Chris.Babcock Fix issue with Android password keyboard input Change 3603825 by John.Opila Shader change doesn't seem to like standalone PC. Change 3603452 by John.Opila Moving openGL shader compilation into loading instead of at the last minute. Change 3593008 by David.Nikdel Merging CL 3504471 from //Fortnite/Dev-Cinematics/Engine/... to //WEX/Main/Engine/... ---------------------------------------- Sequencer: Delay mouse capture until drag for sequencer time slider - Fixes context menus not opening as a result of mouse capture being taken on mouse down ================================================================================================= THESE CHANGES TOUCH MULTIPLE PLATFORMS ================================================================================================= Change 3739931 by Ben.Zeigler changes to some asset manager code modified on WEX, and fix several FStringAssetReference->FSoftObjectPath Change 3723451 by Josh.May Exposed OnBeforePopup to UMG and Blueprint for UWebBrowser. This is triggered by the CEFBrowserHandler when attempting to open hyperlinks targeting _blank and, when not handled, would result in the page never loading. Added OnBeforePopup handling for the HTMLNewsWidget, ensuring the URLs are opened in an external browser. Change 3711256 by Dmitriy.Dyomin Fixed: Friend list invalidation panel relative transform caching issues Also fixed issues with and set slate.cacherenderdata=0 for better batching Change 3698695 by Josh.May Made the UMG default font overridable via config, allowing us to replace it with a game-scope localized Font asset. If there's a better place for this mechanism/accessor to live, please let me know. Added a new 'Default' font that replicates '/Engine/EngineFonts/Roboto'. This also has a localized Font asset variant for zh-Hans. Change 3676085 by Josh.May Implemented MulticastBroadcastReceiver, a BroadcastReceiver capable of "multicasting" intents to other receivers. AppsFlyer defines a similar MultiInstallBroadcastReceiver class specific to the INSTALL_REFERRER intent, but it MUST be the very first one defined (cannot be guaranteed in our build pipeline AFAIK). Added MulticastBroadcastReceiver (for INSTALL_REFERRER) to the AndroidManifest generation logic, allowing BOTH Adjust and AppsFlyer to receive the intent. Added dev channel support for AndroidAppsFlyer, enabled conditionally based on shipping/distribution and whether or not a valid AppsFlyerDevChannel name is specified. For WEX, our dev channel is WEX_Dev. Fixed AppsFlyer_EventAttribute's Java class lookups and constructor signature. Change 3670860 by Ben.Zeigler First version of improvements to tools to analyze chunks Size Map and Reference Viewer now support reading cooked asset data and displaying chunks. Changing the platform dropdown in the Asset Audit window switches the other windows as well Asset Audit window now has "Add Chunks" button, and selecting AllTypes in the Primary Asset drop down will add all primary assets Size Map now shows Disk Size by default, and supports a right click context menu Significant UI improvements to all 3 tools, including keybind support Split Manage references into Hard and Soft, where Hard are set explicitly and soft are inherited. This allows determining why an asset was included in a chunk/primary asset When the AssetManager builds management information for the audit browser/cooker, it now precomputes a chunk mapping for relevant assets. PackageChunkType is used to refer to these virtual primary assets Add callback to content browser delegates to handle adding arbitrary FAssetData to an asset view, used to show chunks Several changes to the ITreeMap UI used by size map Change 3670290 by Josh.May Added AppleAppID configs for AppsFlyer. Added AdSupport and iAd frameworks for IOSAppsFlyer. According to the AppsFlyer documentation, these are required for IDFA and Apple Search Ads tracking. Change 3643531 by Peter.Sauerbrei fix for save game location and certain data backed up to the cloud when it shouldn't Change 3629303 by Ben.Zeigler Merge fix for shared ptr corruption in async loading thread from Main, and enable asnyc loading thread for WEX Copy of CL #3623261 and 3625806 Change 3629219 by Peter.Sauerbrei Merging using WEX_Main_to_UE4_WEX_Staging bringing over the files that Stan didn't have access to Change 3629063 by Stanley.Hayes Engine Merge: Merging using WEX_Main_to_UE4_WEX_Staging(flipped) Change 3618988 by Josh.May Reimplemented DevicePerformanceBucket-based WorldMap class selection to account for the WorldMap actor being pre-serialized into the UMAP. On a related note, ChildActorComponents marked as "editor only" now mark their spawned Actors as Transient to prevent them from getting serialized at cook-time. Change 3597981 by Josh.May Converted WExpCampaignDefinition's RegionDefinition refs back to hard references and, to compensate, converted WExpZoneDefinition's ZoneBoss refs to soft references. This moves the RegionDefinitions and ZoneDefinitions from chunk 2 to chunk 1 without pulling in assets for the ZoneBosses. This also allows us to grab the ZoneBoss refs during UWExpAssetManager::GetMainMenuAssetList. Reworked UWExpAssetManager::GetMainMenuAssetList and UWExpAssetManager::GetLevelAssetList to build more "complete" asset lists by expanding lists of PrimaryAssetIds. Tweaked the WorldMap's ZoneBoss spawning to account for the switch to AssetPtrs. Change 3581214 by Josh.Markiewicz added cookie deletion for Google on logout [CL 3750870 by Stanley Hayes in Main branch]
1627 lines
54 KiB
C++
1627 lines
54 KiB
C++
// Copyright 1998-2017 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( VisualPosition, BlendedVisual.Size );
|
|
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( BackgroundPosition, BackgroundSize );
|
|
|
|
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 )
|
|
{
|
|
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( MyCullingRect );
|
|
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( ShadowOffset, FVector2D( 1.0, 1.0f ) ),
|
|
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 == 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 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
|