// Copyright Epic Games, Inc. All Rights Reserved. #include "MuCOE/SMutableSearchComboBox.h" #include "MuCOE/CustomizableObjectEditorUtilities.h" #include "MuCOE/CustomizableObjectEditorStyle.h" #include "MuCOE/GraphTraversal.h" #include "MuCOE/UnrealEditorPortabilityHelpers.h" #include "PropertyCustomizationHelpers.h" #include "Widgets/Input/SComboBox.h" #include "Widgets/Input/STextComboBox.h" #include "Widgets/Input/SSearchBox.h" #include "Engine/SkeletalMesh.h" #include "Misc/Attribute.h" #include "DetailLayoutBuilder.h" // For font: TODO: move to argument #define LOCTEXT_NAMESPACE "CustomizableObjectDetails" void SMutableSearchComboBox::Construct(const FArguments& InArgs) { check(InArgs._ComboBoxStyle); ItemStyle = InArgs._ItemStyle; MenuRowPadding = InArgs._ComboBoxStyle->MenuRowPadding; bAllowAddNewOptions = InArgs._AllowAddNewOptions; // Work out which values we should use based on whether we were given an override, or should use the style's version OurComboButtonStyle = InArgs._ComboBoxStyle->ComboButtonStyle; if (InArgs._MenuButtonBrush) { OurComboButtonStyle.DownArrowImage = *InArgs._MenuButtonBrush; } const FButtonStyle* const OurButtonStyle = InArgs._ButtonStyle ? InArgs._ButtonStyle : &OurComboButtonStyle.ButtonStyle; this->OnSelectionChanged = InArgs._OnSelectionChanged; OptionsSource = InArgs._OptionsSource; TAttribute SearchVisibility = InArgs._SearchVisibility; const EVisibility CurrentSearchVisibility = SearchVisibility.Get(); TSharedRef ComboBoxMenuContent = SNew(SBox) .MaxDesiredHeight(InArgs._MaxListHeight) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SAssignNew(this->SearchField, SEditableTextBox) .HintText(bAllowAddNewOptions ? LOCTEXT("SearchOrAdd", "Search or add...") : LOCTEXT("Search", "Search...")) .OnTextChanged(this, &SMutableSearchComboBox::OnSearchTextChanged) .OnTextCommitted(this, &SMutableSearchComboBox::OnSearchTextCommitted) .Visibility(SearchVisibility) ] + SVerticalBox::Slot() [ SAssignNew(this->ComboTreeView, SComboTreeType) .TreeItemsSource(&FilteredRootOptionsSource) .OnGenerateRow(this, &SMutableSearchComboBox::GenerateMenuItemRow) .OnGetChildren(this, &SMutableSearchComboBox::OnGetChildren) .OnSelectionChanged(this, &SMutableSearchComboBox::OnSelectionChanged_Internal) .OnKeyDownHandler(this, &SMutableSearchComboBox::OnKeyDownHandler) .SelectionMode(ESelectionMode::Single) ] ]; // Set up content TSharedPtr ButtonContent = InArgs._Content.Widget; if (InArgs._Content.Widget == SNullWidget::NullWidget) { SAssignNew(ButtonContent, STextBlock); } SComboButton::Construct(SComboButton::FArguments() .ComboButtonStyle(&OurComboButtonStyle) .ButtonStyle(OurButtonStyle) .Method(InArgs._Method) .ButtonContent() [ ButtonContent.ToSharedRef() ] .MenuContent() [ ComboBoxMenuContent ] .ContentPadding(InArgs._ContentPadding) .ForegroundColor(InArgs._ForegroundColor) .OnMenuOpenChanged(this, &SMutableSearchComboBox::OnMenuOpenChanged) .IsFocusable(true) ); if (CurrentSearchVisibility == EVisibility::Visible) { SetMenuContentWidgetToFocus(SearchField); } else { SetMenuContentWidgetToFocus(ComboTreeView); } } void SMutableSearchComboBox::RefreshOptions() { // Need to refresh filtered list whenever options change FilteredOptionsSource.Reset(); if (SearchText.IsEmpty()) { FilteredOptionsSource = *OptionsSource; } else { TArray SearchTokens; SearchText.ToString().ParseIntoArrayWS(SearchTokens); for (const TSharedRef& Option : *OptionsSource) { bool bAllTokensMatch = true; for (const FString& SearchToken : SearchTokens) { if (Option->DisplayOption.Find(SearchToken, ESearchCase::Type::IgnoreCase) == INDEX_NONE) { bAllTokensMatch = false; break; } } if (bAllTokensMatch) { FilteredOptionsSource.Add(Option); } } bool bFullMatch = false; FString SearchString = SearchText.ToString(); for (const TSharedRef& Option : *OptionsSource) { if (Option->DisplayOption == SearchString && !Option->ActualOption.IsEmpty()) { bFullMatch = true; break; } } if ( bAllowAddNewOptions && !bFullMatch && !SearchText.IsEmpty() ) { FFilteredOption Data; Data.ActualOption = SearchString; Data.DisplayOption = FString::Printf(TEXT("Add new tag (%s)"), *SearchString); FilteredOptionsSource.Insert(MakeShared(Data),0); } // Ensure filtered options parents are added for (int32 OptionIndex = 0; OptionIndex < FilteredOptionsSource.Num(); ++OptionIndex) { TSharedPtr Parent = FilteredOptionsSource[OptionIndex]->Parent; if (Parent) { FilteredOptionsSource.AddUnique(Parent.ToSharedRef()); while (Parent) { ComboTreeView->SetItemExpansion(Parent.ToSharedRef(), true); Parent = Parent->Parent; } } } } FilteredRootOptionsSource.SetNum(0, EAllowShrinking::No); FilteredRootOptionsSource.Reserve(FilteredOptionsSource.Num()); for (const TSharedRef& Option : FilteredOptionsSource) { if (!Option->Parent.IsValid()) { FilteredRootOptionsSource.Add(Option); } } if (ComboTreeView) { ComboTreeView->RequestTreeRefresh(); } } TSharedRef SMutableSearchComboBox::GenerateMenuItemRow(TSharedRef InItem, const TSharedRef& OwnerTable) { FSlateColor LabelColor = InItem->ActualOption.IsEmpty() ? FSlateColor::UseSubduedForeground() : FSlateColor::UseForeground(); return SNew(SComboRow>, OwnerTable) [ SNew(STextBlock) .Text(FText::FromString(InItem->DisplayOption)) .Font(IDetailLayoutBuilder::GetDetailFont()) .ColorAndOpacity(LabelColor) ]; } void SMutableSearchComboBox::OnGetChildren(TSharedRef InItem, TArray>& OutChildren) { // TODO: Optimize if necessary for (const TSharedRef& Option : FilteredOptionsSource) { if (Option->Parent==InItem) { OutChildren.Add(Option); } } } void SMutableSearchComboBox::OnMenuOpenChanged(bool bOpen) { if (bOpen) { RefreshOptions(); } else { // Set focus back to ComboBox for users focusing the ListView that just closed FSlateApplication::Get().ForEachUser([this](FSlateUser& User) { TSharedRef ThisRef = this->AsShared(); if (User.IsWidgetInFocusPath(this->ComboTreeView)) { User.SetFocus(ThisRef); } }); } } void SMutableSearchComboBox::OnSelectionChanged_Internal(TSharedPtr ProposedSelection, ESelectInfo::Type SelectInfo) { if (!ProposedSelection) { return; } // If the proposed selection is not a valid element (it is a hierarchy label) if (ProposedSelection->ActualOption.IsEmpty()) { return; } // Close combo as long as the selection wasn't from navigation if (SelectInfo != ESelectInfo::OnNavigation) { OnSelectionChanged.ExecuteIfBound(FText::FromString(ProposedSelection->ActualOption)); this->SetIsOpen(false); } } void SMutableSearchComboBox::OnSearchTextChanged(const FText& ChangedText) { SearchText = ChangedText; RefreshOptions(); } void SMutableSearchComboBox::OnSearchTextCommitted(const FText& InText, ETextCommit::Type InCommitType) { if ((InCommitType == ETextCommit::Type::OnEnter) && !FilteredOptionsSource.IsEmpty()) { TSharedPtr Selected; for (const TSharedRef& Option : FilteredOptionsSource) { if (Option->ActualOption == InText.ToString()) { Selected = Option; break; } } if (!Selected) { for (const TSharedRef& Option : FilteredOptionsSource) { if (Option->DisplayOption == InText.ToString()) { Selected = Option; break; } } } if (!Selected) { Selected = FilteredOptionsSource[0]; } ComboTreeView->SetSelection(Selected.ToSharedRef(), ESelectInfo::OnKeyPress); } } FReply SMutableSearchComboBox::OnKeyDownHandler(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { if (InKeyEvent.GetKey() == EKeys::Enter) { // Select the first selected item on hitting enter TArray> SelectedItems = ComboTreeView->GetSelectedItems(); if (SelectedItems.Num() > 0) { OnSelectionChanged_Internal(SelectedItems[0], ESelectInfo::OnKeyPress); return FReply::Handled(); } } return FReply::Unhandled(); } #undef LOCTEXT_NAMESPACE