443 lines
18 KiB
C++
443 lines
18 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SComponentClassCombo.h"
|
|
#include "Widgets/Layout/SSpacer.h"
|
|
#include "Widgets/Images/SImage.h"
|
|
#include "Widgets/SToolTip.h"
|
|
#include "Widgets/Views/SListView.h"
|
|
#include "Widgets/Input/SComboBox.h"
|
|
#include "EditorStyleSet.h"
|
|
#include "Components/SceneComponent.h"
|
|
#include "Engine/Blueprint.h"
|
|
#include "Engine/Selection.h"
|
|
#include "Editor.h"
|
|
|
|
#include "Styling/SlateIconFinder.h"
|
|
#include "ComponentAssetBroker.h"
|
|
#include "ComponentTypeRegistry.h"
|
|
#include "EditorClassUtils.h"
|
|
#include "Widgets/Input/SSearchBox.h"
|
|
#include "SListViewSelectorDropdownMenu.h"
|
|
#include "Misc/TextFilterExpressionEvaluator.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "ComponentClassCombo"
|
|
|
|
FString FComponentClassComboEntry::GetClassName() const
|
|
{
|
|
return ComponentClass != nullptr ? ComponentClass->GetDisplayNameText().ToString() : ComponentName;
|
|
}
|
|
|
|
void FComponentClassComboEntry::AddReferencedObjects(FReferenceCollector& Collector)
|
|
{
|
|
UClass* RawClass = ComponentClass;
|
|
Collector.AddReferencedObject(RawClass);
|
|
if (RawClass && RawClass->IsChildOf(UActorComponent::StaticClass()))
|
|
{
|
|
ComponentClass = RawClass;
|
|
}
|
|
else
|
|
{
|
|
ComponentClass = nullptr;
|
|
}
|
|
|
|
Collector.AddReferencedObject(IconClass);
|
|
}
|
|
|
|
void SComponentClassCombo::Construct(const FArguments& InArgs)
|
|
{
|
|
PrevSelectedIndex = INDEX_NONE;
|
|
OnComponentClassSelected = InArgs._OnComponentClassSelected;
|
|
TextFilter = MakeShared<FTextFilterExpressionEvaluator>(ETextFilterExpressionEvaluatorMode::BasicString);
|
|
|
|
FComponentTypeRegistry::Get().SubscribeToComponentList(ComponentClassList).AddRaw(this, &SComponentClassCombo::UpdateComponentClassList);
|
|
|
|
UpdateComponentClassList();
|
|
|
|
SAssignNew(ComponentClassListView, SListView<FComponentClassComboEntryPtr>)
|
|
.ListItemsSource(&FilteredComponentClassList)
|
|
.OnSelectionChanged(this, &SComponentClassCombo::OnAddComponentSelectionChanged)
|
|
.OnGenerateRow(this, &SComponentClassCombo::GenerateAddComponentRow)
|
|
.SelectionMode(ESelectionMode::Single);
|
|
|
|
SAssignNew(SearchBox, SSearchBox)
|
|
.HintText(LOCTEXT("BlueprintAddComponentSearchBoxHint", "Search Components"))
|
|
.OnTextChanged(this, &SComponentClassCombo::OnSearchBoxTextChanged)
|
|
.OnTextCommitted(this, &SComponentClassCombo::OnSearchBoxTextCommitted);
|
|
|
|
// Create the Construct arguments for the parent class (SComboButton)
|
|
SComboButton::FArguments Args;
|
|
Args.ButtonContent()
|
|
[SNew(SHorizontalBox) + SHorizontalBox::Slot().VAlign(VAlign_Center).AutoWidth().Padding(1.f, 1.f)[SNew(STextBlock).TextStyle(FEditorStyle::Get(), "ContentBrowser.TopBar.Font").Font(FEditorStyle::Get().GetFontStyle("FontAwesome.10")).Text(FText::FromString(FString(TEXT("\xf067"))) /*fa-plus*/)] + SHorizontalBox::Slot().VAlign(VAlign_Center).Padding(1.f)[SNew(STextBlock).Text(LOCTEXT("AddComponentButtonLabel", "Add Component")).TextStyle(FEditorStyle::Get(), "ContentBrowser.TopBar.Font").Visibility(InArgs._IncludeText.Get() ? EVisibility::Visible : EVisibility::Collapsed)]]
|
|
.MenuContent()
|
|
[
|
|
|
|
SNew(SListViewSelectorDropdownMenu<FComponentClassComboEntryPtr>, SearchBox, ComponentClassListView)
|
|
[SNew(SBorder)
|
|
.BorderImage(FEditorStyle::GetBrush("Menu.Background"))
|
|
.Padding(2)
|
|
[SNew(SBox)
|
|
.WidthOverride(250)
|
|
[SNew(SVerticalBox) + SVerticalBox::Slot().Padding(1.f).AutoHeight()[SearchBox.ToSharedRef()] + SVerticalBox::Slot().MaxHeight(400)[ComponentClassListView.ToSharedRef()]]]]]
|
|
.IsFocusable(true)
|
|
.ContentPadding(FMargin(5, 0))
|
|
.ComboButtonStyle(FEditorStyle::Get(), "ToolbarComboButton")
|
|
.ButtonStyle(FEditorStyle::Get(), "FlatButton.Success")
|
|
.ForegroundColor(FLinearColor::White)
|
|
.OnComboBoxOpened(this, &SComponentClassCombo::ClearSelection);
|
|
|
|
SComboButton::Construct(Args);
|
|
|
|
ComponentClassListView->EnableToolTipForceField(true);
|
|
// The base class can automatically handle setting focus to a specified control when the combo button is opened
|
|
SetMenuContentWidgetToFocus(SearchBox);
|
|
}
|
|
|
|
SComponentClassCombo::~SComponentClassCombo()
|
|
{
|
|
FComponentTypeRegistry::Get().GetOnComponentTypeListChanged().RemoveAll(this);
|
|
}
|
|
|
|
void SComponentClassCombo::ClearSelection()
|
|
{
|
|
SearchBox->SetText(FText::GetEmpty());
|
|
|
|
PrevSelectedIndex = INDEX_NONE;
|
|
|
|
// Clear the selection in such a way as to also clear the keyboard selector
|
|
ComponentClassListView->SetSelection(NULL, ESelectInfo::OnNavigation);
|
|
|
|
// Make sure we scroll to the top
|
|
if (ComponentClassList->Num() > 0)
|
|
{
|
|
ComponentClassListView->RequestScrollIntoView((*ComponentClassList)[0]);
|
|
}
|
|
}
|
|
|
|
void SComponentClassCombo::GenerateFilteredComponentList()
|
|
{
|
|
if (TextFilter->GetFilterText().IsEmpty())
|
|
{
|
|
FilteredComponentClassList = *ComponentClassList;
|
|
}
|
|
else
|
|
{
|
|
FilteredComponentClassList.Empty();
|
|
|
|
int32 LastHeadingIndex = INDEX_NONE;
|
|
FComponentClassComboEntryPtr* LastHeadingPtr = nullptr;
|
|
|
|
for (int32 ComponentIndex = 0; ComponentIndex < ComponentClassList->Num(); ComponentIndex++)
|
|
{
|
|
FComponentClassComboEntryPtr& CurrentEntry = (*ComponentClassList)[ComponentIndex];
|
|
|
|
if (CurrentEntry->IsHeading())
|
|
{
|
|
LastHeadingIndex = FilteredComponentClassList.Num();
|
|
LastHeadingPtr = &CurrentEntry;
|
|
}
|
|
else if (CurrentEntry->IsClass() && CurrentEntry->IsIncludedInFilter())
|
|
{
|
|
FString FriendlyComponentName = GetSanitizedComponentName(CurrentEntry);
|
|
|
|
if (TextFilter->TestTextFilter(FBasicStringFilterExpressionContext(FriendlyComponentName)))
|
|
{
|
|
// Add the heading first if it hasn't already been added
|
|
if (LastHeadingIndex != INDEX_NONE)
|
|
{
|
|
FilteredComponentClassList.Insert(*LastHeadingPtr, LastHeadingIndex);
|
|
LastHeadingIndex = INDEX_NONE;
|
|
LastHeadingPtr = nullptr;
|
|
}
|
|
|
|
// Add the class
|
|
FilteredComponentClassList.Add(CurrentEntry);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Select the first non-category item that passed the filter
|
|
for (FComponentClassComboEntryPtr& TestEntry: FilteredComponentClassList)
|
|
{
|
|
if (TestEntry->IsClass())
|
|
{
|
|
ComponentClassListView->SetSelection(TestEntry, ESelectInfo::OnNavigation);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FText SComponentClassCombo::GetCurrentSearchString() const
|
|
{
|
|
return TextFilter->GetFilterText();
|
|
}
|
|
|
|
void SComponentClassCombo::OnSearchBoxTextChanged(const FText& InSearchText)
|
|
{
|
|
TextFilter->SetFilterText(InSearchText);
|
|
SearchBox->SetError(TextFilter->GetFilterErrorText());
|
|
|
|
// Generate a filtered list
|
|
GenerateFilteredComponentList();
|
|
|
|
// Ask the combo to update its contents on next tick
|
|
ComponentClassListView->RequestListRefresh();
|
|
}
|
|
|
|
void SComponentClassCombo::OnSearchBoxTextCommitted(const FText& NewText, ETextCommit::Type CommitInfo)
|
|
{
|
|
if (CommitInfo == ETextCommit::OnEnter)
|
|
{
|
|
auto SelectedItems = ComponentClassListView->GetSelectedItems();
|
|
if (SelectedItems.Num() > 0)
|
|
{
|
|
ComponentClassListView->SetSelection(SelectedItems[0]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// @todo: move this to FKismetEditorUtilities
|
|
static UClass* GetAuthoritativeBlueprintClass(UBlueprint const* const Blueprint)
|
|
{
|
|
UClass* BpClass = (Blueprint->SkeletonGeneratedClass != nullptr) ? Blueprint->SkeletonGeneratedClass :
|
|
Blueprint->GeneratedClass;
|
|
|
|
if (BpClass == nullptr)
|
|
{
|
|
BpClass = Blueprint->ParentClass;
|
|
}
|
|
|
|
UClass* AuthoritativeClass = BpClass;
|
|
if (BpClass != nullptr)
|
|
{
|
|
AuthoritativeClass = BpClass->GetAuthoritativeClass();
|
|
}
|
|
return AuthoritativeClass;
|
|
}
|
|
|
|
void SComponentClassCombo::OnAddComponentSelectionChanged(FComponentClassComboEntryPtr InItem, ESelectInfo::Type SelectInfo)
|
|
{
|
|
if (InItem.IsValid() && InItem->IsClass() && SelectInfo != ESelectInfo::OnNavigation)
|
|
{
|
|
// We don't want the item to remain selected
|
|
ClearSelection();
|
|
|
|
if (InItem->IsClass())
|
|
{
|
|
// Neither do we want the combo dropdown staying open once the user has clicked on a valid option
|
|
SetIsOpen(false, false);
|
|
|
|
if (OnComponentClassSelected.IsBound())
|
|
{
|
|
UClass* ComponentClass = InItem->GetComponentClass();
|
|
if (ComponentClass == nullptr)
|
|
{
|
|
// The class is not loaded yet, so load it:
|
|
const ELoadFlags LoadFlags = LOAD_None;
|
|
UBlueprint* LoadedObject = LoadObject<UBlueprint>(NULL, *InItem->GetComponentPath(), NULL, LoadFlags, NULL);
|
|
ComponentClass = GetAuthoritativeBlueprintClass(LoadedObject);
|
|
}
|
|
|
|
UActorComponent* NewActorComponent = OnComponentClassSelected.Execute(ComponentClass, InItem->GetComponentCreateAction(), InItem->GetAssetOverride());
|
|
if (NewActorComponent)
|
|
{
|
|
InItem->GetOnComponentCreated().ExecuteIfBound(NewActorComponent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (InItem.IsValid() && SelectInfo != ESelectInfo::OnMouseClick)
|
|
{
|
|
int32 SelectedIdx = INDEX_NONE;
|
|
if (FilteredComponentClassList.Find(InItem, /*out*/ SelectedIdx))
|
|
{
|
|
if (!InItem->IsClass())
|
|
{
|
|
int32 SelectionDirection = SelectedIdx - PrevSelectedIndex;
|
|
|
|
// Update the previous selected index
|
|
PrevSelectedIndex = SelectedIdx;
|
|
|
|
// Make sure we select past the category header if we started filtering with it selected somehow (avoiding the infinite loop selecting the same item forever)
|
|
if (SelectionDirection == 0)
|
|
{
|
|
SelectionDirection = 1;
|
|
}
|
|
|
|
if (SelectedIdx + SelectionDirection >= 0 && SelectedIdx + SelectionDirection < FilteredComponentClassList.Num())
|
|
{
|
|
ComponentClassListView->SetSelection(FilteredComponentClassList[SelectedIdx + SelectionDirection], ESelectInfo::OnNavigation);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Update the previous selected index
|
|
PrevSelectedIndex = SelectedIdx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
TSharedRef<ITableRow> SComponentClassCombo::GenerateAddComponentRow(FComponentClassComboEntryPtr Entry, const TSharedRef<STableViewBase>& OwnerTable) const
|
|
{
|
|
check(Entry->IsHeading() || Entry->IsSeparator() || Entry->IsClass());
|
|
|
|
if (Entry->IsHeading())
|
|
{
|
|
return SNew(STableRow<TSharedPtr<FString>>, OwnerTable)
|
|
.Style(&FEditorStyle::Get().GetWidgetStyle<FTableRowStyle>("TableView.NoHoverTableRow"))
|
|
.ShowSelection(false)
|
|
[SNew(SBox)
|
|
.Padding(1.f)
|
|
[SNew(STextBlock)
|
|
.Text(FText::FromString(Entry->GetHeadingText()))
|
|
.TextStyle(FEditorStyle::Get(), TEXT("Menu.Heading"))]];
|
|
}
|
|
else if (Entry->IsSeparator())
|
|
{
|
|
return SNew(STableRow<TSharedPtr<FString>>, OwnerTable)
|
|
.Style(&FEditorStyle::Get().GetWidgetStyle<FTableRowStyle>("TableView.NoHoverTableRow"))
|
|
.ShowSelection(false)
|
|
[SNew(SBox)
|
|
.Padding(1.f)
|
|
[SNew(SBorder)
|
|
.Padding(FEditorStyle::GetMargin(TEXT("Menu.Separator.Padding")))
|
|
.BorderImage(FEditorStyle::GetBrush(TEXT("Menu.Separator")))]];
|
|
}
|
|
else
|
|
{
|
|
|
|
return SNew(SComboRow<TSharedPtr<FString>>, OwnerTable)
|
|
.ToolTip(GetComponentToolTip(Entry))
|
|
[SNew(SHorizontalBox) + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(SSpacer).Size(FVector2D(8.0f, 1.0f))] + SHorizontalBox::Slot().Padding(1.0f).AutoWidth()[SNew(SImage).Image(FSlateIconFinder::FindIconBrushForClass(Entry->GetIconOverrideBrushName() == NAME_None ? Entry->GetIconClass() : nullptr, Entry->GetIconOverrideBrushName()))] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(SSpacer).Size(FVector2D(3.0f, 1.0f))] + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)[SNew(STextBlock).HighlightText(this, &SComponentClassCombo::GetCurrentSearchString).Text(this, &SComponentClassCombo::GetFriendlyComponentName, Entry)]];
|
|
}
|
|
}
|
|
|
|
void SComponentClassCombo::UpdateComponentClassList()
|
|
{
|
|
GenerateFilteredComponentList();
|
|
}
|
|
|
|
FText SComponentClassCombo::GetFriendlyComponentName(FComponentClassComboEntryPtr Entry) const
|
|
{
|
|
// Get a user friendly string from the component name
|
|
FString FriendlyComponentName;
|
|
|
|
if (Entry->GetComponentCreateAction() == EComponentCreateAction::CreateNewCPPClass)
|
|
{
|
|
FriendlyComponentName = LOCTEXT("NewCPPComponentFriendlyName", "New C++ Component...").ToString();
|
|
}
|
|
else if (Entry->GetComponentCreateAction() == EComponentCreateAction::CreateNewBlueprintClass)
|
|
{
|
|
FriendlyComponentName = LOCTEXT("NewBlueprintComponentFriendlyName", "New Blueprint Script Component...").ToString();
|
|
}
|
|
else
|
|
{
|
|
FriendlyComponentName = GetSanitizedComponentName(Entry);
|
|
|
|
// Don't try to match up assets for USceneComponent it will match lots of things and doesn't have any nice behavior for asset adds
|
|
if (Entry->GetComponentClass() != USceneComponent::StaticClass() && Entry->GetComponentNameOverride().IsEmpty())
|
|
{
|
|
// Search the selected assets and look for any that can be used as a source asset for this type of component
|
|
// If there is one we append the asset name to the component name, if there are many we append "Multiple Assets"
|
|
FString AssetName;
|
|
UObject* PreviousMatchingAsset = NULL;
|
|
|
|
FEditorDelegates::LoadSelectedAssetsIfNeeded.Broadcast();
|
|
USelection* Selection = GEditor->GetSelectedObjects();
|
|
for (FSelectionIterator ObjectIter(*Selection); ObjectIter; ++ObjectIter)
|
|
{
|
|
UObject* Object = *ObjectIter;
|
|
check(Object);
|
|
UClass* Class = Object->GetClass();
|
|
|
|
TArray<TSubclassOf<UActorComponent>> ComponentClasses = FComponentAssetBrokerage::GetComponentsForAsset(Object);
|
|
for (int32 ComponentIndex = 0; ComponentIndex < ComponentClasses.Num(); ComponentIndex++)
|
|
{
|
|
if (ComponentClasses[ComponentIndex]->IsChildOf(Entry->GetComponentClass()))
|
|
{
|
|
if (AssetName.IsEmpty())
|
|
{
|
|
// If there is no previous asset then we just accept the name
|
|
AssetName = Object->GetName();
|
|
PreviousMatchingAsset = Object;
|
|
}
|
|
else
|
|
{
|
|
// if there is a previous asset then check that we didn't just find multiple appropriate components
|
|
// in a single asset - if the asset differs then we don't display the name, just "Multiple Assets"
|
|
if (PreviousMatchingAsset != Object)
|
|
{
|
|
AssetName = LOCTEXT("MultipleAssetsForComponentAnnotation", "Multiple Assets").ToString();
|
|
PreviousMatchingAsset = Object;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!AssetName.IsEmpty())
|
|
{
|
|
FriendlyComponentName += FString(" (") + AssetName + FString(")");
|
|
}
|
|
}
|
|
}
|
|
return FText::FromString(FriendlyComponentName);
|
|
}
|
|
|
|
FString SComponentClassCombo::GetSanitizedComponentName(FComponentClassComboEntryPtr Entry)
|
|
{
|
|
FString DisplayName;
|
|
if (Entry->GetComponentNameOverride() != FString())
|
|
{
|
|
DisplayName = Entry->GetComponentNameOverride();
|
|
}
|
|
else if (UClass* ComponentClass = Entry->GetComponentClass())
|
|
{
|
|
if (ComponentClass->HasMetaData(TEXT("DisplayName")))
|
|
{
|
|
DisplayName = ComponentClass->GetMetaData(TEXT("DisplayName"));
|
|
}
|
|
else
|
|
{
|
|
DisplayName = ComponentClass->GetDisplayNameText().ToString();
|
|
if (!ComponentClass->HasAnyClassFlags(CLASS_CompiledFromBlueprint))
|
|
{
|
|
DisplayName.RemoveFromEnd(TEXT("Component"), ESearchCase::IgnoreCase);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DisplayName = Entry->GetClassName();
|
|
}
|
|
return FName::NameToDisplayString(DisplayName, false);
|
|
}
|
|
|
|
TSharedRef<SToolTip> SComponentClassCombo::GetComponentToolTip(FComponentClassComboEntryPtr Entry) const
|
|
{
|
|
// Special handling for the "New..." options
|
|
if (Entry->GetComponentCreateAction() == EComponentCreateAction::CreateNewCPPClass)
|
|
{
|
|
return SNew(SToolTip)
|
|
.Text(LOCTEXT("NewCPPComponentToolTip", "Create a custom actor component using C++"));
|
|
}
|
|
else if (Entry->GetComponentCreateAction() == EComponentCreateAction::CreateNewBlueprintClass)
|
|
{
|
|
return SNew(SToolTip)
|
|
.Text(LOCTEXT("NewBlueprintComponentToolTip", "Create a custom actor component using Blueprints"));
|
|
}
|
|
|
|
// Handle components which have a currently loaded class
|
|
if (const UClass* ComponentClass = Entry->GetComponentClass())
|
|
{
|
|
return FEditorClassUtils::GetTooltip(ComponentClass);
|
|
}
|
|
|
|
// Fallback for components that don't currently have a loaded class
|
|
return SNew(SToolTip)
|
|
.Text(FText::FromString(Entry->GetClassName()));
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|