// 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 AttributeName; FProperty* Attribute; }; /** The item used for visualizing the attribute in the list. */ class SAttributeItem : public SComboRow< TSharedPtr > { 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, AssociatedNode) SLATE_END_ARGS() /** * Construct the widget * * @param InArgs A declaration from which to construct the widget */ void Construct(const FArguments& InArgs, const TSharedRef& 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 >::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 > > OwnerWidget = OwnerTablePtr.Pin(); const TSharedPtr* 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 Item, const TSharedRef< STableViewBase >& OwnerTable); /** Called by Slate when an item is selected from the tree/list. */ void OnAttributeSelectionChanged(TSharedPtr Item, ESelectInfo::Type SelectInfo); /** Updates the list of items in the dropdown menu */ TSharedPtr UpdatePropertyOptions(); /** Delegate to be called when an attribute is picked from the list */ FOnAttributePicked OnAttributePicked; /** The search box */ TSharedPtr SearchBoxPtr; /** Holds the Slate List widget which holds the attributes for the Attribute Viewer. */ TSharedPtr >> AttributeList; /** Array of items that can be selected in the dropdown menu */ TArray> PropertyOptions; /** Filters needed for filtering the assets */ TSharedPtr 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 >) .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 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 SAttributeListWidget::UpdatePropertyOptions() { PropertyOptions.Empty(); TSharedPtr InitiallySelected = MakeShareable(new FAttributeViewerNode(nullptr, "None")); PropertyOptions.Add(InitiallySelected); // Gather all UAttribute classes for (TObjectIterator 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 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 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 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 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 Item, ESelectInfo::Type SelectInfo) { OnAttributePicked.ExecuteIfBound(Item->Attribute); } void SGameplayAttributeWidget::Construct(const FArguments& InArgs) { FilterMetaData = InArgs._FilterMetaData; OnAttributeChanged = InArgs._OnAttributeChanged; SelectedProperty = InArgs._DefaultProperty; TWeakPtr WeakSelf = StaticCastWeakPtr(AsWeak()); ChildSlot [ SNew(SBorder) .OnMouseButtonDown_Lambda([WeakSelf](const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) { const TSharedPtr 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 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 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 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