You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#rb none
#lockdown Nick.Penwarden
============================
MAJOR FEATURES & CHANGES
============================
Change 3626305 by Phillip.Kavan
#jira UE-49269 - Workaround fix for crash after packaging a nativized QAGame build with all AnimBP assets disabled for nativization by default.
Change 3627162 by Phillip.Kavan
#jira UE-49239 - Fix an invalid cast emitted to nativized codegen for converted AnimBP types.
- Regression introduced in CL# 3613358.
Change 3756887 by Ben.Zeigler
#jira UE-52380 Fix inconsistency with how FSoftObjectPtr case is managed between FLinkerSave and FArchiveSaveTagImports, which would cause a cook ensure under some circumstances
Copy of CL #3756788
Change 3756888 by Ben.Zeigler
#jira UE-45505 Fix issue where FCoreUObjectDelegates::OnAssetLoaded was being called from an inner loop inside EndLoad. Maps would register components from that callback, and if those registers started their own loads, those objects would be returned in a partially loaded state. We now defer the asset loaded callback to the very end of the loop so recursive loads work properly
Copy of CL #3753986
#thomas.sarkanen
Change 3759254 by Ben.Zeigler
Disable the duplicate PrimaryAssetId for editor only types like Maps. This can happen if content folk copy maps using the content browser, and does not actually cause a runtime problem. It still ensures for cooked types
Change 3759278 by Ben.Zeigler
Add IsTempPackage to FPackageName
Fix issue where temp/memory packages shown in a content browser/asset audit window would spam the log when it failed to find source control info for them
Change 3759613 by Phillip.Kavan
Add support for casting between mismatched soft pointer types in nativized Blueprint C++ assignment statements and function calls.
Change summary:
- Extended FEmitHelper::GenerateAutomaticCast() to consider soft pointer terms and inject C++ code to explicitly cast the RHS when needed.
#jira UE-52205
Change 3760040 by Dan.Oconnor
Add Call Stack control for viewing Blueprint call stacks when paused at a breakpoint, available from the Developer Tools menu
#jira UE-2296
Change 3760955 by Phillip.Kavan
Fix conditional (SA/CIS issue).
Change 3761356 by Ben.Zeigler
Fix DLC staging rules to handle metadata correctly and remove debug log I accidentally added. The DLC staging now iterates in a similar way to the normal staging, it just may also excluded Engine
Change 3761859 by Zak.Middleton
#ue4 - Fix crash in UStaticMesh::GetAssetRegistryTags() when FindObject is used during saving. Added Lex::ToString for physics enums ECollisionTraceFlag, EPhysicsType, and EBodyCollisionResponse.
#jira UE-52478
#tests QA game, content browser
Change 3761860 by mason.seay
Submitting test content for Async Load issue
Change 3762559 by Ben.Zeigler
#jira UE-52407 Fix it so FText can be specified in blueprint functions as default parameters. The UI showed up before but the data was lost
Change GetDefaultsAsString on Pin to always return an internal string so it can correctly be import texted, add GetDefaultsAsText for display purposes
Change 3764459 by Marc.Audy
PR #4224: Fix LoadLevelInstanceBySoftObjectPtr (Contributed by phlknght)
#jira UE-52415
Change 3764580 by Ben.Zeigler
Clean up delegates in UObjectGlobals.h, fixing several incorrect comments and moving some editor delegates into WITH_EDITOR
Change 3764602 by Ben.Zeigler
#jira UE-52487 Fix it so OnAssetLoaded gets correctly called for Assets that were async loaded while in the editor/standalone editor game.
This is necessary because they would not get registered with various editor systems for the rest of the editor session, even if opened manually
Change 3764603 by Ben.Zeigler
Related to UE-52487, now that async loading blueprints in the editor properly registers them with the blueprint actions, we need to unregister them when automated tests want them to unload. Add a ClearEditorReferences function to UBlueprint that calls the OnUnloaded editor delegate, so EngineTest doesn't need to include the editor module
Change 3764768 by Ben.Zeigler
#jira UE-52524 Fix null access crash when pasting an invalid macro instance node
Change 3766415 by Fred.Kimberley
Removing invalid assets. Most of these are out of date.
Change 3766417 by Fred.Kimberley
Add warnings when we try to export a package without a type.
Change 3766514 by Fred.Kimberley
Added a #include to fix the build.
Change 3766542 by Fred.Kimberley
Added a #include to fix the build.
Change 3766558 by Fred.Kimberley
Rename variables to avoid warnings about hiding previous variable declarations.
Change 3767619 by Marc.Audy
bActorIsBeingDestroyed must be part of transaction history
#jira UE-51796
Change 3767993 by Dan.Oconnor
Preserve graph editor controls when clicking on a hyper link, this speeds up navigation via the debugger 'step' command and Find in Blueprints control
#jira UE-52596
Change 3768146 by Marc.Audy
Fix material instance dynamic not correctly finding object in details panel customization as a result soft path changes
#jira UE-52488
Change 3769586 by Marc.Audy
Fix expose on spawn related error messages
Change 3769863 by Dan.Oconnor
Blueprint call stack now has access to frame offsets and can highlight nodes that are active on previous stack frames
#jira UE-52452
Change 3771200 by Dan.Oconnor
CIS fix - add missing DO_BLUEPRINT_GUARD
Change 3771555 by Ben.Zeigler
Add transactions for several pin class changing actions which were missing them
Change 3771589 by Ben.Zeigler
#jira UE-52665 Fix it so changing the type of a create widget or spawn actor node will correctly propagate the type change to reroute/wildcard nodes instead of disconnecting
Change 3771683 by Dan.Oconnor
Call Stack polish: background color no longer changes when undocked, prettify-ing "ExecuteUbergraph_blahblah" in to "Event Graph", resizing works correctly, added overlay message when no call stack is available
#jira UE-52567
Change 3771734 by Dan.Oconnor
Add entries for native code in the blueprint call stack view, clarifying re-entrancy
Change 3774293 by Ben.Zeigler
#jira UE-52665 Minimal fix for making sure type changes propagate through multiple rerout nodes, going to make a larger refactor in a second checkin
Change 3774328 by Ben.Zeigler
#jira UE-52665 Refactor knot nodes so there is one type propagation function that takes a direction, this fixes an issue where the second knot node in a chain would not have it's type changed when input type changed
Change 3774342 by Ben.Zeigler
#jira UE-52661 Fix crash when using blueprinted components created by a specialized subclass of UBlueprint, from PR #4249
Change 3774476 by Fred.Kimberley
Add class and function info to pin names for async nodes. This fixes a problem where redirectors for async node pins did not work.
https://udn.unrealengine.com/questions/402882/propertyredirect-fails-with-uk2node-latentgameplay.html?childToView=403808
Change 3774645 by Ben.Zeigler
#jira UE-41743 Fix it so struct split pins handle renames correctly, both for user structs and native structs
Refactor the variable rename checking in make/break struct to use the generic one I just added
Change 3775412 by Phillip.Kavan
UX improvements for Blueprint single-step debugging and breakpoints. Also added Step Out and Step Over debugging commands.
Change summary:
- Remapped the existing Step In command from F10 to F11 hotkey.
- Mapped existing Step Over command to F10 and existing Step Out command to ALT-SHIFT-F11 hotkeys.
- Added new (repurposed) icon assets for single-step debugging toolbar commands.
- Modified FPlayWorldCommands::BuildToolbar() to add new Step Over and Step Out commands to the toolbar.
- Modified FCompilerResultsLog::CalculateStableIdentifierForLatentActionManager() to remove special-case code for intermediate Tunnel Instance nodes, as these are now reverse-mapped through FullSourceBacktrackMap.
- Modified FKismetDebugUtilities::CheckBreakConditions() to more generally manage the current graph stack (i.e. not just for Blueprint Function graphs). Also fixed a bug where we had failed to reset the target graph stack depth after completing a Step Out/Over iteration.
- Modified FBlueprintDebugData::FindAllCodeLocationsFromSourceNode() to remove the additional iteration for the special Macro source node table (no longer required).
- Modified FBlueprintDebugData::RegisterNodeToCodeAssociation() to remove the Macro-specific parameters and the additional insertions into the special Macro tables (no longer required).
- Modified UK2Node_MathExpression::ValidateNodeDuringCompilation() to remove the special-case for Macro Instance source nodes, as Macro source nodes are now being mapped through the same table.
- Added FindMatchingTunnelInstanceNode() as a utility method for now in BlueprintConnectionDrawingPolicy.cpp in order to match up Macro/Composite graph source nodes with nested Tunnel Instance nodes at the current graph level. *** TODO: For 4.19 we probably should revert back to using a secondary table in the debug data to map Tunnel Instance node hierarchies to code offsets in order to result in a faster lookup time here. ***
- Modified FKismetConnectionDrawingPolicy::BuilldExecutionRoadmap() to replace the special-case for Macro Instance source nodes with a more general check for Tunnel Instance nodes that also handles Composite source nodes.
- Revised UK2Node_TunnelBoundary to strip out most of what was being used to support the profiler, while keeping its basic compiled goto behavior in order to still function as a NOP node.
- Added FKismetCompilerContext::SpawnIntermediateTunnelBoundaryNodes().
- Modified FKismetCompilerContext::ExpandTunnelsAndMacros() to no longer overwrite intermediate Macro source node mappings in the lookup table with the Macro Instance source node that triggered the Macro graph expansion. Also revised the TunnelNode case to spawn intermediate TunnelBoundary (NOP) nodes around Macro and Composite gateways; this allows breakpoints to hit on the Tunnel nodes around a source graph expansion.
- Modified FScriptBuilderBase::EmitInstrumentation() to remove special-case handling for Macro and Tunnel source nodes. These are now being mapped directly through the SourceBacktrackMap instead.
- Removed alternate breakpoint icon assets for Macro Instance and Composite nodes (no longer needed).
- Removed UK2Node::GetActiveBreakpointToolTipText() and its UK2Node_MacroInstance override (no longer required).
- Removed special case in SGraphNodeK2Base::GetOverlayBrushes() for Macro Instance and Composite nodes (no longer needed).
- Removed special-case mappings and interface methods for Tunnel nodes in FCompilerResultsLog (no longer required).
- Removed the LineNumberToMacroSourceNodeMap and LineNumberToMacroInstanceNodeMap members from the FDebuggingInfoForSingleFunction struct (no longer in use).
- Removed FBlueprintDebugData::FindMacroSourceNodeFromCodeLocation() and FindMacroInstanceNodesFromCodeLocation().
- Removed FKismetDebugUtilities::FindMacroSourceNodeForCodeLocation() (no longer in use).
- Removed special-case handling for Macro Instance nodes in FKismetDebugUtilities::OnScriptException() (no longer required). Macro source nodes are no longer being mapped to code offsets through a separate table, and we don't need to worry about saving/restoring the Active Object when debugging with a Macro Graph in the active tab.
#jira UE-2880
#jira UE-16817
Change 3776606 by mason.seay
Updated content to prevent warning from appearing
Change 3777051 by Dan.Oconnor
ComponentTemplate references in UBlueprint can no be cleared after compiling the (blueprint defined) component
#jira UE-52484
Change 3777108 by Dan.Oconnor
Look up call stack frame source name when caching a script call stack for display. This relies on debug data being generated for event stubs
#jira UE-52717, UE-52719
Change 3778277 by Marc.Audy
Fixed potential null material reference causing crash.
#jira UE-52803
Change 3778288 by Marc.Audy
PR #3957: Making FAlphaBlend BlueprintType in order to fix a bunch of broken UPROPERTY's as of 4.17 (Contributed by ill)
#jira UE-49082
Change 3778321 by Phillip.Kavan
Fix for a regression in BP script execution behavior related to misidentified latent node expansions from a macro source graph.
Change summary:
- Removed FCompilerResultsLog::FullSourceBacktrackMap (no longer in use).
- Restored FCompilerResultsLog::IntermediateTunnelNodeToTunnelInstanceMap (which was in place prior to CL# 37754112); this table was being used to map intermediate nodes resulting from a tunnel instance node expansion back to the outer tunnel instance node that triggered the expansion. Its once again being used for that reason, but I reduced the scope a bit to only include the execution path within the expansion, as that's the only mapping that we need.
- Restored FCompilerResultsLog::RegisterIntermediateTunnelNode(), but renamed it to NotifyIntermediateTunnelNode() to be consistent with the other parts of the MessageLog interface, and also removed the part of the implementation that was adding to a secondary macro expansion-to-source backtrack map (since macro expansion node lookup is now done through the main source backtrack map).
- Restored FCompilerResultsLog::GetIntermediateTunnelInstance().
- Modified FCompilerResultsLog::NotifyIntermediateObjectCreation() to remove the part of the implementation that was adding to the secondary node-only-to-source backtrack map (it was previously just a redundant copy of the main one except in the case of macro expansions).
- Modified FCompilerResultsLog::CalculateStableIdentifierForLatentActionManager() to restore the calculation of a stable UUID for nodes sourced from a macro expansion, where we had incorporated the outer intermediate tunnel instance node chain.
#jira UE-52872
Change 3778329 by Marc.Audy
PR #4241: Enforce calling superclass on ActorComponent::BeginPlay (Contributed by rlefebvre)
#jira UE-52574
Change 3778349 by Marc.Audy
Minor cleanup
Change 3759702 by Ben.Zeigler
#jira UE-52287 Prevent cook metadata like DevelopmentAssetRegistry.bin from being packed into a shipping game, by moving it into a Metadata subdirectory and updating deployment scripts to avoid that directory.
Right now it doesn't package them at all, we could change it to package them as Debug Non-UFS if desired
Change it so the asset audit UI will only load DevelopmentAssetRegistry.bin files, the cooked registry files don't have enough information any more to be useful
Remove ability for runtime game to load DevelopmentAssetRegistry.bin, this ended up not being useful
#jira UE-52158 Fix it to refresh the list of possible asset audit platforms when the refresh button is pushed
Change 3766414 by Fred.Kimberley
Data validation plugin
Change 3769923 by Ben.Zeigler
#jira UE-30347 Change ResourceSize mode enum from Inclusive to EstimatedTotal, which includes UObject serialization data as well as data for any subobjects. It now does NOT include externally referenced assets, which it did for some assets but not others
Fix Texture EstimatedTotal memory to handle LOD bias, it now reports the largest possible size in a cooked game of any platform
Fix many GetResourceSizeEx calls to match the new definition and improve accuracy
Switched several editor tools to use EstimatedTotal now that it is more useful, and removed some unused memory stats
Remove ResourceSize from UObject asset registry tags as it was misleading and inaccurate, for now it is only possible to get this for loaded objects
Remove MapFileSize from Worlds as it redundant with the generic file size. Fixed the generic file size to display using the Size format
Several UI fixes for Asset Audit and Size Map to deal with this change. Asset Audit no longer has the memory size columns, and the memory size drop down in Size Map is disabled for cooked builds
Change 3771365 by Ben.Zeigler
#jira UE-52670 Add project setting bValidateUnloadedSoftActorReferences that is true by default to match current behavior. If you set it to false it will no longer load packages to look for soft actor references when deleting/renaming actors.
[CL 3779057 by Marc Audy 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.Get() == OriginalNode.Get() )
|
|
{
|
|
return CopiedNode;
|
|
}
|
|
|
|
for( int32 ChildNodeIndex = 0; ChildNodeIndex < OriginalNode->Children.Num(); ++ChildNodeIndex )
|
|
{
|
|
const auto& OriginalChildNode = OriginalNode->Children[ ChildNodeIndex ];
|
|
const auto& CopiedChildNode = CopiedNode->Children[ ChildNodeIndex ];
|
|
|
|
const auto& FoundMatchingCopy = FindNodeInCopiedTree( NodeToFind, OriginalChildNode.ToSharedRef(), CopiedChildNode.ToSharedRef() );
|
|
if( FoundMatchingCopy.IsValid() )
|
|
{
|
|
return FoundMatchingCopy;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
|
|
void STreeMap::Undo()
|
|
{
|
|
if( UndoStates.Num() > 0 )
|
|
{
|
|
// Restore from undo state
|
|
int32 UndoStateIndex;
|
|
if( CurrentUndoLevel == INDEX_NONE )
|
|
{
|
|
// We haven't undone anything since the last time a new undo state was added
|
|
UndoStateIndex = UndoStates.Num() - 1;
|
|
|
|
// Store an undo state for the current state (before undo was pressed)
|
|
TakeUndoSnapshot();
|
|
}
|
|
else
|
|
{
|
|
// Move down to the next undo level
|
|
UndoStateIndex = CurrentUndoLevel - 1;
|
|
}
|
|
|
|
// Is there anything else to undo?
|
|
if( UndoStateIndex >= 0 )
|
|
{
|
|
// Undo from history
|
|
{
|
|
const FTreeMapNodeDataRef NewTreeMapNodeData = UndoStates[ UndoStateIndex ]->CopyNodeRecursively();
|
|
const FTreeMapNodeDataRef NewActiveRoot = FindNodeInCopiedTree( ActiveRootTreeMapNode.ToSharedRef(), TreeMapNodeData.ToSharedRef(), NewTreeMapNodeData ).ToSharedRef();
|
|
TreeMapNodeData = NewTreeMapNodeData;
|
|
|
|
const bool bShouldPlayTransition = true; // @todo treemap: A bit worried about volatility of LastTreeMap node refs here
|
|
SetTreeRoot( NewActiveRoot, bShouldPlayTransition );
|
|
}
|
|
|
|
CurrentUndoLevel = UndoStateIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void STreeMap::Redo()
|
|
{
|
|
// Is there anything to redo? If we've haven't tried to undo since the last time new
|
|
// undo state was added, then CurrentUndoLevel will be INDEX_NONE
|
|
if( CurrentUndoLevel != INDEX_NONE )
|
|
{
|
|
const int32 NextUndoLevel = CurrentUndoLevel + 1;
|
|
if( UndoStates.Num() > NextUndoLevel )
|
|
{
|
|
// Restore from undo state
|
|
{
|
|
const FTreeMapNodeDataRef NewTreeMapNodeData = UndoStates[ NextUndoLevel ]->CopyNodeRecursively();
|
|
const FTreeMapNodeDataRef NewActiveRoot = FindNodeInCopiedTree( ActiveRootTreeMapNode.ToSharedRef(), TreeMapNodeData.ToSharedRef(), NewTreeMapNodeData ).ToSharedRef();
|
|
TreeMapNodeData = NewTreeMapNodeData;
|
|
|
|
const bool bShouldPlayTransition = true; // @todo treemap: A bit worried about volatility of LastTreeMap node refs here
|
|
SetTreeRoot( NewActiveRoot, bShouldPlayTransition );
|
|
}
|
|
|
|
CurrentUndoLevel = NextUndoLevel;
|
|
|
|
if( UndoStates.Num() <= CurrentUndoLevel + 1 )
|
|
{
|
|
// We've redone all available undo states
|
|
CurrentUndoLevel = INDEX_NONE;
|
|
|
|
// Pop last undo state that we created on initial undo
|
|
UndoStates.RemoveAt( UndoStates.Num() - 1 );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void STreeMap::ReparentNode( const FTreeMapNodeDataRef DroppedNode, const FTreeMapNodeDataRef NewParentNode )
|
|
{
|
|
// Can't reparent the tree root node
|
|
if( DroppedNode != TreeMapNodeData )
|
|
{
|
|
struct Local
|
|
{
|
|
/** Checks to see if the specified Node is a child of 'PossibleParent' at any level */
|
|
static bool IsMyParent( const FTreeMapNodeDataRef& Node, const FTreeMapNodeDataRef& PossibleParent )
|
|
{
|
|
if( Node == PossibleParent )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if( PossibleParent->Parent != nullptr )
|
|
{
|
|
return IsMyParent( Node, PossibleParent->Parent->AsShared() );
|
|
}
|
|
|
|
return false;
|
|
}
|
|
};
|
|
|
|
// Can't parent owning nodes to their children
|
|
if( !Local::IsMyParent( DroppedNode, NewParentNode ) )
|
|
{
|
|
// Can't reparent to self
|
|
if( NewParentNode != DroppedNode )
|
|
{
|
|
// Only if parent has changed
|
|
if( &NewParentNode.Get() != DroppedNode->Parent )
|
|
{
|
|
// Take an Undo snapshot before we change anything
|
|
TakeUndoSnapshot();
|
|
|
|
if( DroppedNode->Parent != nullptr )
|
|
{
|
|
DroppedNode->Parent->Children.RemoveSingle( DroppedNode );
|
|
}
|
|
NewParentNode->Children.Add( DroppedNode );
|
|
DroppedNode->Parent = &NewParentNode.Get();
|
|
|
|
// If we container node became a leaf node, we need to make sure it has a valid size set
|
|
// @todo treemap: This seems a bit... weird. It won't reverse either, if you put the node back (save with overridden size!) Maybe we need a bool for "auto size"=true. OR, have a separate size for Node vs. Leaf?
|
|
if( DroppedNode->Size == 0.0f && DroppedNode->IsLeafNode() )
|
|
{
|
|
DroppedNode->Size = 1.0f;
|
|
}
|
|
|
|
// Invalidate the tree
|
|
const bool bShouldPlayTransition = true;
|
|
RebuildTreeMap( bShouldPlayTransition );
|
|
|
|
// After we have a new tree, play a highlight effect on the reparented node so the user can see where it ended
|
|
// up in the tree. Tree map layout can be fairly volatile after a parenting change.
|
|
HighlightPulseNode = DroppedNode;
|
|
HighlightPulseStartTime = FSlateApplication::Get().GetCurrentTime();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
FReply STreeMap::DeleteHoveredNode()
|
|
{
|
|
FReply Reply = FReply::Unhandled();
|
|
|
|
// Wait until we've finished animating a transition
|
|
if( !IsNavigationTransitionActive() )
|
|
{
|
|
if( MouseOverVisual != nullptr )
|
|
{
|
|
// Only non-root nodes can be deleted
|
|
auto NodeToDelete = MouseOverVisual->NodeData->AsShared();
|
|
if( NodeToDelete->Parent != nullptr )
|
|
{
|
|
// Don't allow the current active tree root to be deleted. The user should zoom out first!
|
|
if( NodeToDelete != ActiveRootTreeMapNode )
|
|
{
|
|
TakeUndoSnapshot();
|
|
|
|
// Delete the node
|
|
{
|
|
NodeToDelete->Parent->Children.RemoveSingle( NodeToDelete );
|
|
NodeToDelete->Parent = nullptr;
|
|
|
|
// Note: NodeToDelete will actually be deleted when it goes out of scope later in this function (shared pointer), but it is already removed from our tree
|
|
}
|
|
|
|
const bool bShouldPlayTransition = true;
|
|
RebuildTreeMap( bShouldPlayTransition );
|
|
|
|
Reply = FReply::Handled();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
|
|
|
|
FReply STreeMap::InsertNewNodeAsChildOfHoveredNode( const FGeometry& MyGeometry )
|
|
{
|
|
FReply Reply = FReply::Unhandled();
|
|
|
|
// Wait until we've finished animating a transition
|
|
if( !IsNavigationTransitionActive() )
|
|
{
|
|
if( MouseOverVisual != nullptr )
|
|
{
|
|
auto ParentNode = MouseOverVisual->NodeData->AsShared();
|
|
|
|
// Allocate the new node but don't add it to the tree yet. We want the user to give the node a name first!
|
|
|
|
// NOTE: Ownership of this node will be transferred to StartRenamingNode, where it will be referenced
|
|
// in the delegate callback for the editable text change commit handler. If the user opts to not type anything, the
|
|
// node will be deleted instead of being added to the tree.
|
|
FTreeMapNodeDataRef NewNode( new FTreeMapNodeData() );
|
|
NewNode->Size = 1.0f; // Leaf nodes always get a size of 1.0 for now
|
|
NewNode->Parent = MouseOverVisual->NodeData;
|
|
|
|
// Start editing the new node before we insert it
|
|
const bool bIsNewNode = true;
|
|
StartRenamingNode( MyGeometry, NewNode, MouseOverVisual->Position, bIsNewNode );
|
|
|
|
Reply = FReply::Handled();
|
|
}
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
|
|
|
|
bool STreeMap::SupportsKeyboardFocus() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
|
|
FReply STreeMap::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyboardEvent )
|
|
{
|
|
FReply Reply = FReply::Unhandled();
|
|
|
|
const FKey Key = InKeyboardEvent.GetKey();
|
|
|
|
if( AllowEditing.Get() )
|
|
{
|
|
if( Key == EKeys::Delete )
|
|
{
|
|
// If the cursor is over a node, delete it!
|
|
DeleteHoveredNode();
|
|
Reply = FReply::Handled();
|
|
}
|
|
else if( Key == EKeys::Insert )
|
|
{
|
|
// If the cursor is over a node, insert a new node as a child!
|
|
InsertNewNodeAsChildOfHoveredNode( MyGeometry );
|
|
Reply = FReply::Handled();
|
|
}
|
|
else if( Key == EKeys::Z && InKeyboardEvent.IsControlDown() )
|
|
{
|
|
// Undo
|
|
Undo();
|
|
Reply = FReply::Handled();
|
|
}
|
|
else if( Key == EKeys::Y && InKeyboardEvent.IsControlDown() )
|
|
{
|
|
// Redo
|
|
Redo();
|
|
Reply = FReply::Handled();
|
|
}
|
|
}
|
|
|
|
return Reply;
|
|
}
|
|
|
|
|
|
void STreeMap::RenamingNode_OnTextCommitted( const FText& NewText, ETextCommit::Type, TSharedRef<FTreeMapNodeData> NodeToRename )
|
|
{
|
|
TSharedPtr<SWidget> RenamingNodeWidgetPinned( RenamingNodeWidget.Pin() );
|
|
if( RenamingNodeWidgetPinned.IsValid() )
|
|
{
|
|
// Kill the window after the text has been committed
|
|
TSharedRef<SWindow> ParentWindow = FSlateApplication::Get().FindWidgetWindow( RenamingNodeWidgetPinned.ToSharedRef() ).ToSharedRef();
|
|
|
|
RenamingNodeWidget.Reset(); // Avoid reentrancy
|
|
RenamingNodeData.Reset();
|
|
FSlateApplication::Get().RequestDestroyWindow( ParentWindow );
|
|
|
|
|
|
// Make sure new name is OK
|
|
if( NewText.ToString() != NodeToRename->Name &&
|
|
!NewText.IsEmpty() )
|
|
{
|
|
// Store undo state
|
|
TakeUndoSnapshot();
|
|
|
|
// Rename it!
|
|
NodeToRename->Name = NewText.ToString();
|
|
|
|
if( bIsNamingNewNode )
|
|
{
|
|
// Add the new node to the tree
|
|
TakeUndoSnapshot();
|
|
|
|
// Insert the new node
|
|
{
|
|
auto ParentNode = NodeToRename->Parent->AsShared();
|
|
ParentNode->Children.Add( NodeToRename );
|
|
}
|
|
|
|
const bool bShouldPlayTransition = true;
|
|
RebuildTreeMap( bShouldPlayTransition );
|
|
}
|
|
else
|
|
{
|
|
// NOTE: No refresh needed, as node labels are pulled directly from nodes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void STreeMap::StartRenamingNode( const FGeometry& MyGeometry, const FTreeMapNodeDataRef& NodeData, const FVector2D& RelativePosition, const bool bIsNewNode )
|
|
{
|
|
TSharedRef<SBorder> RenamerWidget = SNew( SBorder );
|
|
|
|
TSharedRef<SEditableTextBox> EditableText =
|
|
SNew( SEditableTextBox )
|
|
.Text( FText::FromString( NodeData->Name) )
|
|
.SelectAllTextWhenFocused( true )
|
|
.RevertTextOnEscape( true )
|
|
.MinDesiredWidth( 100 )
|
|
.OnTextCommitted( this, &STreeMap::RenamingNode_OnTextCommitted, NodeData->AsShared() )
|
|
;
|
|
RenamerWidget->SetContent( EditableText );
|
|
|
|
RenamingNodeWidget = RenamerWidget;
|
|
RenamingNodeData = NodeData;
|
|
bIsNamingNewNode = bIsNewNode;
|
|
|
|
const bool bFocusImmediately = true;
|
|
FSlateApplication::Get().PushMenu( AsShared(), FWidgetPath(), RenamerWidget, MyGeometry.LocalToAbsolute( RelativePosition ), FPopupTransitionEffect::None, bFocusImmediately );
|
|
|
|
// Focus the text box right after we spawn it so that the user can start typing
|
|
FSlateApplication::Get().SetKeyboardFocus( EditableText, EFocusCause::SetDirectly );
|
|
}
|
|
|
|
|
|
|
|
void STreeMap::StopRenamingNode()
|
|
{
|
|
TSharedPtr<SWidget> RenamingNodeWidgetPinned( RenamingNodeWidget.Pin() );
|
|
if( RenamingNodeWidgetPinned.IsValid() )
|
|
{
|
|
// Kill the window after the text has been committed
|
|
TSharedRef<SWindow> ParentWindow = FSlateApplication::Get().FindWidgetWindow( RenamingNodeWidgetPinned.ToSharedRef() ).ToSharedRef();
|
|
FSlateApplication::Get().RequestDestroyWindow( ParentWindow );
|
|
}
|
|
}
|
|
|
|
|
|
void STreeMap::ApplyVisualizationToNodes( const FTreeMapNodeDataRef& Node )
|
|
{
|
|
const FLinearColor RootDefaultColor = FLinearColor( 0.125f, 0.125f, 0.125f ); // @todo treemap custom: Make configurable in the STreeMap settings?
|
|
int32 TreeDepth = 0;
|
|
ApplyVisualizationToNodesRecursively( Node, RootDefaultColor, TreeDepth );
|
|
}
|
|
|
|
|
|
void STreeMap::ApplyVisualizationToNodesRecursively( const FTreeMapNodeDataRef& Node, const FLinearColor& DefaultColor, const int32 TreeDepth )
|
|
{
|
|
// Select default color saturation based on tree depth
|
|
FLinearColor MyDefaultColor = DefaultColor;
|
|
auto HSV = MyDefaultColor.LinearRGBToHSV();
|
|
const float SaturationReductionPerDepthLevel = 0.05f; // @todo treemap custom: Make configurable in the tree map settings? Calculate tree depth?
|
|
const float MinAllowedSaturation = 0.1f;
|
|
HSV.G = FMath::Max( MinAllowedSaturation, HSV.G - TreeDepth * SaturationReductionPerDepthLevel );
|
|
MyDefaultColor = HSV.HSVToLinearRGB();
|
|
|
|
|
|
// Size
|
|
{
|
|
if( SizeNodesByAttribute.IsValid() )
|
|
{
|
|
FTreeMapAttributeDataPtr* AttributeDataPtr = Node->Attributes.Find( SizeNodesByAttribute->Name );
|
|
if( AttributeDataPtr != nullptr )
|
|
{
|
|
const FTreeMapAttributeData& AttributeData = *AttributeDataPtr->Get();
|
|
|
|
// Make sure the data value is in range
|
|
if( ensure( SizeNodesByAttribute->Values.Contains( AttributeData.Value ) ) )
|
|
{
|
|
// @todo treemap: This doesn't work well with container nodes. Our range of sizes doesn't go big enough to compete with cumulative sizes!
|
|
Node->Size = SizeNodesByAttribute->Values[ AttributeData.Value ]->NodeSize;
|
|
}
|
|
else
|
|
{
|
|
// Invalid attribute data!
|
|
Node->Size = Node->IsLeafNode() ? SizeNodesByAttribute->DefaultValue->NodeSize : 0.0f; // Leaf nodes get a default size, but container nodes use auto-sizing
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The node doesn't have this attribute on it. Use default.
|
|
Node->Size = Node->IsLeafNode() ? SizeNodesByAttribute->DefaultValue->NodeSize : 0.0f; // Leaf nodes get a default size, but container nodes use auto-sizing
|
|
}
|
|
}
|
|
|
|
else
|
|
{
|
|
// @todo treemap: Really we want this to "restore the original size set by the user", not make up new defaults. Disabled for now.
|
|
|
|
// Default size
|
|
// Node->Size = Node->IsLeafNode() ? 1.0f : 0.0f; // Leaf nodes get a default size, but container nodes use auto-sizing
|
|
}
|
|
}
|
|
|
|
|
|
// Color
|
|
{
|
|
if( ColorNodesByAttribute.IsValid() )
|
|
{
|
|
FTreeMapAttributeDataPtr* AttributeDataPtr = Node->Attributes.Find( ColorNodesByAttribute->Name );
|
|
if( AttributeDataPtr != nullptr )
|
|
{
|
|
const FTreeMapAttributeData& AttributeData = *AttributeDataPtr->Get();
|
|
|
|
// Make sure the data value is in range
|
|
if( ensure( ColorNodesByAttribute->Values.Contains( AttributeData.Value ) ) )
|
|
{
|
|
Node->Color = ColorNodesByAttribute->Values[ AttributeData.Value ]->NodeColor;
|
|
}
|
|
else
|
|
{
|
|
// Invalid attribute data!
|
|
Node->Color = ColorNodesByAttribute->DefaultValue->NodeColor;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The node doesn't have this attribute on it. Use default.
|
|
Node->Color = ColorNodesByAttribute->DefaultValue->NodeColor;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Default color
|
|
Node->Color = MyDefaultColor;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
for( int32 ChildIndex = 0; ChildIndex < Node->Children.Num(); ++ChildIndex )
|
|
{
|
|
const auto& ChildNode = Node->Children[ ChildIndex ];
|
|
|
|
// Make up a distinct color for all of the root's top level nodes
|
|
FLinearColor ChildColor = MyDefaultColor;
|
|
if( TreeDepth == 0 )
|
|
{
|
|
// Choose a hue evenly spaced across the spectrum
|
|
float ColorHue = 360.0f * (float)( ChildIndex + 1 ) / (float)Node->Children.Num();
|
|
auto ChildColorHSV = FLinearColor::White;
|
|
ChildColorHSV.R = ColorHue;
|
|
ChildColorHSV.G = 1.0f; // Full saturation!
|
|
ChildColor = ChildColorHSV.HSVToLinearRGB();
|
|
}
|
|
|
|
ApplyVisualizationToNodesRecursively( ChildNode.ToSharedRef(), ChildColor, TreeDepth + 1 );
|
|
}
|
|
}
|
|
|
|
void STreeMap::ShowOptionsMenuAt(const FPointerEvent& InMouseEvent)
|
|
{
|
|
FWidgetPath WidgetPath = InMouseEvent.GetEventPath() != nullptr ? *InMouseEvent.GetEventPath() : FWidgetPath();
|
|
const FVector2D& ScreenSpacePosition = InMouseEvent.GetScreenSpacePosition();
|
|
|
|
ShowOptionsMenuAtInternal(ScreenSpacePosition, WidgetPath);
|
|
}
|
|
|
|
void STreeMap::ShowOptionsMenuAtInternal(const FVector2D& ScreenSpacePosition, const FWidgetPath& WidgetPath)
|
|
{
|
|
struct Local
|
|
{
|
|
static void MakeEditNodeAttributeMenu( FMenuBuilder& MenuBuilder, FTreeMapNodeDataPtr EditingNode, TSharedPtr< FTreeMapAttribute > Attribute, STreeMap* Self )
|
|
{
|
|
MenuBuilder.AddMenuEntry(
|
|
LOCTEXT( "RemoveAttribute", "Not Set" ),
|
|
FText(), // No tooltip (intentional)
|
|
FSlateIcon(), // Icon
|
|
FUIAction(
|
|
FExecuteAction::CreateStatic( &Local::EditNodeAttribute_Execute, EditingNode, Attribute, TSharedPtr< FTreeMapAttributeValue >(), Self ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateStatic( &Local::EditNodeAttribute_IsChecked, EditingNode, Attribute, TSharedPtr< FTreeMapAttributeValue >(), Self ) ),
|
|
NAME_None, // Extension point
|
|
EUserInterfaceActionType::ToggleButton );
|
|
|
|
MenuBuilder.AddMenuSeparator();
|
|
|
|
// @todo treemap: These probably should be sorted rather than hash order
|
|
for( auto HashIter( Attribute->Values.CreateConstIterator() ); HashIter; ++HashIter )
|
|
{
|
|
FTreeMapAttributeValuePtr Value = HashIter.Value();
|
|
|
|
MenuBuilder.AddMenuEntry(
|
|
FText::FromName( Value->Name ),
|
|
FText(), // No tooltip (intentional)
|
|
FSlateIcon(), // Icon
|
|
FUIAction(
|
|
FExecuteAction::CreateStatic( &Local::EditNodeAttribute_Execute, EditingNode, Attribute, Value, Self ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateStatic( &Local::EditNodeAttribute_IsChecked, EditingNode, Attribute, Value, Self ) ),
|
|
NAME_None, // Extension point
|
|
EUserInterfaceActionType::ToggleButton );
|
|
}
|
|
}
|
|
|
|
|
|
static void EditNodeAttribute_Execute( FTreeMapNodeDataPtr EditingNode, TSharedPtr< FTreeMapAttribute > Attribute, TSharedPtr< FTreeMapAttributeValue > Value, STreeMap* Self )
|
|
{
|
|
bool bAnyChanges = false;
|
|
if( Value.IsValid() )
|
|
{
|
|
if( !EditingNode->Attributes.Contains( Attribute->Name ) )
|
|
{
|
|
EditingNode->Attributes.Add( Attribute->Name, MakeShareable( new FTreeMapAttributeData( Value->Name ) ) );
|
|
bAnyChanges = true;
|
|
}
|
|
else if( EditingNode->Attributes[ Attribute->Name ]->Value != Value->Name )
|
|
{
|
|
EditingNode->Attributes[ Attribute->Name ]->Value = Value->Name;
|
|
bAnyChanges = true;
|
|
}
|
|
else
|
|
{
|
|
// Nothing changed
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Clearing attribute
|
|
if( EditingNode->Attributes.Contains( Attribute->Name ) )
|
|
{
|
|
EditingNode->Attributes.Remove( Attribute->Name );
|
|
bAnyChanges = true;
|
|
}
|
|
else
|
|
{
|
|
// Nothing changed
|
|
}
|
|
}
|
|
|
|
// Has anything changed?
|
|
if( bAnyChanges )
|
|
{
|
|
const bool bShouldPlayTransition = true;
|
|
Self->RebuildTreeMap( bShouldPlayTransition );
|
|
}
|
|
}
|
|
|
|
|
|
static bool EditNodeAttribute_IsChecked( FTreeMapNodeDataPtr EditingNode, TSharedPtr< FTreeMapAttribute > Attribute, TSharedPtr< FTreeMapAttributeValue > Value, STreeMap* Self )
|
|
{
|
|
if( Value.IsValid() )
|
|
{
|
|
return EditingNode->Attributes.Contains( Attribute->Name ) && EditingNode->Attributes[ Attribute->Name ]->Value == Value->Name;
|
|
}
|
|
else
|
|
{
|
|
return !EditingNode->Attributes.Contains( Attribute->Name );
|
|
}
|
|
}
|
|
|
|
|
|
static void SizeByAttribute_Execute( TSharedPtr< FTreeMapAttribute > Attribute, STreeMap* Self )
|
|
{
|
|
// Has anything changed?
|
|
if( Self->SizeNodesByAttribute != Attribute )
|
|
{
|
|
// Apply the new visualization!
|
|
Self->SizeNodesByAttribute = Attribute;
|
|
|
|
const bool bShouldPlayTransition = true;
|
|
Self->RebuildTreeMap( bShouldPlayTransition );
|
|
}
|
|
}
|
|
|
|
|
|
static bool SizeByAttribute_IsChecked( TSharedPtr< FTreeMapAttribute > Attribute, STreeMap* Self )
|
|
{
|
|
return Self->SizeNodesByAttribute == Attribute;
|
|
}
|
|
|
|
|
|
static void MakeSizeByAttributeMenu( FMenuBuilder& MenuBuilder, STreeMap* Self )
|
|
{
|
|
MenuBuilder.AddMenuEntry(
|
|
LOCTEXT( "NoSizeByAttribute", "Off" ),
|
|
FText(), // No tooltip (intentional)
|
|
FSlateIcon(), // Icon
|
|
FUIAction(
|
|
FExecuteAction::CreateStatic( &Local::SizeByAttribute_Execute, TSharedPtr< FTreeMapAttribute >(), Self ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateStatic( &Local::SizeByAttribute_IsChecked, TSharedPtr< FTreeMapAttribute >(), Self ) ),
|
|
NAME_None, // Extension point
|
|
EUserInterfaceActionType::ToggleButton );
|
|
|
|
if( Self->Customization.IsValid() )
|
|
{
|
|
MenuBuilder.AddMenuSeparator();
|
|
|
|
// @todo treemap: These probably should be sorted rather than hash order
|
|
for( auto HashIter( Self->Customization->GetAttributes().CreateConstIterator() ); HashIter; ++HashIter )
|
|
{
|
|
FTreeMapAttributePtr Attribute = HashIter.Value();
|
|
|
|
MenuBuilder.AddMenuEntry(
|
|
FText::FromName( Attribute->Name ),
|
|
FText(), // No tooltip (intentional)
|
|
FSlateIcon(), // Icon
|
|
FUIAction(
|
|
FExecuteAction::CreateStatic( &Local::SizeByAttribute_Execute, Attribute, Self ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateStatic( &Local::SizeByAttribute_IsChecked, Attribute, Self ) ),
|
|
NAME_None, // Extension point
|
|
EUserInterfaceActionType::ToggleButton );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void ColorByAttribute_Execute( TSharedPtr< FTreeMapAttribute > Attribute, STreeMap* Self )
|
|
{
|
|
// Has anything changed?
|
|
if( Self->ColorNodesByAttribute != Attribute )
|
|
{
|
|
// Apply the new visualization!
|
|
Self->ColorNodesByAttribute = Attribute;
|
|
|
|
const bool bShouldPlayTransition = true;
|
|
Self->RebuildTreeMap( bShouldPlayTransition );
|
|
}
|
|
}
|
|
|
|
|
|
static bool ColorByAttribute_IsChecked( TSharedPtr< FTreeMapAttribute > Attribute, STreeMap* Self )
|
|
{
|
|
return Self->ColorNodesByAttribute == Attribute;
|
|
}
|
|
|
|
|
|
static void MakeColorByAttributeMenu( FMenuBuilder& MenuBuilder, STreeMap* Self )
|
|
{
|
|
MenuBuilder.AddMenuEntry(
|
|
LOCTEXT( "NoColorByAttribute", "Off" ),
|
|
FText(), // No tooltip (intentional)
|
|
FSlateIcon(), // Icon
|
|
FUIAction(
|
|
FExecuteAction::CreateStatic( &Local::ColorByAttribute_Execute, TSharedPtr< FTreeMapAttribute >(), Self ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateStatic( &Local::ColorByAttribute_IsChecked, TSharedPtr< FTreeMapAttribute >(), Self ) ),
|
|
NAME_None, // Extension point
|
|
EUserInterfaceActionType::ToggleButton );
|
|
|
|
if( Self->Customization.IsValid() )
|
|
{
|
|
MenuBuilder.AddMenuSeparator();
|
|
|
|
// @todo treemap: These probably should be sorted rather than hash order
|
|
for( auto HashIter( Self->Customization->GetAttributes().CreateConstIterator() ); HashIter; ++HashIter )
|
|
{
|
|
FTreeMapAttributePtr Attribute = HashIter.Value();
|
|
|
|
MenuBuilder.AddMenuEntry(
|
|
FText::FromName( Attribute->Name ),
|
|
FText(), // No tooltip (intentional)
|
|
FSlateIcon(), // Icon
|
|
FUIAction(
|
|
FExecuteAction::CreateStatic( &Local::ColorByAttribute_Execute, Attribute, Self ),
|
|
FCanExecuteAction(),
|
|
FIsActionChecked::CreateStatic( &Local::ColorByAttribute_IsChecked, Attribute, Self ) ),
|
|
NAME_None, // Extension point
|
|
EUserInterfaceActionType::ToggleButton );
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
// Only present a context menu if the tree has been customized
|
|
if( Customization.IsValid() )
|
|
{
|
|
const bool bShouldCloseMenuAfterSelection = true;
|
|
FMenuBuilder OptionsMenuBuilder( bShouldCloseMenuAfterSelection, nullptr );
|
|
|
|
if( AllowEditing.Get() && Customization.IsValid() && MouseOverVisual != nullptr )
|
|
{
|
|
// Node editing options
|
|
OptionsMenuBuilder.BeginSection( NAME_None, LOCTEXT( "EditNodeAttributesSection", "Edit Node" ) );
|
|
{
|
|
const auto& EditingNode = MouseOverVisual->NodeData->AsShared();
|
|
|
|
for( auto HashIter( Customization->GetAttributes().CreateConstIterator() ); HashIter; ++HashIter )
|
|
{
|
|
FTreeMapAttributePtr Attribute = HashIter.Value();
|
|
|
|
OptionsMenuBuilder.AddSubMenu(
|
|
FText::FromName( Attribute->Name ),
|
|
LOCTEXT( "EditAttribute_ToolTip", "Edits this attribute on the node under the curosr." ),
|
|
FNewMenuDelegate::CreateStatic( &Local::MakeEditNodeAttributeMenu, FTreeMapNodeDataPtr( EditingNode ), Attribute, this ) );
|
|
}
|
|
}
|
|
OptionsMenuBuilder.EndSection();
|
|
}
|
|
|
|
OptionsMenuBuilder.BeginSection( NAME_None, LOCTEXT( "ChangeLayoutSection", "Layout" ) );
|
|
{
|
|
OptionsMenuBuilder.AddSubMenu( LOCTEXT( "SizeBy", "Size by" ), LOCTEXT( "SizeBy_ToolTip", "Sets which criteria to base the size of the nodes in the tree map on." ), FNewMenuDelegate::CreateStatic( &Local::MakeSizeByAttributeMenu, this ) );
|
|
OptionsMenuBuilder.AddSubMenu( LOCTEXT( "ColorBy", "Color by" ), LOCTEXT( "ColorBy_ToolTip", "Sets which criteria to base the color of the nodes in the tree map on." ), FNewMenuDelegate::CreateStatic( &Local::MakeColorByAttributeMenu, this ) );
|
|
}
|
|
OptionsMenuBuilder.EndSection();
|
|
|
|
TSharedRef< SWidget > WindowContent =
|
|
SNew( SBorder )
|
|
[
|
|
OptionsMenuBuilder.MakeWidget()
|
|
];
|
|
|
|
const bool bFocusImmediately = false;
|
|
FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, WindowContent, ScreenSpacePosition, FPopupTransitionEffect::ContextMenu, bFocusImmediately);
|
|
}
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|