Files
UnrealEngine/Engine/Source/Editor/MaterialEditor/Private/FindInMaterial.cpp
2025-05-18 13:04:45 +08:00

465 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "FindInMaterial.h"
#include "Layout/WidgetPath.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Images/SImage.h"
#include "Styling/AppStyle.h"
#include "MaterialGraph/MaterialGraphNode_Comment.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "Materials/MaterialExpressionComment.h"
#include "Widgets/Input/SSearchBox.h"
#define LOCTEXT_NAMESPACE "FindInMaterial"
//////////////////////////////////////////////////////////////////////////
// FFindInMaterialResult
FFindInMaterialResult::FFindInMaterialResult(const FString& InValue)
:Value(InValue), DuplicationIndex(0), Class(NULL), Pin(), GraphNode(NULL)
{
}
FFindInMaterialResult::FFindInMaterialResult(const FString& InValue, TSharedPtr<FFindInMaterialResult>& InParent, UClass* InClass, int InDuplicationIndex)
: Parent(InParent), Value(InValue), DuplicationIndex(InDuplicationIndex), Class(InClass), Pin(), GraphNode(NULL)
{
}
FFindInMaterialResult::FFindInMaterialResult(const FString& InValue, TSharedPtr<FFindInMaterialResult>& InParent, UEdGraphPin* InPin)
: Parent(InParent), Value(InValue), DuplicationIndex(0), Class(nullptr), Pin(InPin), GraphNode(NULL)
{
}
FFindInMaterialResult::FFindInMaterialResult(const FString& InValue, TSharedPtr<FFindInMaterialResult>& InParent, UEdGraphNode* InNode)
: Parent(InParent), Value(InValue), DuplicationIndex(0), Class(InNode->GetClass()), Pin(), GraphNode(InNode)
{
if (GraphNode.IsValid())
{
CommentText = GraphNode->NodeComment;
}
}
FReply FFindInMaterialResult::OnClick(TWeakPtr<class FMaterialEditor> MaterialEditor)
{
if (GraphNode.IsValid())
{
MaterialEditor.Pin()->JumpToNode(GraphNode.Get());
}
else if (UEdGraphPin* ResolvedPin = Pin.Get())
{
MaterialEditor.Pin()->JumpToNode(ResolvedPin->GetOwningNode());
}
return FReply::Handled();
}
FText FFindInMaterialResult::GetCategory() const
{
if (Class == nullptr && Pin.Get())
{
return LOCTEXT("PinCategory", "Pin");
}
return LOCTEXT("NodeCategory", "Node");
}
TSharedRef<SWidget> FFindInMaterialResult::CreateIcon() const
{
FSlateColor IconColor = FSlateColor::UseForeground();
const FSlateBrush* Brush = NULL;
if (UEdGraphPin* ResolvedPin = Pin.Get())
{
if (ResolvedPin->PinType.IsArray())
{
Brush = FAppStyle::GetBrush(TEXT("GraphEditor.ArrayPinIcon"));
}
else if (ResolvedPin->PinType.bIsReference)
{
Brush = FAppStyle::GetBrush(TEXT("GraphEditor.RefPinIcon"));
}
else
{
Brush = FAppStyle::GetBrush(TEXT("GraphEditor.PinIcon"));
}
const UEdGraphSchema* Schema = ResolvedPin->GetSchema();
IconColor = Schema->GetPinTypeColor(ResolvedPin->PinType);
}
else if (GraphNode.IsValid())
{
Brush = FAppStyle::GetBrush(TEXT("GraphEditor.NodeGlyph"));
}
return SNew(SImage)
.Image(Brush)
.ColorAndOpacity(IconColor)
.ToolTipText(GetCategory());
}
FString FFindInMaterialResult::GetCommentText() const
{
return CommentText;
}
FString FFindInMaterialResult::GetValueText() const
{
FString DisplayVal;
if (UEdGraphPin* ResolvedPin = Pin.Get())
{
if ((!ResolvedPin->DefaultValue.IsEmpty() || !ResolvedPin->AutogeneratedDefaultValue.IsEmpty() || ResolvedPin->DefaultObject || !ResolvedPin->DefaultTextValue.IsEmpty())
&& (ResolvedPin->Direction == EGPD_Input)
&& (ResolvedPin->LinkedTo.Num() == 0))
{
if (ResolvedPin->DefaultObject)
{
const AActor* DefaultActor = Cast<AActor>(ResolvedPin->DefaultObject);
DisplayVal = FString::Printf(TEXT("(%s)"), DefaultActor ? *DefaultActor->GetActorLabel() : *ResolvedPin->DefaultObject->GetName());
}
else if (!ResolvedPin->DefaultValue.IsEmpty())
{
DisplayVal = FString::Printf(TEXT("(%s)"), *ResolvedPin->DefaultValue);
}
else if (!ResolvedPin->AutogeneratedDefaultValue.IsEmpty())
{
DisplayVal = FString::Printf(TEXT("(%s)"), *ResolvedPin->AutogeneratedDefaultValue);
}
else if (!ResolvedPin->DefaultTextValue.IsEmpty())
{
DisplayVal = FString::Printf(TEXT("(%s)"), *ResolvedPin->DefaultTextValue.ToString());
}
}
}
return DisplayVal;
}
//////////////////////////////////////////////////////////////////////////
// SFindInMaterial
void SFindInMaterial::Construct(const FArguments& InArgs, TSharedPtr<FMaterialEditor> InMaterialEditor)
{
MaterialEditorPtr = InMaterialEditor;
this->ChildSlot
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.FillWidth(1)
[
SAssignNew(SearchTextField, SSearchBox)
.HintText(LOCTEXT("GraphSearchHint", "Search"))
.OnTextChanged(this, &SFindInMaterial::OnSearchTextChanged)
.OnTextCommitted(this, &SFindInMaterial::OnSearchTextCommitted)
]
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(0.f, 4.f, 0.f, 0.f)
[
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("Menu.Background"))
[
SAssignNew(TreeView, STreeViewType)
.TreeItemsSource(&ItemsFound)
.OnGenerateRow(this, &SFindInMaterial::OnGenerateRow)
.OnGetChildren(this, &SFindInMaterial::OnGetChildren)
.OnSelectionChanged(this, &SFindInMaterial::OnTreeSelectionChanged)
.OnMouseButtonDoubleClick(this, &SFindInMaterial::OnTreeSelectionDoubleClick)
.SelectionMode(ESelectionMode::Multi)
]
]
];
}
void SFindInMaterial::FocusForUse()
{
// NOTE: Careful, GeneratePathToWidget can be reentrant in that it can call visibility delegates and such
FWidgetPath FilterTextBoxWidgetPath;
FSlateApplication::Get().GeneratePathToWidgetUnchecked(SearchTextField.ToSharedRef(), FilterTextBoxWidgetPath);
// Set keyboard focus directly
FSlateApplication::Get().SetKeyboardFocus(FilterTextBoxWidgetPath, EFocusCause::SetDirectly);
}
void SFindInMaterial::PopulateSearchItems(const TArray<UEdGraphNode*>& Nodes)
{
if (Nodes.Num() == 0)
{
return;
}
// Reset to default state.
SearchTextField->SetText(FText::GetEmpty());
RootSearchResult.Reset();
RootSearchResult = FSearchResult(new FFindInMaterialResult(FString("MaterialTreeRoot")));
ItemsFound.Empty();
for (UEdGraphNode* Node : Nodes)
{
const FString NodeName = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
const FString NodeType = Node->GetNodeTitle(ENodeTitleType::ListView).ToString();
ItemsFound.Add(FSearchResult(new FFindInMaterialResult(NodeName == NodeType ? NodeName : NodeName + " - " + NodeType, RootSearchResult, Node)));
}
// Refresh the tree and expand items.
TreeView->RequestTreeRefresh();
for (auto It(ItemsFound.CreateIterator()); It; ++It)
{
TreeView->SetItemExpansion(*It, true);
}
}
void SFindInMaterial::OnSearchTextChanged(const FText& Text)
{
SearchValue = Text.ToString();
}
void SFindInMaterial::OnSearchTextCommitted(const FText& Text, ETextCommit::Type CommitType)
{
if (CommitType == ETextCommit::OnEnter)
{
InitiateSearch();
}
}
void SFindInMaterial::InitiateSearch()
{
TArray<FString> Tokens;
if (SearchValue.Contains("\"") && SearchValue.ParseIntoArray(Tokens, TEXT("\""), true) > 0)
{
for (auto &TokenIt : Tokens)
{
// we have the token, we don't need the quotes anymore, they'll just confused the comparison later on
TokenIt = TokenIt.TrimQuotes();
// We remove the spaces as all later comparison strings will also be de-spaced
TokenIt = TokenIt.Replace(TEXT(" "), TEXT(""));
}
// due to being able to handle multiple quoted blocks like ("Make Epic" "Game Now") we can end up with
// and empty string between (" ") blocks so this simply removes them
struct FRemoveMatchingStrings
{
bool operator()(const FString& RemovalCandidate) const
{
return RemovalCandidate.IsEmpty();
}
};
Tokens.RemoveAll(FRemoveMatchingStrings());
}
else
{
// unquoted search equivalent to a match-any-of search
SearchValue.ParseIntoArray(Tokens, TEXT(" "), true);
}
for (auto It(ItemsFound.CreateIterator()); It; ++It)
{
TreeView->SetItemExpansion(*It, false);
}
ItemsFound.Empty();
if (Tokens.Num() > 0)
{
HighlightText = FText::FromString(SearchValue);
MatchTokens(Tokens);
}
// Insert a fake result to inform user if none found
if (ItemsFound.Num() == 0)
{
ItemsFound.Add(FSearchResult(new FFindInMaterialResult(LOCTEXT("MaterialSearchNoResults", "No Results found").ToString())));
}
TreeView->RequestTreeRefresh();
for (auto It(ItemsFound.CreateIterator()); It; ++It)
{
TreeView->SetItemExpansion(*It, true);
}
}
void SFindInMaterial::MatchTokens(const TArray<FString> &Tokens)
{
RootSearchResult.Reset();
UEdGraph* Graph = MaterialEditorPtr.Pin()->Material->MaterialGraph;
MatchTokensInGraph(Graph, Tokens);
}
void SFindInMaterial::MatchTokensInGraph(const UEdGraph* Graph, const TArray<FString>& Tokens)
{
if (Graph == NULL)
{
return;
}
RootSearchResult = FSearchResult(new FFindInMaterialResult(FString("MaterialTreeRoot")));
for (auto It(Graph->Nodes.CreateConstIterator()); It; ++It)
{
UEdGraphNode* Node = *It;
const FString NodeName = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
const FString NodeType = Node->GetNodeTitle(ENodeTitleType::ListView).ToString();
FSearchResult NodeResult(new FFindInMaterialResult(NodeName == NodeType ? NodeName : NodeName + " - " + NodeType, RootSearchResult, Node));
FString NodeSearchString = NodeName + NodeType + Node->NodeComment;
NodeSearchString = NodeSearchString.Replace(TEXT(" "), TEXT(""));
bool bNodeMatchesSearch = StringMatchesSearchTokens(Tokens, NodeSearchString);
// Use old Material Expression search functions too
if (!bNodeMatchesSearch)
{
bool bMatchesAllTokens = true;
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
{
for (int32 Index = 0; Index < Tokens.Num(); ++Index)
{
if (!MatNode->MaterialExpression->MatchesSearchQuery(*Tokens[Index]))
{
bMatchesAllTokens = false;
break;
}
}
}
else if (UMaterialGraphNode_Comment* MatComment = Cast<UMaterialGraphNode_Comment>(Node))
{
for (int32 Index = 0; Index < Tokens.Num(); ++Index)
{
if (!MatComment->MaterialExpressionComment->MatchesSearchQuery(*Tokens[Index]))
{
bMatchesAllTokens = false;
break;
}
}
}
else
{
bMatchesAllTokens = false;
}
if (bMatchesAllTokens)
{
bNodeMatchesSearch = true;
}
}
for (TArray<UEdGraphPin*>::TIterator PinIt(Node->Pins); PinIt; ++PinIt)
{
UEdGraphPin* Pin = *PinIt;
if (Pin && Pin->PinFriendlyName.CompareTo(FText::FromString(TEXT(" "))) != 0)
{
FText PinName = Pin->GetSchema()->GetPinDisplayName(Pin);
FString PinSearchString = Pin->PinName.ToString() + Pin->PinFriendlyName.ToString() + Pin->DefaultValue + Pin->PinType.PinCategory.ToString() + Pin->PinType.PinSubCategory.ToString() + (Pin->PinType.PinSubCategoryObject.IsValid() ? Pin->PinType.PinSubCategoryObject.Get()->GetFullName() : TEXT(""));
PinSearchString = PinSearchString.Replace(TEXT(" "), TEXT(""));
if (StringMatchesSearchTokens(Tokens, PinSearchString))
{
FSearchResult PinResult(new FFindInMaterialResult(PinName.ToString(), NodeResult, Pin));
NodeResult->Children.Add(PinResult);
}
}
}
if ((NodeResult->Children.Num() > 0) || bNodeMatchesSearch)
{
ItemsFound.Add(NodeResult);
}
}
for (const UEdGraph* Subgraph : Graph->SubGraphs)
{
MatchTokensInGraph(Subgraph, Tokens);
}
}
TSharedRef<ITableRow> SFindInMaterial::OnGenerateRow(FSearchResult InItem, const TSharedRef<STableViewBase>& OwnerTable)
{
FString CommentText = InItem->GetCommentText();
return SNew(STableRow< TSharedPtr<FFindInMaterialResult> >, OwnerTable)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
InItem->CreateIcon()
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(2, 0)
[
SNew(STextBlock)
.Text(FText::FromString(InItem->Value))
.HighlightText(HighlightText)
.ToolTipText(FText::Format(LOCTEXT("MaterialResultSearchToolTipFmt", "{0} : {1}"), InItem->GetCategory(), FText::FromString(InItem->Value)))
]
+ SHorizontalBox::Slot()
.FillWidth(1)
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
.Padding(2, 0)
[
SNew(STextBlock)
.Text(FText::FromString(InItem->GetValueText()))
.HighlightText(HighlightText)
]
+ SHorizontalBox::Slot()
.FillWidth(1)
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
.Padding(2, 0)
[
SNew(STextBlock)
.Text(CommentText.IsEmpty() ? FText::GetEmpty() : FText::Format(LOCTEXT("NodeCommentFmt", "Node Comment:[{0}]"), FText::FromString(CommentText)))
.ColorAndOpacity(FLinearColor::Yellow)
.HighlightText(HighlightText)
]
];
}
void SFindInMaterial::OnGetChildren(FSearchResult InItem, TArray< FSearchResult >& OutChildren)
{
OutChildren += InItem->Children;
}
void SFindInMaterial::OnTreeSelectionChanged(FSearchResult Item, ESelectInfo::Type)
{
if (Item.IsValid())
{
Item->OnClick(MaterialEditorPtr);
}
}
void SFindInMaterial::OnTreeSelectionDoubleClick(FSearchResult Item)
{
if (Item.IsValid())
{
Item->OnClick(MaterialEditorPtr);
}
}
bool SFindInMaterial::StringMatchesSearchTokens(const TArray<FString>& Tokens, const FString& ComparisonString)
{
bool bFoundAllTokens = true;
//search the entry for each token, it must have all of them to pass
for (auto TokIT(Tokens.CreateConstIterator()); TokIT; ++TokIT)
{
const FString& Token = *TokIT;
if (!ComparisonString.Contains(Token))
{
bFoundAllTokens = false;
break;
}
}
return bFoundAllTokens;
}
/////////////////////////////////////////////////////
#undef LOCTEXT_NAMESPACE