Files
UnrealEngine/Engine/Source/Runtime/InteractiveToolsFramework/Public/InteractiveTool.h
2025-05-18 13:04:45 +08:00

641 lines
23 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Containers/Array.h"
#include "Containers/Map.h"
#include "Containers/UnrealString.h"
#include "CoreMinimal.h"
#include "Delegates/Delegate.h"
#include "HAL/Platform.h"
#include "InputBehaviorSet.h"
#include "InteractiveToolActionSet.h"
#include "Internationalization/Text.h"
#include "Misc/AssertionMacros.h"
#include "Misc/Optional.h"
#include "Shader.h"
#include "Templates/EnableIf.h"
#include "Templates/Function.h"
#include "Templates/Models.h"
#include "Templates/UniquePtr.h"
#include "Templates/UnrealTemplate.h"
#include "ToolContextInterfaces.h"
#include "UObject/Class.h"
#include "UObject/Object.h"
#include "UObject/ObjectMacros.h"
#include "UObject/ObjectPtr.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
#include "InteractiveTool.generated.h"
class FCanvas;
class FInteractiveToolActionSet;
class FProperty;
class IToolsContextRenderAPI;
class UInputBehavior;
class UInteractiveTool;
class UInteractiveToolManager;
struct FPropertyChangedEvent;
/** Passed to UInteractiveTool::Shutdown to indicate how Tool should shut itself down*/
UENUM(BlueprintType)
enum class EToolShutdownType : uint8
{
/** Tool cleans up and exits. Pass this to tools that do not have Accept/Cancel options. */
Completed = 0,
/** Tool commits current preview to scene */
Accept = 1,
/** Tool discards current preview without modifying scene */
Cancel = 2
};
/**
* FInteractiveToolInfo provides information about a tool (name, tooltip, etc)
*/
struct FInteractiveToolInfo
{
/** Name of Tool. May be FText::Empty(), but will default to Tool->GetClass()->GetDisplayNameText() in InteractiveTool constructor */
FText ToolDisplayName = FText::GetEmpty();
};
class FWatchablePropertySet
{
public:
//
// Property watching infrastructure
//
class FPropertyWatcher
{
public:
virtual ~FPropertyWatcher() = default;
virtual void CheckAndUpdate() = 0;
virtual void SilentUpdate() = 0;
};
template <typename PropType>
class TPropertyWatcher : public FPropertyWatcher
{
public:
using FValueGetter = TFunction<PropType(void)>;
using FChangedCallback = TFunction<void(const PropType&)>;
using FNotEqualTestFunction = TFunction<bool(const PropType&, const PropType&)>; // Define "!=" for PropType
/**
* Describes a type having a "!=" comparasion operator. (Note: it would probably be cleaner to require operator==,
* however some types have operator!= but no operator==, and we do only use != in the code.)
*/
struct CInequalityComparable
{
template <typename T>
auto Requires(bool& Result, const T& A, const T& B) -> decltype(
Result = A != B
);
};
// If PropType is CInequalityComparable, allow the two-argument constructor and default to using the type's
// existing != operator.
// If PropType is not CInequalityComparable, the caller must use the three-argument constructor to provide a
// "not equal" function.
// Two-argument constructor, enabled only if PropType is CInequalityComparable
template<typename Q = PropType, typename = typename TEnableIf<TModels_V<CInequalityComparable, Q>>::Type>
TPropertyWatcher(const PropType& Property,
FChangedCallback OnChangedIn)
: Cached(),
GetValue([&Property]() {return Property; }),
OnChanged(MoveTemp(OnChangedIn)),
NotEqual([](const PropType& A, const PropType& B) { return A != B; })
{}
// Two-argument constructor, enabled only if PropType is CInequalityComparable
template<typename Q = PropType, typename = typename TEnableIf<TModels_V<CInequalityComparable, Q>>::Type>
TPropertyWatcher(FValueGetter GetValueIn,
FChangedCallback OnChangedIn)
: Cached(),
GetValue(MoveTemp(GetValueIn)),
OnChanged(MoveTemp(OnChangedIn)),
NotEqual([](const PropType& A, const PropType& B) { return A != B; })
{}
TPropertyWatcher(const PropType& Property,
FChangedCallback OnChangedIn,
FNotEqualTestFunction NotEqualIn)
: Cached(),
GetValue([&Property]() {return Property; }),
OnChanged(MoveTemp(OnChangedIn)),
NotEqual(MoveTemp(NotEqualIn))
{}
TPropertyWatcher(FValueGetter GetValueIn,
FChangedCallback OnChangedIn,
FNotEqualTestFunction NotEqualIn)
: Cached(),
GetValue(MoveTemp(GetValueIn)),
OnChanged(MoveTemp(OnChangedIn)),
NotEqual(MoveTemp(NotEqualIn))
{}
void CheckAndUpdate() final
{
PropType Value = GetValue();
if ((!Cached.IsSet()) || NotEqual(Cached.GetValue(), Value))
{
Cached = Value;
OnChanged(Cached.GetValue());
}
}
void SilentUpdate() final
{
Cached = GetValue();
}
private:
TOptional<PropType> Cached;
FValueGetter GetValue;
FChangedCallback OnChanged;
FNotEqualTestFunction NotEqual;
};
FWatchablePropertySet() = default;
FWatchablePropertySet(const FWatchablePropertySet&) = delete;
FWatchablePropertySet& operator=(const FWatchablePropertySet&) = delete;
void CheckAndUpdateWatched()
{
for ( auto& PropWatcher : PropertyWatchers )
{
PropWatcher->CheckAndUpdate();
}
}
void SilentUpdateWatched()
{
for ( auto& PropWatcher : PropertyWatchers )
{
PropWatcher->SilentUpdate();
}
}
/**
* Silently updates just a single watcher, using an index gotten from WatchProperty.
* Useful when you want watching to still work for other properties changed in the
* same tick.
*/
void SilentUpdateWatcherAtIndex(int32 i)
{
check(i >= 0 && i < PropertyWatchers.Num());
PropertyWatchers[i]->SilentUpdate();
}
/** @return Index of the watcher, which can be used in SilentUpdateWatcherAtIndex */
template <typename PropType>
int32 WatchProperty(const PropType& ValueIn,
typename TPropertyWatcher<PropType>::FChangedCallback OnChangedIn)
{
return PropertyWatchers.Emplace(MakeUnique<TPropertyWatcher<PropType>>(ValueIn, OnChangedIn));
}
/** @return Index of the watcher, which can be used in SilentUpdateWatcherAtIndex */
template <typename PropType>
int32 WatchProperty(typename TPropertyWatcher<PropType>::FValueGetter GetValueIn,
typename TPropertyWatcher<PropType>::FChangedCallback OnChangedIn)
{
return PropertyWatchers.Emplace(MakeUnique<TPropertyWatcher<PropType>>(GetValueIn, OnChangedIn));
}
/** @return Index of the watcher, which can be used in SilentUpdateWatcherAtIndex */
template <typename PropType>
int32 WatchProperty(const PropType& ValueIn,
typename TPropertyWatcher<PropType>::FChangedCallback OnChangedIn,
typename TPropertyWatcher<PropType>::FNotEqualTestFunction NotEqualsIn)
{
return PropertyWatchers.Emplace(MakeUnique<TPropertyWatcher<PropType>>(ValueIn, OnChangedIn, NotEqualsIn));
}
/** @return Index of the watcher, which can be used in SilentUpdateWatcherAtIndex */
template <typename PropType>
int32 WatchProperty(typename TPropertyWatcher<PropType>::FValueGetter GetValueIn,
typename TPropertyWatcher<PropType>::FChangedCallback OnChangedIn,
typename TPropertyWatcher<PropType>::FNotEqualTestFunction NotEqualsIn)
{
return PropertyWatchers.Emplace(MakeUnique<TPropertyWatcher<PropType>>(GetValueIn, OnChangedIn, NotEqualsIn));
}
private:
TArray<TUniquePtr<FPropertyWatcher>> PropertyWatchers;
};
/** This delegate is used by UInteractiveToolPropertySet */
DECLARE_MULTICAST_DELEGATE_TwoParams(FInteractiveToolPropertySetModifiedSignature, UObject*, FProperty*);
/**
* A UInteractiveTool contains a set of UObjects that contain "properties" of the Tool, ie
* the configuration flags, parameters, etc that control the Tool. Currently any UObject
* can be added as a property set, however there is no automatic mechanism for those child
* UObjects to notify the Tool when a property changes.
*
* If you make your property set UObjects subclasses of UInteractiveToolPropertySet, then
* when the Tool Properties are changed *in the Editor*, the parent Tool will be automatically notified.
* You can override UInteractiveTool::OnPropertyModified() to act on these notifications
*/
UCLASS(Transient, MinimalAPI)
class UInteractiveToolPropertySet : public UObject, public FWatchablePropertySet
{
GENERATED_BODY()
public:
/** @return the multicast delegate that is called when properties are modified */
FInteractiveToolPropertySetModifiedSignature& GetOnModified()
{
return OnModified;
}
/** Return true if this property set is enabled. Enabled/Disable state is intended to be used to control things like visibility in UI/etc. */
bool IsPropertySetEnabled() const
{
return bIsPropertySetEnabled;
}
//
// Setting saving/serialization
//
/**
* Save and restore values of current Tool Properties between tool invocations. The standard usage of
* this setup is to call PropertySet->RestoreProperties() in the UInteractiveTool::Setup() implementation,
* and PropertySet->SaveProperties() in the UInteractiveTool::Shutdown() implementation.
*
* The default behaviour of these functions is to Save or Restore every property in the property set. It is not
* necessary to save/restore all possible Properties (in many cases this would not make sense), so individual
* properties may be skipped by adding the "TransientToolProperty" tag to their metadata on a property by property basis.
*
* Property sets which need more exotic behaviour upon Save and Restore may override these routines
*
* GetDynamicPropertyCache() can be used to return an instance of the specified property set subclass which
* may be used as a place to save/restore these properties by customized Save/Restore functions
*
* Note: the current design of this system assumes that the CDO will keep the referenced objects alive.
* This assumption is incorrect in Runtime builds, and some external mechanism must be used to keep
* the elements in the CachedPropertiesMap alive.
*/
/**
* Save the values of this PropertySet with the given CacheIdentifier.
* The Tool parameter is currently ignored.
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void SaveProperties(UInteractiveTool* SaveFromTool, const FString& CacheIdentifier = TEXT(""));
/**
* Restore the values of the Property Set with the given CacheIdentifier.
* The Tool parameter is currently ignored.
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void RestoreProperties(UInteractiveTool* RestoreToTool, const FString& CacheIdentifier = TEXT(""));
protected:
// Utility func used to implement the default Save/RestoreProperties funcs
INTERACTIVETOOLSFRAMEWORK_API virtual void SaveRestoreProperties(UInteractiveTool* RestoreToTool, const FString& CacheIdentifier, bool bSaving);
/**
* GetDynamicPropertyCache return class-internal objects that subclasses can use to save/restore properties.
* @param CacheIdentifier multiple versions of the UInteractiveToolPropertySet can be stored, CacheIdentifier indicates which one to use
* @param bWasCreatedOut true is returned here if this is the first time the object was seen
* @return instance of the current subclass that can be used to save/restore values
*/
INTERACTIVETOOLSFRAMEWORK_API TObjectPtr<UInteractiveToolPropertySet> GetDynamicPropertyCache(const FString& CacheIdentifier, bool& bWasCreatedOut);
public:
#if WITH_EDITOR
/**
* Posts a message to the OnModified delegate with the modified FProperty
* @warning Please consider listening to OnModified instead of overriding this function
* @warning this function is currently only called in Editor (not at runtime)
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent);
#endif
protected:
UPROPERTY(Transient, DuplicateTransient, NonTransactional, SkipSerialization)
TMap<FString, TObjectPtr<UInteractiveToolPropertySet>> CachedPropertiesMap;
// Controls whether a property set is shown in the UI. Transient so that disabling a PropertySet in one tool doesn't disable it in others.
UPROPERTY(Transient, DuplicateTransient, SkipSerialization, meta=(TransientToolProperty))
bool bIsPropertySetEnabled = true;
friend class UInteractiveTool; // so that tool can enable/disable
FInteractiveToolPropertySetModifiedSignature OnModified;
};
/**
* UInteractiveTool is the base class for all Tools in the InteractiveToolsFramework.
* A Tool is is a "lightweight mode" that may "own" one or more Actors/Components/etc in
* the current scene, may capture certain input devices or event streams, and so on.
* The base implementation essentially does nothing but provide sane default behaviors.
*
* The BaseTools/ subfolder contains implementations of various kinds of standard
* "tool behavior", like a tool that responds to a mouse click, etc, that can be
* extended to implement custom behaviors.
*
* In the framework, you do not create instances of UInteractiveTool yourself.
* You provide a UInteractiveToolBuilder implementation that can properly construct
* an instance of your Tool, this is where for example default parameters would be set.
* The ToolBuilder is registered with the ToolManager, and then UInteractiveToolManager::ActivateTool()
* is used to kick things off.
*
* @todo callback/delegate for if/when .InputBehaviors changes
* @todo callback/delegate for when tool properties change
*/
UCLASS(Transient, MinimalAPI)
class UInteractiveTool : public UObject, public IInputBehaviorSource
{
GENERATED_BODY()
public:
INTERACTIVETOOLSFRAMEWORK_API UInteractiveTool();
/**
* Called by ToolManager to initialize the Tool *after* ToolBuilder::BuildTool() has been called
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void Setup();
/**
* Called by ToolManager to shut down the Tool
* @param ShutdownType indicates how the tool should shutdown (ie Accept or Cancel current preview, etc)
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void Shutdown(EToolShutdownType ShutdownType);
/**
* Allow the Tool to do any custom drawing (ie via PDI/RHI)
* @param RenderAPI Abstraction that provides access to Rendering in the current ToolsContext
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void Render(IToolsContextRenderAPI* RenderAPI);
/**
* Allow the Tool to do any custom screen space drawing
* @param Canvas the FCanvas to use to do the drawing
* @param RenderAPI Abstraction that provides access to Rendering in the current ToolsContext
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void DrawHUD( FCanvas* Canvas, IToolsContextRenderAPI* RenderAPI );
/**
* Non overrideable func which does processing and calls the tool's OnTick
* @param DeltaTime the time delta since last tick
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void Tick(float DeltaTime) final;
/**
* @return ToolManager that owns this Tool
*/
INTERACTIVETOOLSFRAMEWORK_API virtual UInteractiveToolManager* GetToolManager() const;
/**
* @return true if this Tool support being Cancelled, ie calling Shutdown(EToolShutdownType::Cancel)
*/
INTERACTIVETOOLSFRAMEWORK_API virtual bool HasCancel() const;
/**
* @return true if this Tool support being Accepted, ie calling Shutdown(EToolShutdownType::Accept)
*/
INTERACTIVETOOLSFRAMEWORK_API virtual bool HasAccept() const;
/**
* @return true if this Tool is currently in a state where it can be Accepted. This may be false if for example there was an error in the Tool.
*/
INTERACTIVETOOLSFRAMEWORK_API virtual bool CanAccept() const;
//
// Input Behaviors support
//
/**
* Add an input behavior for this Tool. Typically only effective when called during tool Setup(), as
* the behavior set gets submitted to the input router after that call.
* @param Behavior behavior to add
* @param Source Optional pointer that could be used to identify the behavior for removal later.
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void AddInputBehavior(UInputBehavior* Behavior, void* Source = nullptr);
/**
* Remove all input behaviors that had the given source pointer set during their addition.
* Typically only effective when called during tool Setup(), as the behavior set gets submitted
* to the input router after that call.
* @param Source Identifying pointer
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void RemoveInputBehaviorsBySource(void* Source);
/**
* @return Current input behavior set.
*/
INTERACTIVETOOLSFRAMEWORK_API virtual const UInputBehaviorSet* GetInputBehaviors() const;
//
// Property support
//
/**
* @return list of property UObjects for this tool (ie to add to a DetailsViewPanel, for example)
*/
INTERACTIVETOOLSFRAMEWORK_API virtual TArray<UObject*> GetToolProperties(bool bEnabledOnly = true) const;
/**
* OnPropertySetsModified is broadcast whenever the contents of the ToolPropertyObjects array is modified
*/
DECLARE_MULTICAST_DELEGATE(OnInteractiveToolPropertySetsModified);
OnInteractiveToolPropertySetsModified OnPropertySetsModified;
/**
* OnPropertyModifiedDirectlyByTool is broadcast whenever the ToolPropertyObjects array stays the same, but a property
* inside of one of the objects is changed internally by the tool. This allows any external display of such properties
* to properly update. In a DetailsViewPanel, for instance, it refreshes certain cached states such as edit condition
* states for other properties.
*
* This should only broadcast when the tool itself is responsible for the change, so it typically isn't broadcast
* from the tool's OnPropertyModified function.
*/
DECLARE_MULTICAST_DELEGATE_OneParam(OnInteractiveToolPropertyInternallyModified, UObject*);
OnInteractiveToolPropertyInternallyModified OnPropertyModifiedDirectlyByTool;
/**
* Automatically called by UInteractiveToolPropertySet.OnModified delegate to notify Tool of child property set changes
* @param PropertySet which UInteractiveToolPropertySet was modified
* @param Property which FProperty in the set was modified
*/
virtual void OnPropertyModified(UObject* PropertySet, FProperty* Property)
{
}
protected:
/** The current set of InputBehaviors provided by this Tool */
UPROPERTY(Transient, DuplicateTransient, NonTransactional, SkipSerialization)
TObjectPtr<UInputBehaviorSet> InputBehaviors;
/** The current set of Property UObjects provided by this Tool. May contain pointer to itself. */
UPROPERTY(Transient, DuplicateTransient, NonTransactional, SkipSerialization)
TArray<TObjectPtr<UObject>> ToolPropertyObjects;
/**
* Add a Property object for this Tool
* @param Property object to add
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void AddToolPropertySource(UObject* PropertyObject);
/**
* Add a PropertySet object for this Tool
* @param PropertySet Property Set object to add
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void AddToolPropertySource(UInteractiveToolPropertySet* PropertySet);
/**
* Remove a PropertySet object from this Tool. If found, will broadcast OnPropertySetsModified
* @param PropertySet property set to remove.
* @return true if PropertySet is found and removed
*/
INTERACTIVETOOLSFRAMEWORK_API virtual bool RemoveToolPropertySource(UInteractiveToolPropertySet* PropertySet);
/**
* Replace a PropertySet object on this Tool with another property set. If replaced, will broadcast OnPropertySetsModified
* @param CurPropertySet property set to remove
* @param ReplaceWith property set to add
* @param bSetToEnabled if true, ReplaceWith property set is explicitly enabled (otherwise enable/disable state is unmodified)
* @return true if CurPropertySet is found and replaced
*/
INTERACTIVETOOLSFRAMEWORK_API virtual bool ReplaceToolPropertySource(UInteractiveToolPropertySet* CurPropertySet, UInteractiveToolPropertySet* ReplaceWith, bool bSetToEnabled = true);
/**
* Enable/Disable a PropertySet object for this Tool. If found and state was modified, will broadcast OnPropertySetsModified
* @param PropertySet Property Set object to modify
* @param bEnabled whether to enable or disable
* @return true if PropertySet was found
*/
INTERACTIVETOOLSFRAMEWORK_API virtual bool SetToolPropertySourceEnabled(UInteractiveToolPropertySet* PropertySet, bool bEnabled);
/**
* Call after changing a propertyset internally in the tool to allow external views of the property
* set to update properly. This is meant as an outward notification mechanism, not a way to to
* pass along notifications, so don't call this if the property is changed externally (i.e., this
* should not usually be called from OnPropertyModified unless the tool adds changes of its own).
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void NotifyOfPropertyChangeByTool(UInteractiveToolPropertySet* PropertySet) const;
enum EAcceptWarning
{
NoWarning,
EmptyForbidden
};
/**
* Helper function to update a standard warning when we need to explain why "Accept" is disabled
* Note that calling this base implementation will clear any unrelated warnings.
* Note that this function is not automatically called by the base class.
*
* @param Warning Reason that tool cannot be accepted (or EAcceptWarning::NoWarning if no warning need be displayed)
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void UpdateAcceptWarnings(EAcceptWarning Warning);
private:
// Tracks whether the UpdateAcceptWarnings function showed a warning the last time it was called.
// Used to avoid clearing the tool display message in cases where it was not set by this function.
bool bLastShowedAcceptWarning = false;
protected:
/**
* Allow the Tool to do any necessary processing on Tick
* @param DeltaTime the time delta since last tick
*/
virtual void OnTick(float DeltaTime){};
//
// Action support/system
//
// Your Tool subclass can register a set of "Actions" it can execute
// by overloading RegisterActions(). Then external systems can use GetActionSet() to
// find out what Actions your Tool supports, and ExecuteAction() to run those actions.
//
public:
/**
* Get the internal Action Set for this Tool. The action set is created and registered on-demand.
* @return pointer to initialized Action set
*/
INTERACTIVETOOLSFRAMEWORK_API virtual FInteractiveToolActionSet* GetActionSet();
/**
* Request that the Action identified by ActionID be executed.
* Default implementation forwards these requests to internal ToolActionSet.
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void ExecuteAction(int32 ActionID);
protected:
/**
* Override this function to register the set of Actions this Tool supports, using FInteractiveToolActionSet::RegisterAction.
* Note that for the actions to be triggered, you will also need to add corresponding registration per tool
* -- see Engine\Plugins\Editor\ModelingToolsEditorMode\Source\ModelingToolsEditorMode\Public\ModelingToolsActions.h for examples
*/
INTERACTIVETOOLSFRAMEWORK_API virtual void RegisterActions(FInteractiveToolActionSet& ActionSet);
private:
/**
* Set of actions this Tool can execute. This variable is allocated on-demand.
* Use GetActionSet() instead of accessing this pointer directly!
*/
FInteractiveToolActionSet* ToolActionSet = nullptr;
//
// Tool Information (name, icon, help text, etc)
//
public:
/**
* @return ToolInfo structure for this Tool
*/
virtual FInteractiveToolInfo GetToolInfo() const
{
return DefaultToolInfo;
}
/**
* Replace existing ToolInfo with new data
*/
virtual void SetToolInfo(const FInteractiveToolInfo& NewInfo)
{
DefaultToolInfo = NewInfo;
}
/**
* Set Tool name
*/
virtual void SetToolDisplayName(const FText& NewName)
{
DefaultToolInfo.ToolDisplayName = NewName;
}
private:
/**
* ToolInfo for this Tool
*/
FInteractiveToolInfo DefaultToolInfo;
private:
// InteractionMechanic needs to be able to talk to Tool internals, eg property sets, behaviors, etc
friend class UInteractionMechanic;
};