Files
UnrealEngine/Engine/Plugins/Runtime/GameplayAbilities/Source/GameplayAbilitiesEditor/Private/SGameplayAttributeWidget.cpp
Brandyn / Techy fcc1b09210 init
2026-04-04 15:40:51 -05:00

548 lines
17 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SGameplayAttributeWidget.h"
#include "Widgets/Layout/SSeparator.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Views/STableViewBase.h"
#include "Widgets/Views/STableRow.h"
#include "Widgets/Views/SListView.h"
#include "Widgets/Input/SComboBox.h"
#include "Widgets/Input/SSearchBox.h"
#include "AbilitySystemComponent.h"
#include "AttributeSet.h"
#include "Editor.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "HAL/PlatformApplicationMisc.h"
#include "Misc/TextFilter.h"
#include "Misc/OutputDeviceNull.h"
#include "ScopedTransaction.h"
#include "SlateOptMacros.h"
#include "UObject/UnrealType.h"
#include "UObject/UObjectHash.h"
#include "UObject/UObjectIterator.h"
#define LOCTEXT_NAMESPACE "K2Node"
DECLARE_DELEGATE_OneParam(FOnAttributePicked, FProperty*);
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
struct FAttributeViewerNode
{
public:
FAttributeViewerNode(FProperty* InAttribute, FString InAttributeName)
{
Attribute = InAttribute;
AttributeName = MakeShareable(new FString(InAttributeName));
}
/** The displayed name for this node. */
TSharedPtr<FString> AttributeName;
FProperty* Attribute;
};
/** The item used for visualizing the attribute in the list. */
class SAttributeItem : public SComboRow< TSharedPtr<FAttributeViewerNode> >
{
public:
SLATE_BEGIN_ARGS(SAttributeItem)
: _HighlightText()
, _TextColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.0f))
{}
/** The text this item should highlight, if any. */
SLATE_ARGUMENT(FText, HighlightText)
/** The color text this item will use. */
SLATE_ARGUMENT(FSlateColor, TextColor)
/** The node this item is associated with. */
SLATE_ARGUMENT(TSharedPtr<FAttributeViewerNode>, AssociatedNode)
SLATE_END_ARGS()
/**
* Construct the widget
*
* @param InArgs A declaration from which to construct the widget
*/
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView)
{
AssociatedNode = InArgs._AssociatedNode;
this->ChildSlot
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 3.0f, 6.0f, 3.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(FText::FromString(*AssociatedNode->AttributeName.Get()))
.HighlightText(InArgs._HighlightText)
.ColorAndOpacity(this, &SAttributeItem::GetTextColor)
.IsEnabled(true)
]
];
TextColor = InArgs._TextColor;
STableRow< TSharedPtr<FAttributeViewerNode> >::ConstructInternal(
STableRow::FArguments()
.ShowSelection(true),
InOwnerTableView
);
}
/** Returns the text color for the item based on if it is selected or not. */
FSlateColor GetTextColor() const
{
const TSharedPtr< ITypedTableView< TSharedPtr<FAttributeViewerNode> > > OwnerWidget = OwnerTablePtr.Pin();
const TSharedPtr<FAttributeViewerNode>* MyItem = OwnerWidget->Private_ItemFromWidget(this);
const bool bIsSelected = OwnerWidget->Private_IsItemSelected(*MyItem);
if (bIsSelected)
{
return FSlateColor::UseForeground();
}
return TextColor;
}
private:
/** The text color for this item. */
FSlateColor TextColor;
/** The Attribute Viewer Node this item is associated with. */
TSharedPtr< FAttributeViewerNode > AssociatedNode;
};
class SAttributeListWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SAttributeListWidget)
{
}
SLATE_ARGUMENT(FString, FilterMetaData)
SLATE_ARGUMENT(FOnAttributePicked, OnAttributePickedDelegate)
SLATE_END_ARGS()
/**
* Construct the widget
*
* @param InArgs A declaration from which to construct the widget
*/
void Construct(const FArguments& InArgs);
virtual ~SAttributeListWidget();
private:
typedef TTextFilter< const FProperty& > FAttributeTextFilter;
/** Called by Slate when the filter box changes text. */
void OnFilterTextChanged(const FText& InFilterText);
/** Creates the row widget when called by Slate when an item appears on the list. */
TSharedRef< ITableRow > OnGenerateRowForAttributeViewer(TSharedPtr<FAttributeViewerNode> Item, const TSharedRef< STableViewBase >& OwnerTable);
/** Called by Slate when an item is selected from the tree/list. */
void OnAttributeSelectionChanged(TSharedPtr<FAttributeViewerNode> Item, ESelectInfo::Type SelectInfo);
/** Updates the list of items in the dropdown menu */
TSharedPtr<FAttributeViewerNode> UpdatePropertyOptions();
/** Delegate to be called when an attribute is picked from the list */
FOnAttributePicked OnAttributePicked;
/** The search box */
TSharedPtr<SSearchBox> SearchBoxPtr;
/** Holds the Slate List widget which holds the attributes for the Attribute Viewer. */
TSharedPtr<SListView<TSharedPtr< FAttributeViewerNode > >> AttributeList;
/** Array of items that can be selected in the dropdown menu */
TArray<TSharedPtr<FAttributeViewerNode>> PropertyOptions;
/** Filters needed for filtering the assets */
TSharedPtr<FAttributeTextFilter> AttributeTextFilter;
/** Filter for meta data */
FString FilterMetaData;
};
SAttributeListWidget::~SAttributeListWidget()
{
if (OnAttributePicked.IsBound())
{
OnAttributePicked.Unbind();
}
}
void SAttributeListWidget::Construct(const FArguments& InArgs)
{
struct Local
{
static void AttributeToStringArray(const FProperty& Property, OUT TArray< FString >& StringArray)
{
UClass* Class = Property.GetOwnerClass();
if ((Class->IsChildOf(UAttributeSet::StaticClass()) && !Class->ClassGeneratedBy) ||
(Class->IsChildOf(UAbilitySystemComponent::StaticClass()) && !Class->ClassGeneratedBy))
{
StringArray.Add(FString::Printf(TEXT("%s.%s"), *Class->GetName(), *Property.GetName()));
}
}
};
FilterMetaData = InArgs._FilterMetaData;
OnAttributePicked = InArgs._OnAttributePickedDelegate;
// Setup text filtering
AttributeTextFilter = MakeShareable(new FAttributeTextFilter(FAttributeTextFilter::FItemToStringArray::CreateStatic(&Local::AttributeToStringArray)));
UpdatePropertyOptions();
TSharedPtr< SWidget > ClassViewerContent;
SAssignNew(ClassViewerContent, SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SAssignNew(SearchBoxPtr, SSearchBox)
.HintText(NSLOCTEXT("Abilities", "SearchBoxHint", "Search Attributes"))
.OnTextChanged(this, &SAttributeListWidget::OnFilterTextChanged)
.DelayChangeNotificationsWhileTyping(true)
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SSeparator)
.Visibility(EVisibility::Collapsed)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SAssignNew(AttributeList, SListView<TSharedPtr< FAttributeViewerNode > >)
.Visibility(EVisibility::Visible)
.SelectionMode(ESelectionMode::Single)
.ListItemsSource(&PropertyOptions)
// Generates the actual widget for a tree item
.OnGenerateRow(this, &SAttributeListWidget::OnGenerateRowForAttributeViewer)
// Find out when the user selects something in the tree
.OnSelectionChanged(this, &SAttributeListWidget::OnAttributeSelectionChanged)
];
ChildSlot
[
ClassViewerContent.ToSharedRef()
];
}
TSharedRef< ITableRow > SAttributeListWidget::OnGenerateRowForAttributeViewer(TSharedPtr<FAttributeViewerNode> Item, const TSharedRef< STableViewBase >& OwnerTable)
{
TSharedRef< SAttributeItem > ReturnRow = SNew(SAttributeItem, OwnerTable)
.HighlightText(SearchBoxPtr->GetText())
.TextColor(FLinearColor(1.0f, 1.0f, 1.0f, 1.f))
.AssociatedNode(Item);
return ReturnRow;
}
TSharedPtr<FAttributeViewerNode> SAttributeListWidget::UpdatePropertyOptions()
{
PropertyOptions.Empty();
TSharedPtr<FAttributeViewerNode> InitiallySelected = MakeShareable(new FAttributeViewerNode(nullptr, "None"));
PropertyOptions.Add(InitiallySelected);
// Gather all UAttribute classes
for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
{
UClass *Class = *ClassIt;
if (Class->IsChildOf(UAttributeSet::StaticClass()) && !Class->ClassGeneratedBy)
{
// Allow entire classes to be filtered globally
if (Class->HasMetaData(TEXT("HideInDetailsView")))
{
continue;
}
for (TFieldIterator<FProperty> PropertyIt(Class, EFieldIteratorFlags::ExcludeSuper); PropertyIt; ++PropertyIt)
{
FProperty *Property = *PropertyIt;
// Ignore property types that cannot represent gameplay attributes
if (!FGameplayAttribute::IsSupportedProperty(Property))
{
continue;
}
// if we have a search string and this doesn't match, don't show it
if (AttributeTextFilter.IsValid() && !AttributeTextFilter->PassesFilter(*Property))
{
continue;
}
// don't show attributes that are filtered by meta data
if (!FilterMetaData.IsEmpty() && Property->HasMetaData(*FilterMetaData))
{
continue;
}
// Allow properties to be filtered globally (never show up)
if (Property->HasMetaData(TEXT("HideInDetailsView")))
{
continue;
}
TSharedPtr<FAttributeViewerNode> SelectableProperty = MakeShareable(new FAttributeViewerNode(Property, FString::Printf(TEXT("%s.%s"), *Class->GetName(), *Property->GetName())));
PropertyOptions.Add(SelectableProperty);
}
}
// UAbilitySystemComponent can add 'system' attributes
if (Class->IsChildOf(UAbilitySystemComponent::StaticClass()) && !Class->ClassGeneratedBy)
{
for (TFieldIterator<FProperty> PropertyIt(Class, EFieldIteratorFlags::ExcludeSuper); PropertyIt; ++PropertyIt)
{
FProperty* Property = *PropertyIt;
// SystemAttributes have to be explicitly tagged
if (Property->HasMetaData(TEXT("SystemGameplayAttribute")) == false)
{
continue;
}
// if we have a search string and this doesn't match, don't show it
if (AttributeTextFilter.IsValid() && !AttributeTextFilter->PassesFilter(*Property))
{
continue;
}
TSharedPtr<FAttributeViewerNode> SelectableProperty = MakeShareable(new FAttributeViewerNode(Property, FString::Printf(TEXT("%s.%s"), *Class->GetName(), *Property->GetName())));
PropertyOptions.Add(SelectableProperty);
}
}
}
return InitiallySelected;
}
void SAttributeListWidget::OnFilterTextChanged(const FText& InFilterText)
{
AttributeTextFilter->SetRawFilterText(InFilterText);
SearchBoxPtr->SetError(AttributeTextFilter->GetFilterErrorText());
UpdatePropertyOptions();
}
void SAttributeListWidget::OnAttributeSelectionChanged(TSharedPtr<FAttributeViewerNode> Item, ESelectInfo::Type SelectInfo)
{
OnAttributePicked.ExecuteIfBound(Item->Attribute);
}
void SGameplayAttributeWidget::Construct(const FArguments& InArgs)
{
FilterMetaData = InArgs._FilterMetaData;
OnAttributeChanged = InArgs._OnAttributeChanged;
SelectedProperty = InArgs._DefaultProperty;
TWeakPtr<SGameplayAttributeWidget> WeakSelf = StaticCastWeakPtr<SGameplayAttributeWidget>(AsWeak());
ChildSlot
[
SNew(SBorder)
.OnMouseButtonDown_Lambda([WeakSelf](const FGeometry& MyGeometry, const FPointerEvent& MouseEvent)
{
const TSharedPtr<SGameplayAttributeWidget> Self = WeakSelf.Pin();
if (Self.IsValid() && MouseEvent.IsMouseButtonDown(EKeys::RightMouseButton))
{
Self->OnMenu(MouseEvent);
return FReply::Handled();
}
return FReply::Unhandled();
})
.Padding(0.0f)
.BorderImage(FStyleDefaults::GetNoBrush())
[
// set up the combo button
SAssignNew(ComboButton, SComboButton)
.OnGetMenuContent(this, &SGameplayAttributeWidget::GenerateAttributePicker)
.ContentPadding(FMargin(2.0f, 2.0f))
.ToolTipText(this, &SGameplayAttributeWidget::GetSelectedValueAsString)
.ButtonContent()
[
SNew(STextBlock)
.Text(this, &SGameplayAttributeWidget::GetSelectedValueAsString)
]
]
];
}
void SGameplayAttributeWidget::OnCopyAttribute(FProperty* AttributeProperty)
{
FGameplayAttribute Attribute(AttributeProperty);
if (Attribute.IsValid())
{
if (const TObjectPtr<UObject> OwnerObject = Attribute.GetAttributeSetClass()->GetDefaultObject(false))
{
FString ExportedString;
FGameplayAttribute::StaticStruct()->ExportText(ExportedString, &Attribute, &Attribute, OwnerObject, 0, nullptr);
FPlatformApplicationMisc::ClipboardCopy(*ExportedString);
}
}
}
FGameplayAttribute AttributeTryImportTextFromClipboard()
{
FString PastedText;
FPlatformApplicationMisc::ClipboardPaste(PastedText);
FOutputDeviceNull NullOut;
FGameplayAttribute Attribute;
FGameplayAttribute::StaticStruct()->ImportText(*PastedText, &Attribute, /*OwnerObject*/nullptr, 0, &NullOut, FGameplayAttribute::StaticStruct()->GetName(), /*bAllowNativeOverride*/true);
return Attribute;
}
bool SGameplayAttributeWidget::CanPaste() const
{
const FGameplayAttribute Attribute = AttributeTryImportTextFromClipboard();
return Attribute.IsValid();
}
void SGameplayAttributeWidget::OnPasteAttribute()
{
const FGameplayAttribute Attribute = AttributeTryImportTextFromClipboard();
if (Attribute.IsValid())
{
FScopedTransaction Transaction(LOCTEXT("GameplayAttributeWidget_PasteAttribute", "Paste Gameplay Attribute"));
SelectedProperty = Attribute.GetUProperty();
OnAttributeChanged.ExecuteIfBound(SelectedProperty);
}
}
void SGameplayAttributeWidget::OnMenu(const FPointerEvent& MouseEvent)
{
FMenuBuilder MenuBuilder(/*bShouldCloseWindowAfterMenuSelection=*/ true, /*CommandList=*/ nullptr);
FGameplayAttribute SelectedAttribute(SelectedProperty);
auto IsValidAttribute = [](const FGameplayAttribute& Attribute)
{
return Attribute.IsValid();
};
MenuBuilder.AddMenuEntry(
LOCTEXT("GameplayAttribute_SearchForReferences", "Search For References"),
FText::Format(LOCTEXT("GameplayAttributeWidget_SearchForReferencesTooltip", "Find references to attribute {0}"), GetSelectedValueAsString()),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Search"),
FUIAction(FExecuteAction::CreateLambda([&]()
{
const FText AttributeName = GetSelectedValueAsString();
if (FEditorDelegates::OnOpenReferenceViewer.IsBound() && !AttributeName.IsEmpty())
{
TArray<FAssetIdentifier> AssetIdentifiers;
AssetIdentifiers.Emplace(FGameplayAttribute::StaticStruct(), *AttributeName.ToString());
FEditorDelegates::OnOpenReferenceViewer.Broadcast(AssetIdentifiers, FReferenceViewerParams());
}
}))
);
MenuBuilder.AddSeparator();
MenuBuilder.AddMenuEntry(
NSLOCTEXT("PropertyView", "CopyProperty", "Copy"),
FText::Format(LOCTEXT("GameplayAttributeWidget_CopyAttributeTooltip", "Copy attribute {0} to clipboard"), GetSelectedValueAsString()),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GenericCommands.Copy"),
FUIAction(FExecuteAction::CreateSP(this, &SGameplayAttributeWidget::OnCopyAttribute, SelectedProperty), FCanExecuteAction::CreateLambda(IsValidAttribute, SelectedAttribute)));
const FGameplayAttribute Attribute = AttributeTryImportTextFromClipboard();
FText FormattedPastedAttribute;
if (Attribute.GetUProperty())
{
UClass* Class = Attribute.GetUProperty()->GetOwnerClass();
FString PropertyString = FString::Printf(TEXT("%s.%s"), *Class->GetName(), *Attribute.GetUProperty()->GetName());
FormattedPastedAttribute = FText::FromString(PropertyString);
}
else
{
FormattedPastedAttribute = FText::FromString(TEXT("None"));
}
MenuBuilder.AddMenuEntry(
NSLOCTEXT("PropertyView", "PasteProperty", "Paste"),
FText::Format(LOCTEXT("GameplayAttributeWidget_PasteAttributeTooltip", "Paste attribute ({0}) from clipboard."), FormattedPastedAttribute),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "GenericCommands.Paste"),
FUIAction(FExecuteAction::CreateSP(this, &SGameplayAttributeWidget::OnPasteAttribute), FCanExecuteAction::CreateSP(this, &SGameplayAttributeWidget::CanPaste)));
// Spawn context menu
FWidgetPath WidgetPath = MouseEvent.GetEventPath() != nullptr ? *MouseEvent.GetEventPath() : FWidgetPath();
FSlateApplication::Get().PushMenu(AsShared(), WidgetPath, MenuBuilder.MakeWidget(), MouseEvent.GetScreenSpacePosition(), FPopupTransitionEffect(FPopupTransitionEffect::ContextMenu));
}
void SGameplayAttributeWidget::OnAttributePicked(FProperty* InProperty)
{
if (OnAttributeChanged.IsBound())
{
OnAttributeChanged.Execute(InProperty);
}
// Update the selected item for displaying
SelectedProperty = InProperty;
// close the list
ComboButton->SetIsOpen(false);
}
TSharedRef<SWidget> SGameplayAttributeWidget::GenerateAttributePicker()
{
FOnAttributePicked OnPicked(FOnAttributePicked::CreateRaw(this, &SGameplayAttributeWidget::OnAttributePicked));
return SNew(SBox)
.WidthOverride(280)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.MaxHeight(500)
[
SNew(SAttributeListWidget)
.OnAttributePickedDelegate(OnPicked)
.FilterMetaData(FilterMetaData)
]
];
}
FText SGameplayAttributeWidget::GetSelectedValueAsString() const
{
if (SelectedProperty)
{
UClass* Class = SelectedProperty->GetOwnerClass();
FString PropertyString = FString::Printf(TEXT("%s.%s"), *Class->GetName(), *SelectedProperty->GetName());
return FText::FromString(PropertyString);
}
return FText::FromString(TEXT("None"));
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
#undef LOCTEXT_NAMESPACE