Files
UnrealEngineUWP/Engine/Source/Developer/TreeMap/STreeMap.cpp
Marc Audy 2f10ee3611 Copying //UE4/Dev-Framework to //UE4/Dev-Main (Source: //UE4/Dev-Framework @ 3544039)
#lockdown Nick.Penwarden
#rb none
#rnx

=====================================
 MAJOR FEATURES + CHANGES
=====================================

Change 3343905 by Dan.Oconnor

	ResolveMember optimizations and moved into cpp. ResolveMember<UFunction> now checks UClass::FuncMap before doing more expensive searches

Change 3346637 by Ben.Zeigler

	Actually fix in non editor builds

Change 3355484 by Dan.Oconnor

	Back out FMemberReference Optimization

Change 3425833 by Ben.Zeigler

	#jira UE-31749 Fix it so Undo works properly when modifying a local variable
	#jira UE-44736 Fix it so changing the type of a local variable correctly resets the default value

Change 3510091 by Marc.Audy

	Expose on Spawn functional test

	#rnx

Change 3510100 by Marc.Audy

	Fix spelling error

	#rnx

Change 3510132 by Marc.Audy

	Fix issues with marking a widget blueprint class as abstract

Change 3510133 by Marc.Audy

	Minor code cleanup

	#rnx

Change 3510178 by Ben.Zeigler

	#jira UE-46500 Fix it so editor-only and transient stuct members are not serialized for literal blueprint structs. It's unsafe to serialize them because they may not exist in the cooked build

Change 3510466 by Ben.Zeigler

	Start adding basic ability system tests to enginetest, very minimal so far

Change 3511295 by Marc.Audy

	Fix wasted work going weak -> object -> weak -> object

	#rnx

Change 3511824 by Marc.Audy

	Fix spelling error in tooltip
	#jira UE-46515

	#rnx

Change 3514446 by Ben.Zeigler

	Fix ActorBoundEvent and ComponentBoundEvent to always refresh their event signature from the delegate property they are bound to. This is required to correctly deal with delegate signatures being moved or renamed. Both types now do the fixup one time, in ReconstructNode.

Change 3514578 by Marc.Audy

	Move clearing of the actor component need end of frame update mark to base class instead of just primitive component

Change 3514583 by Ben.Zeigler

	Better fix to last delegate checkin that also handles moving functions between modules but not renaming

Change 3515325 by Dan.Oconnor

	Fix for rare orphan pin false positive, rare exposed on spawn false positive

	#rnx

Change 3515761 by Marc.Audy

	fix shipping configuration

	#rnx

Change 3515772 by Marc.Audy

	Fix static analysis warnings

	#rnx

Change 3516287 by Marc.Audy

	Fix references to instanced components not being updated when resetting component to default
	#jira UE-44706

	#rnx

Change 3516303 by Marc.Audy

	Back out CL# 3516287 while an oddity is investigated

	#rnx

Change 3516563 by Marc.Audy

	(4.17) Fix references to instanced components not being updated when resetting component to default
	#jira UE-44706

Change 3516637 by Phillip.Kavan

	#jira UE-44661 - Fix potential crash when changing the ChildActorComponent class default value on a Blueprint that also sets the class in the Construction Script.

	Change summary:
	- Modified UChildActorComponent::DestroyChildActor() to move the check for PendingKill/Unreachable so that we can also rename a defunct ChildActor instance out of the way in order to allow for a new ChildActor instance w/ the cached name.

Change 3517735 by Marc.Audy

	Avoid unnecessary string copy

	#rnx

Change 3517931 by Marc.Audy

	Small optimization to CleanupActors

Change 3518221 by Dan.Oconnor

	Fix rare crash when running ConformImplementedEvents when async loading
	#jira UE-45348

Change 3518270 by Ben.Zeigler

	#jira UE-46574 Add FCollectionReference type and customization to allow setting an FName to an editor collection
	Add AssetCollection to PrimaryAssetLabel that derives the bundled assets from an editor collection

Change 3518271 by Marc.Audy

	Get rid of unnecessary construction differentiation if custom reset is being used

Change 3518310 by Ben.Marsh

	Re-adding IOS files with correct case.

Change 3518423 by Ben.Zeigler

	#jira UE-46574 Initial support for chunk installation in Asset Manager.
	Refactor AssetManagerSettings so it copies runtime bools into the asset manager for fast access
	Add a concept of a stalled streamable manager handle, handles can be created stalled and will not execute their async load until all needed resources have been acquired externally

Change 3518480 by Marc.Audy

	Correctly get the variable reference for an input variable get from the member scope rather than a member variable of the same name on the class
	#jira UE-46737

Change 3518498 by Ben.Zeigler

	Fix bug with AssetManager where requesting the same load twice in a row before the first one finishes caused the complete callback to get called too early for the second load
	Update test map to catch this

Change 3518526 by Ben.Zeigler

	IOS Fix

Change 3518619 by Ben.Zeigler

	#jira UE-46744 Fix issue where refreshing asset manager editor settings would throw away asset label rules overrides, causing the recursive flag to accidentally get set

Change 3518747 by Phillip.Kavan

	#jira UE-43154 - Prevent ConstructGenericObject nodes from compiling if the selected type does not include 'BlueprintType' in its inheritance hierarchy.

	Change summary:
	- Moved UGameplayStatics::CanSpawnObjectOfClass() into UK2Node_GenericCreateObject as a local util method (per JIRA notes). This was not exposed to Blueprints and as such was inconsistent with the rest of the API.
	- Modified UGameplayStatics::SpawnObject() to no longer call CanSpawnObjectOfClass(). This seemed redundant as this will already have been called during node validation at Blueprint compile time.
	- Refactored CanSpawnObjectOfClass() into FK2Node_GenericCreateObject_Utils. Walking up the inheritance chain no longer starts out w/ the assumption that 'BlueprintType' is set by default, which was previously including a lot of engine-specific classes into the "allowed" set (e.g. UByteProperty). Also unified the 2 loop iterations that were being used to check for 'BlueprintType'/'NotBlueprintType' and 'DontUseGenericSpawnObjectName', as well as the check for whether or not the class is a derivative of AActor/UActorComponent.
	- Modified UK2Node_GenericCreateObject::EarlyValidation() to call FK2Node_GenericCreateObject_Utils::CanSpawnObjectOfClass() and emit a slightly more informative error message to the BP compiler message log.

Change 3518756 by Michael.Noland

	(4.17) Framework: Prevent various asserts when USplineComponent methods are called on a spline with no points

Change 3518760 by Michael.Noland

	Core: Changed FRuntimeAssetCache ensures to ensureAsRuntimeWarning

Change 3518771 by Michael.Noland

	AI: Prevent an ensure in UBlackboardComponent::ClearValue when called on a component with a null BlackboardAsset

Change 3518818 by Michael.Noland

	Rendering: Fixed a whitespace issue in UCanvasRenderTarget2D::RepaintCanvas()

	#rnx

Change 3518822 by Michael.Noland

	Sequencer: Prevented crashes in some methods of UMovieSceneSequencePlayer when there is no Sequence set
	Sequencer: Prevented a crash in FMovieSceneRootEvaluationTemplateInstance::Evaluate when the instance has no template set

Change 3518824 by Michael.Noland

	Landscape: Marked ULandscapeComponent and ULandscapeHeightfieldCollisionComponent as Within=LandscapeProxy, since they do CastChecked on their Outer all the time

Change 3519073 by Michael.Noland

	QAGame: Fixed a crash in UQASynth::PlaySynth() if called on a directly created instance rather than using the factory method

Change 3519076 by Michael.Noland

	Preventing crashes in UAutomationPerformaceHelper (sic) when spawned abnormally for fuzzing (assumes that the outer will have a route to a world)
	#rnx

Change 3519079 by Michael.Noland

	Sequencer: Fixed a potential crash in UMediaPlaylist::Insert and UMediaPlaylist::RemoveAt when passed an invalid index

Change 3519081 by Michael.Noland

	Blueprints: Added support for creating appropriate outers for objects that must be nested within another class during fuzzing (ones that specify Within=, other relationships aren't knowable yet)

Change 3519082 by Michael.Noland

	VR: Prevent a crash in UMRMeshComponent::ConnectReconstructor when passed a null reconstructor

Change 3519084 by Michael.Noland

	Rendering: Prevent crashes when UNiagaraComponent::GetEffectDataInterface is called on a component with no effect asset set

Change 3521889 by Michael.Noland

	Sequencer: Prevented a bogus static analysis warning by reworking the code (FixedFrameInterval could have only been set if the pointer were valid from the line above)
	#rnx

Change 3521987 by Michael.Noland

	Animation: Prevent a couple of potential asserts in UControlRig::GetOrAllocateSubControlRig

Change 3522101 by Michael.Noland

	Physics: Improved the comment on UPhysicalMaterial::Friction

	#rn

Change 3522105 by Michael.Noland

	Physics: Fixed a few crashes in UVehicleWheel when spawned directly

Change 3522106 by Michael.Noland

	Framework: Marked ULevelStreaming as Within=World, since it does CastChecked on the Outer all the time

Change 3522109 by Michael.Noland

	Animation: Marked UAnimInstance as Within=SkeletalMeshComponent since it assumes the outer in various places

Change 3522121 by Michael.Noland

	Mobile: Prevent UMobileInstalledContent methods from crashing when called on a created instance in an uncooked build (no installed manifest)

Change 3522783 by Zak.Middleton

	#ue4 - Imported new simple collision for Engine/Content/BasicShaps/Cylinder.uasset which is a single convex shape (rather than being 4 shapes as before).

Change 3525477 by Dan.Oconnor

	Remove Tooltip, Category, and HideCategories tooltip from the blueprint generated class if source data is cleared

Change 3526538 by Ben.Zeigler

	Refresh primary asset labels if their bundles are different at all and not just if they're added or removed. This is required because they now work based on collections or directories. This fixes issue with the onboarding collection changes not correctly modifying chunks
	Copy of CL #3526501

Change 3526817 by Ben.Zeigler

	#jira UE-46917 Fix issue where maps that do not contain level script blueprints were being counted as unindexed for find in blueprints. The old behavior depended on detecting the existence of empty tags, but the asset registry now filters those out so treat maps with no FiB data as indexed

Change 3526873 by Ben.Zeigler

	#jira UE-46627 Change it so blueprint or native subclasses of static mesh actor cannot be added to clusters, as they are not likely to be immutable the way the base class is
	Add code to to the ubergraph frame to fall back to hard reference serialization if the reference collector doesn't support weak references, such as the cluster collector

Change 3526958 by Marc.Audy

	(4.17) Don't copy and then break pin links when reconstructing. Instead simply move.
	#jira UE-46935

Change 3528916 by Marc.Audy

	PR #3609: Adds GetKeysForAxis() to complement GetKeysForAction() in UPlayerInput (Contributed by alanedwardes)
	#jira UE-45347

Change 3529080 by mason.seay

	BP asset for undetermined type bug

Change 3529381 by Marc.Audy

	Fix ability to insert duplicates in to a set or map

Change 3529471 by Dan.Oconnor

	Fix for clang 4.0 error: definition of builtin function '__rdtsc' inline unsigned long long __rdtsc()

Change 3530876 by Marc.Audy

	Based on PR #3457: Add MakeSet BP node (Contributed by projectgheist)
	Also refactored MakeArray/Set to share a base MakeContainer class
	Cleaned up some dead code from MakeArray
	Added icon for MakeSet
	Added Functional Test for MakeSet
	#jira UE-43717

Change 3531070 by Phillip.Kavan

	#jira UE-46866 - Fix crash on load when an external variable member reference's owning type cannot be loaded.

	Change summary:
	- Modified FBlueprintEditorUtils::GetSkeletonClass() to check for NULL before attempting to check for the generating BP.

Change 3531081 by Marc.Audy

	Remove deprecated CustomMapParamValue code

Change 3531094 by Phillip.Kavan

	#jira UE-46952 - Fix a packaging code build failure that will occur with a nativized Blueprint class that contains a UInterfaceProperty.

	Change summary:
	- Modified TScriptInterface::operator=() to cast the given 'SourceObject' instance to the 'InterfaceType' type before assigning to 'SourceInterface'. This was necessary because if the caller (in this case nativized codegen) passes in a UObject* that does not explicitly inherit from 'InterfaceType', then it will need to go through the object's GetInterfaceAddress() API instead and cast the result back to an 'InterfaceType' pointer.

Change 3531186 by Phillip.Kavan

	Back out changelist 3531094 (temp CIS fix).

	#rnx

Change 3532082 by Marc.Audy

	Move garbage collection timers and other management to UEngine instead of UWorld
	Fixes CollectGarbage blueprint node not working in shipping
	#jira UE-46566

Change 3532134 by Phillip.Kavan

	Restored changelist 3531094 w/ fix for non-unity.

	- Mirrored from //UE4/Release-4.17 (CL# 3531232).

	#rnx

Change 3533009 by Marc.Audy

	Fixup missing function and deprecation warnings

Change 3534056 by Marc.Audy

	(4.17) Fix expose on spawn of map and sets to work
	#jira UE-47140

Change 3534761 by Marc.Audy

	(4.17) Apply code review changes to Dev-Framework as well

	#rnx

Change 3535147 by Dan.Oconnor

	Build fix, already made in 4.17

	#rnx

Change 3535530 by mason.seay

	Resaving to remove error when opening level blueprint

Change 3535581 by Marc.Audy

	Class Properties are only identical if they are literally the same object. Do not consider the deep compare port flags as object property base does.
	#jira UE-46533

Change 3535583 by Marc.Audy

	When properties are imported in to a child actor component the cached instance data is invalidated, so clear it.
	#jira UE-46533

Change 3535617 by Marc.Audy

	PR #3788: UE-39237: Prevent (im-)pure casting during BP debugging (Contributed by projectgheist)
	#jira UE-47188
	#jira UE-39237

Change 3535671 by Marc.Audy

	Change NodeFactory to look at interface to use sequence node instead of each node having to add itself

Change 3535955 by Marc.Audy

	Prevent MakeSet from removing split pins

Change 3536114 by Michael.Noland

	Paper2D: Removing deprecated code from 4.3/4.4 era

	#rnx

Change 3536120 by Michael.Noland

	Animation: Removed deprecated FTAlphaBlend class and AlphaBlendType.h header

Change 3536124 by Michael.Noland

	Physics: Removed deprecated methods that were replaced by _AssumesLocked variations

Change 3536131 by Michael.Noland

	Slate: Converting remaining uses of EKeyboardFocusCause to EFocusCause and properly deprecating it

Change 3536138 by Michael.Noland

	Slate: Removed any deprecated code older than 4.10 that didn't affect content compatibility

Change 3536167 by Dan.Oconnor

	When a client provides a skeleton class as the self scope, make sure we also use a skel class for non-self scopes - but only if using the compilation manager. Skel classes are not reliably up to date when not using the compilation manager
	#jira UE-46904

Change 3536221 by Michael.Noland

	Editor: Removing deprecated code from 4.9 or earlier

Change 3536240 by Michael.Noland

	Blueprints: Removed long-deprecated TypeToString method from the K2 schema
	#rnx

Change 3536243 by Michael.Noland

	AI: Prevent crashes if UMockTask_Log is created manually rather than via the CreateTask factory method

Change 3536244 by Michael.Noland

	Core: Prevent FScopedExternalProfilerBase::StopScopedTimer() from asserting if called an unmatched number of times with StartScopedTimer, as both are exposed to BPs now

Change 3536250 by Michael.Noland

	CoreUObject: Removed any deprecated code older than 4.10 that didn't affect content compatibility

Change 3536253 by Michael.Noland

	Core: Removed any deprecated code older than 4.10 that didn't affect content compatibility

Change 3536310 by Michael.Noland

	Engine: Removed any deprecated code older than 4.10 that didn't affect content compatibility

Change 3536397 by Mieszko.Zielinski

	Fixed UCrowdFollowingComponent::UpdateCachedDirections crashing when CharacterMovement is not set #UE4

	#jira UE-46860

Change 3536404 by Michael.Noland

	Platform: Added a warning for others when they try to remove this 'deprecated' method

Change 3536639 by Michael.Noland

	CharacterMovement: Changed the name of a variable introduced in CL# 3536397 to better match intent
	#rnx

Change 3536893 by Michael.Noland

	Blueprints: Clear the stale value on the value pin when a map find node fails to find an item
	#jira UE-47233

Change 3536902 by Michael.Noland

	Framework: Killed a couple of more deprecated methods that were not exposed to Blueprints

	#rnx

Change 3537038 by Ben.Marsh

	Fixing case of iOS directories, pt1

Change 3537039 by Ben.Marsh

	Fixing case of iOS directories, pt2

Change 3538246 by Michael.Noland

	UnrealTournament: Fixing issues with renamed enum

	#rnx

Change 3538618 by Ben.Zeigler

	Fix ensure when closing sequencer transform UI

Change 3540213 by Ben.Zeigler

	#jira UE-47313 Fix crash serializing a MapProperty where the value type has changed for a type that implements ConvertFromType. The address passed to ConvertFromType needs to be the container root, not the specific value address, keys worked because the offset was 0.

Change 3540253 by Marc.Audy

	Only copy default values for input pins as output pins do not have them

	#rnx

Change 3540376 by Marc.Audy

	Add utility FromPinType for FEdGraphTerminalType

	#rnx

Change 3540433 by Marc.Audy

	Add MakeMap
	#jira UE-47093
	Unify IsConnectionDisallowed for containers and fix static analysis warning
	#jira UE-47291

Change 3540585 by Phillip.Kavan

	#jira UE-47117 - Fix crash on launch of a nativized build that includes an instanced default subobject that's referenced by another instanced default subobject.

	Change summary:
	- Modified FEmitDefaultValueHelper::HandleSpecialTypes() to only direct HandleInstancedSubobject() to emit code to create the instanced subobject if it's not a default subobject. This was previously being incorrectly interpreted as an object having the 'RF_ArchetypeObject' flag set; however, default subobjects will also have that flag set in addition to the 'RF_DefaultSubobject' flag.
	- Modified FEmitDefaultValueHelper::HandleInstancedSubobject() to assert in the 'GetDefaultSubobjectByName' case if the given object is not also a default subobject.

Change 3541147 by Dan.Oconnor

	Fix for not being able to override custom events when using the compilation manager post 3536167
	#jira UE-47292
	#rnx

Change 3541177 by Ben.Zeigler

	#jira UE-46595, UE-46553 Fix issue where creating a widget template could cause a widget blueprint being cooked to have the wrong package flags, making it appear to be an uncooked package
	Copy of CL #3541027

Change 3541325 by Dan.Oconnor

	K2node data table data needs to preload data before the compilation queue is flushed

	#rnx
	#jira UE-47319

Change 3541409 by Michael.Noland

	Blueprints: Added code to reapply any active breakpoints after recompilation when using the BP compilation manager
	#jira UE-47322

	[reimplementing CL# 3541404 in Dev-Framework]

Change 3541418 by Dan.Oconnor

	Fix for bad SKEL_ CDO reference in blueprint bytecode
	#jira UE-47265

	#rnx

Change 3541482 by Dan.Oconnor

	Blanket fix up of preload calls that are being done in AllocateDefaultPins. AllocatDefaultPins is not called until compile, meaning if these preload calls load blueprints they will be loaded while the compilation manager is compiling blueprints

	#rnx
	#jira UE-47319

Change 3541817 by Marc.Audy

	Fix static analysis warnings

	#rnx

Change 3542299 by Michael.Noland

	Blueprints: Speculative fix for static analysis warning
	#rnx

Change 3542406 by Marc.Audy

	Use a check slow to avoid any cost

	#rnx

Change 3542486 by Michael.Noland

	Asset Manager: Removing an unnecessary ensure (it's a potentially expected case)

	#jira UE-47380

Change 3542659 by Michael.Noland

	Blueprints: Clear out null entries in the LastEditedDocuments list during PostLoad() and remove entries when a graph is being deleted to prevent their generation in the first place
	#jira UE-47385

Change 3543620 by Dan.Oconnor

	Remove overzealous ensure - we may recompile blueprints that are asynchronously loading when a user triggers a synchronous compile
	#jira UE-47415
	#rnx

Change 3518415 by Ben.Zeigler

	#jira UE-46574 Deprecate IPlatformChunkInstall::SetChunkInstallDelgate as it was spelled wrong, was only half implemented, and did not support success vs failure
	Replace with AddChunkInstallDelegate, which supports a bool error code and is bound once instead of separately for each chunk. All implementations support this delegate at a basic level, although several could be improved to call the failure delegate in more cases

Change 3534339 by Michael.Noland

	Platforms: Changed DEPRECATED() macro description to use 4.xx rather than a speciifc version in examples, so it doesn't show up when removing deprecated code

[CL 3544050 by Marc Audy in Main branch]
2017-07-19 09:49:59 -04:00

1564 lines
52 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;
}
}
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.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 = 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& 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 != 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;
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 != 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( 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 != 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,
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, 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 != 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 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