You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
UE-9331 - Missing icons for BP component classes when restarting editor Moved the code to work out the correct class icon for a Blueprint or asset data into FClassIconFinder so that it can be shared between the asset thumbnails and the components context menu. Added an extra IconClass member to FComponentClassComboEntry so that unloaded Blueprints can set this appropriately despite having a null ComponentClass. [CL 2451813 by Jamie Dale in Main branch]
475 lines
14 KiB
C++
475 lines
14 KiB
C++
// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "UnrealEd.h"
|
|
|
|
#include "BlueprintGraphDefinitions.h"
|
|
#include "ClassIconFinder.h"
|
|
#include "ComponentAssetBroker.h"
|
|
#include "ComponentTypeRegistry.h"
|
|
#include "EditorClassUtils.h"
|
|
#include "Engine/Selection.h"
|
|
#include "IDocumentation.h"
|
|
#include "KismetEditorUtilities.h"
|
|
#include "SComponentClassCombo.h"
|
|
#include "SlateBasics.h"
|
|
#include "SListViewSelectorDropdownMenu.h"
|
|
#include "SSearchBox.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "ComponentClassCombo"
|
|
|
|
FString FComponentClassComboEntry::GetClassName() const
|
|
{
|
|
return ComponentClass != nullptr ? ComponentClass->GetDisplayNameText().ToString() : ComponentName;
|
|
}
|
|
|
|
void SComponentClassCombo::Construct(const FArguments& InArgs)
|
|
{
|
|
PrevSelectedIndex = INDEX_NONE;
|
|
OnComponentClassSelected = InArgs._OnComponentClassSelected;
|
|
|
|
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(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(const FString& InSearchText)
|
|
{
|
|
if ( InSearchText.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 ( FriendlyComponentName.Contains( InSearchText, ESearchCase::IgnoreCase ) )
|
|
{
|
|
// 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 CurrentSearchString;
|
|
}
|
|
|
|
void SComponentClassCombo::OnSearchBoxTextChanged( const FText& InSearchText )
|
|
{
|
|
CurrentSearchString = InSearchText;
|
|
|
|
// Generate a filtered list
|
|
GenerateFilteredComponentList(CurrentSearchString.ToString());
|
|
|
|
// 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( FEditorClassUtils::GetTooltip(Entry->GetComponentClass()) )
|
|
[
|
|
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( FClassIconFinder::FindIconForClass( 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(CurrentSearchString.ToString());
|
|
}
|
|
|
|
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;
|
|
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);
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|