// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. #include "GraphEditorCommon.h" #include "SGraphEditorActionMenu.h" #include "GraphActionNode.h" #include "SScrollBorder.h" #include "IDocumentation.h" #include "EditorCategoryUtils.h" #include "SSearchBox.h" #include "SInlineEditableTextBlock.h" #define LOCTEXT_NAMESPACE "GraphActionMenu" namespace GraphActionMenuHelpers { bool ActionMatchesName(const FEdGraphSchemaAction* InGraphAction, const FName& ItemName) { bool bCheck = false; bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2Var::StaticGetTypeId() && ((FEdGraphSchemaAction_K2Var*)InGraphAction)->GetVariableName() == ItemName); bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2LocalVar::StaticGetTypeId() && ((FEdGraphSchemaAction_K2LocalVar*)InGraphAction)->GetVariableName() == ItemName); bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2Graph::StaticGetTypeId() && ((FEdGraphSchemaAction_K2Graph*)InGraphAction)->EdGraph && ((FEdGraphSchemaAction_K2Graph*)InGraphAction)->EdGraph->GetFName() == ItemName); bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2Enum::StaticGetTypeId() && ((FEdGraphSchemaAction_K2Enum*)InGraphAction)->GetPathName() == ItemName); bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2Struct::StaticGetTypeId() && ((FEdGraphSchemaAction_K2Struct*)InGraphAction)->GetPathName() == ItemName); bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2Delegate::StaticGetTypeId() && ((FEdGraphSchemaAction_K2Delegate*)InGraphAction)->GetDelegateName() == ItemName); bCheck |= (InGraphAction->GetTypeId() == FEdGraphSchemaAction_K2TargetNode::StaticGetTypeId() && ((FEdGraphSchemaAction_K2TargetNode*)InGraphAction)->NodeTemplate->GetNodeTitle(ENodeTitleType::EditableTitle).ToString() == ItemName.ToString()); return bCheck; } } void SDefaultGraphActionWidget::Construct(const FArguments& InArgs, const FCreateWidgetForActionData* InCreateData) { ActionPtr = InCreateData->Action; MouseButtonDownDelegate = InCreateData->MouseButtonDownDelegate; this->ChildSlot [ SNew(SHorizontalBox) .ToolTipText( FText::FromString(InCreateData->Action->TooltipDescription) ) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(STextBlock) .Font(FSlateFontInfo( FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Regular.ttf"), 9 )) .Text(InCreateData->Action->MenuDescription) .HighlightText(InArgs._HighlightText) ] ]; } FReply SDefaultGraphActionWidget::OnMouseButtonDown( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { if( MouseButtonDownDelegate.Execute( ActionPtr ) ) { return FReply::Handled(); } return FReply::Unhandled(); } ////////////////////////////////////////////////////////////////////////// class SGraphActionCategoryWidget : public SCompoundWidget { SLATE_BEGIN_ARGS( SGraphActionCategoryWidget ) {} SLATE_ATTRIBUTE( FText, HighlightText ) SLATE_EVENT( FOnTextCommitted, OnTextCommitted ) SLATE_EVENT( FIsSelected, IsSelected ) SLATE_ATTRIBUTE( bool, IsReadOnly ) SLATE_END_ARGS() TWeakPtr ActionNode; public: TWeakPtr InlineWidget; void Construct( const FArguments& InArgs, TSharedPtr InActionNode ) { ActionNode = InActionNode; FText CategoryTooltip; FString CategoryLink, CategoryExcerpt; FEditorCategoryUtils::GetCategoryTooltipInfo(*InActionNode->GetDisplayName().ToString(), CategoryTooltip, CategoryLink, CategoryExcerpt); TSharedRef ToolTipWidget = IDocumentation::Get()->CreateToolTip(CategoryTooltip, NULL, CategoryLink, CategoryExcerpt); this->ChildSlot [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .VAlign(VAlign_Center) [ SAssignNew(InlineWidget, SInlineEditableTextBlock) .Font( FSlateFontInfo( FPaths::EngineContentDir() / TEXT("Slate/Fonts/Roboto-Bold.ttf"), 9 ) ) .Text( FEditorCategoryUtils::GetCategoryDisplayString(InActionNode->GetDisplayName()) ) .ToolTip( ToolTipWidget ) .HighlightText( InArgs._HighlightText ) .OnVerifyTextChanged( this, &SGraphActionCategoryWidget::OnVerifyTextChanged ) .OnTextCommitted( InArgs._OnTextCommitted ) .IsSelected( InArgs._IsSelected ) .IsReadOnly( InArgs._IsReadOnly ) ] ]; } // SWidget interface virtual FReply OnDrop( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) override { TSharedPtr GraphDropOp = DragDropEvent.GetOperationAs(); if (GraphDropOp.IsValid()) { GraphDropOp->DroppedOnCategory( ActionNode.Pin()->GetCategoryPath() ); return FReply::Handled(); } return FReply::Unhandled(); } virtual void OnDragEnter( const FGeometry& MyGeometry, const FDragDropEvent& DragDropEvent ) override { TSharedPtr GraphDropOp = DragDropEvent.GetOperationAs(); if (GraphDropOp.IsValid()) { GraphDropOp->SetHoveredCategoryName( ActionNode.Pin()->GetDisplayName().ToString() ); } } virtual void OnDragLeave( const FDragDropEvent& DragDropEvent ) override { TSharedPtr GraphDropOp = DragDropEvent.GetOperationAs(); if (GraphDropOp.IsValid()) { GraphDropOp->SetHoveredCategoryName( FString(TEXT("")) ); } } // End of SWidget interface /** Callback for the SInlineEditableTextBlock to verify the text before commit */ bool OnVerifyTextChanged(const FText& InText, FText& OutErrorMessage) { if(InText.ToString().Len() > NAME_SIZE) { OutErrorMessage = LOCTEXT("CategoryNameTooLong_Error", "Name too long!"); return false; } return true; } }; ////////////////////////////////////////////////////////////////////////// void SGraphActionMenu::Construct( const FArguments& InArgs, bool bIsReadOnly/* = true*/ ) { this->SelectedSuggestion = INDEX_NONE; this->bIgnoreUIUpdate = false; this->bAutoExpandActionMenu = InArgs._AutoExpandActionMenu; this->bShowFilterTextBox = InArgs._ShowFilterTextBox; this->bAlphaSortItems = InArgs._AlphaSortItems; this->OnActionSelected = InArgs._OnActionSelected; this->OnActionDoubleClicked = InArgs._OnActionDoubleClicked; this->OnActionDragged = InArgs._OnActionDragged; this->OnCategoryDragged = InArgs._OnCategoryDragged; this->OnCreateWidgetForAction = InArgs._OnCreateWidgetForAction; this->OnCreateCustomRowExpander = InArgs._OnCreateCustomRowExpander; this->OnCollectAllActions = InArgs._OnCollectAllActions; this->OnCategoryTextCommitted = InArgs._OnCategoryTextCommitted; this->OnCanRenameSelectedAction = InArgs._OnCanRenameSelectedAction; this->OnGetSectionTitle = InArgs._OnGetSectionTitle; this->FilteredRootAction = FGraphActionNode::NewRootNode(); this->OnActionMatchesName = InArgs._OnActionMatchesName; // If a delegate for filtering text is passed in, assign it so that it will be used instead of the built-in filter box if(InArgs._OnGetFilterText.IsBound()) { this->OnGetFilterText = InArgs._OnGetFilterText; } TreeView = SNew(STreeView< TSharedPtr >) .ItemHeight(24) .TreeItemsSource(&(this->FilteredRootAction->Children)) .OnGenerateRow(this, &SGraphActionMenu::MakeWidget, bIsReadOnly) .OnSelectionChanged(this, &SGraphActionMenu::OnItemSelected) .OnMouseButtonDoubleClick(this, &SGraphActionMenu::OnItemDoubleClicked) .OnContextMenuOpening(InArgs._OnContextMenuOpening) .OnGetChildren(this, &SGraphActionMenu::OnGetChildrenForCategory) .SelectionMode(ESelectionMode::Single) .OnItemScrolledIntoView(this, &SGraphActionMenu::OnItemScrolledIntoView); this->ChildSlot [ SNew(SVerticalBox) // FILTER BOX +SVerticalBox::Slot() .AutoHeight() [ SAssignNew(FilterTextBox, SSearchBox) // If there is an external filter delegate, do not display this filter box .Visibility(InArgs._OnGetFilterText.IsBound()? EVisibility::Collapsed : EVisibility::Visible) .OnTextChanged( this, &SGraphActionMenu::OnFilterTextChanged ) .OnTextCommitted( this, &SGraphActionMenu::OnFilterTextCommitted ) ] // ACTION LIST +SVerticalBox::Slot() .Padding(FMargin(0.0f, 2.0f, 0.0f, 0.0f)) .FillHeight(1.f) [ SNew(SScrollBorder, TreeView.ToSharedRef()) [ TreeView.ToSharedRef() ] ] ]; if (!InArgs._ShowFilterTextBox) { FilterTextBox->SetVisibility(EVisibility::Collapsed); } // Get all actions. RefreshAllActions(false); } void SGraphActionMenu::RefreshAllActions(bool bPreserveExpansion, bool bHandleOnSelectionEvent/*=true*/) { // Save Selection (of only the first selected thing) TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); TSharedPtr SelectedAction = SelectedNodes.Num() > 0 ? SelectedNodes[0] : NULL; AllActions.Empty(); OnCollectAllActions.ExecuteIfBound(AllActions); GenerateFilteredItems(bPreserveExpansion); // Re-apply selection #0 if possible if (SelectedAction.IsValid()) { // Clear the selection, we will be re-selecting the previous action TreeView->ClearSelection(); if(bHandleOnSelectionEvent) { SelectItemByName(*SelectedAction->GetDisplayName().ToString(), ESelectInfo::OnMouseClick, SelectedAction->SectionID, SelectedNodes[0]->IsCategoryNode()); } else { // If we do not want to handle the selection, set it directly so it will reselect the item but not handle the event. SelectItemByName(*SelectedAction->GetDisplayName().ToString(), ESelectInfo::Direct, SelectedAction->SectionID, SelectedNodes[0]->IsCategoryNode()); } } } TSharedRef SGraphActionMenu::GetFilterTextBox() { return FilterTextBox.ToSharedRef(); } void SGraphActionMenu::GetSelectedActions(TArray< TSharedPtr >& OutSelectedActions) const { OutSelectedActions.Empty(); TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); if(SelectedNodes.Num() > 0) { for ( int32 NodeIndex = 0; NodeIndex < SelectedNodes.Num(); NodeIndex++ ) { OutSelectedActions.Append( SelectedNodes[NodeIndex]->Actions ); } } } void SGraphActionMenu::OnRequestRenameOnActionNode() { TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); if(SelectedNodes.Num() > 0) { if (!SelectedNodes[0]->BroadcastRenameRequest()) { TreeView->RequestScrollIntoView(SelectedNodes[0]); } } } bool SGraphActionMenu::CanRequestRenameOnActionNode() const { TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); if(SelectedNodes.Num() == 1 && OnCanRenameSelectedAction.IsBound()) { return OnCanRenameSelectedAction.Execute(SelectedNodes[0]); } return false; } FString SGraphActionMenu::GetSelectedCategoryName() const { TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); return (SelectedNodes.Num() > 0) ? SelectedNodes[0]->GetDisplayName().ToString() : FString(); } void SGraphActionMenu::GetSelectedCategorySubActions(TArray>& OutActions) const { TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); for ( int32 SelectionIndex = 0; SelectionIndex < SelectedNodes.Num(); SelectionIndex++ ) { if ( SelectedNodes[SelectionIndex].IsValid() ) { GetCategorySubActions(SelectedNodes[SelectionIndex], OutActions); } } } void SGraphActionMenu::GetCategorySubActions(TWeakPtr InAction, TArray>& OutActions) const { if(InAction.IsValid()) { TSharedPtr CategoryNode = InAction.Pin(); TArray> Children; CategoryNode->GetLeafNodes(Children); for (int32 i = 0; i < Children.Num(); ++i) { TSharedPtr CurrentChild = Children[i]; if (CurrentChild.IsValid() && CurrentChild->IsActionNode()) { for ( int32 ActionIndex = 0; ActionIndex != CurrentChild->Actions.Num(); ActionIndex++ ) { OutActions.Add(CurrentChild->Actions[ActionIndex]); } } } } } bool SGraphActionMenu::SelectItemByName(const FName& ItemName, ESelectInfo::Type SelectInfo, int32 SectionId/* = INDEX_NONE */, bool bIsCategory/* = false*/) { if (ItemName != NAME_None) { TSharedPtr SelectionNode; TArray> GraphNodes; FilteredRootAction->GetAllNodes(GraphNodes); for (int32 i = 0; i < GraphNodes.Num() && !SelectionNode.IsValid(); ++i) { TSharedPtr CurrentGraphNode = GraphNodes[i]; FEdGraphSchemaAction* GraphAction = CurrentGraphNode->GetPrimaryAction().Get(); // If the user is attempting to select a category, make sure it's a category if( CurrentGraphNode->IsCategoryNode() == bIsCategory ) { if(SectionId == INDEX_NONE || CurrentGraphNode->SectionID == SectionId) { if (GraphAction) { if ((OnActionMatchesName.IsBound() && OnActionMatchesName.Execute(GraphAction, ItemName)) || GraphActionMenuHelpers::ActionMatchesName(GraphAction, ItemName)) { SelectionNode = GraphNodes[i]; break; } } if (CurrentGraphNode->GetDisplayName().ToString() == FName::NameToDisplayString(ItemName.ToString(), false)) { SelectionNode = CurrentGraphNode; break; } } } // One of the children may match for(int32 ChildIdx = 0; ChildIdx < CurrentGraphNode->Children.Num() && !SelectionNode.IsValid(); ++ChildIdx) { TSharedPtr CurrentChildNode = CurrentGraphNode->Children[ChildIdx]; for ( int32 ActionIndex = 0; ActionIndex < CurrentChildNode->Actions.Num(); ActionIndex++ ) { FEdGraphSchemaAction* ChildGraphAction = CurrentChildNode->Actions[ActionIndex].Get(); // If the user is attempting to select a category, make sure it's a category if( CurrentChildNode->IsCategoryNode() == bIsCategory ) { if(SectionId == INDEX_NONE || CurrentChildNode->SectionID == SectionId) { if(ChildGraphAction) { if ((OnActionMatchesName.IsBound() && OnActionMatchesName.Execute(GraphAction, ItemName)) || GraphActionMenuHelpers::ActionMatchesName(ChildGraphAction, ItemName)) { SelectionNode = GraphNodes[i]->Children[ChildIdx]; break; } } else if (CurrentChildNode->GetDisplayName().ToString() == FName::NameToDisplayString(ItemName.ToString(), false)) { SelectionNode = CurrentChildNode; break; } } } } } } if(SelectionNode.IsValid()) { TreeView->SetSelection(SelectionNode,SelectInfo); TreeView->RequestScrollIntoView(SelectionNode); return true; } } else { TreeView->ClearSelection(); return true; } return false; } void SGraphActionMenu::ExpandCategory(const FString& CategoryName) { if (CategoryName.Len()) { TArray> GraphNodes; FilteredRootAction->GetAllNodes(GraphNodes); for (int32 i = 0; i < GraphNodes.Num(); ++i) { if (GraphNodes[i]->GetDisplayName().ToString() == CategoryName) { GraphNodes[i]->ExpandAllChildren(TreeView); } } } } static bool CompareGraphActionNode(TSharedPtr A, TSharedPtr B) { check(A.IsValid()); check(B.IsValid()); // First check grouping is the same if (A->GetDisplayName().ToString() != B->GetDisplayName().ToString()) { return false; } if (A->HasValidAction() && B->HasValidAction()) { return A->GetPrimaryAction()->MenuDescription.CompareTo(B->GetPrimaryAction()->MenuDescription) == 0; } else if(!A->HasValidAction() && !B->HasValidAction()) { return true; } else { return false; } } template void RestoreExpansionState(TSharedPtr< STreeView > InTree, const TArray& ItemSource, const TSet& OldExpansionState, ComparisonType ComparisonFunction) { check(InTree.IsValid()); // Iterate over new tree items for(int32 ItemIdx=0; ItemIdx::TConstIterator OldExpansionIter(OldExpansionState); OldExpansionIter; ++OldExpansionIter) { const ItemType OldItem = *OldExpansionIter; // See if this matches this new item if(ComparisonFunction(OldItem, NewItem)) { // It does, so expand it InTree->SetItemExpansion(NewItem, true); } } } } void SGraphActionMenu::GenerateFilteredItems(bool bPreserveExpansion) { // First, save off current expansion state TSet< TSharedPtr > OldExpansionState; if(bPreserveExpansion) { TreeView->GetExpandedItems(OldExpansionState); } // Clear the filtered root action FilteredRootAction->ClearChildren(); // Trim and sanitized the filter text (so that it more likely matches the action descriptions) FString TrimmedFilterString = FText::TrimPrecedingAndTrailing(GetFilterText()).ToString(); // Tokenize the search box text into a set of terms; all of them must be present to pass the filter TArray FilterTerms; TrimmedFilterString.ParseIntoArray(/*out*/ &FilterTerms, TEXT(" "), true); // Generate a list of sanitized versions of the strings TArray SanitizedFilterTerms; for (int32 iFilters = 0; iFilters < FilterTerms.Num() ; iFilters++) { FString EachString = FName::NameToDisplayString( FilterTerms[iFilters], false ); EachString = EachString.Replace( TEXT( " " ), TEXT( "" ) ); SanitizedFilterTerms.Add( EachString ); } ensure( SanitizedFilterTerms.Num() == FilterTerms.Num() );// Both of these should match ! const bool bRequiresFiltering = FilterTerms.Num() > 0; int32 BestMatchCount = 0; int32 BestMatchIndex = INDEX_NONE; for (int32 CurTypeIndex=0; CurTypeIndex < AllActions.GetNumActions(); ++CurTypeIndex) { FGraphActionListBuilderBase::ActionGroup& CurrentAction = AllActions.GetAction( CurTypeIndex ); // If we're filtering, search check to see if we need to show this action bool bShowAction = true; int32 EachWeight = 0; if (bRequiresFiltering) { // Combine the actions string, separate with \n so terms don't run into each other, and remove the spaces (incase the user is searching for a variable) // In the case of groups containing multiple actions, they will have been created and added at the same place in the code, using the same description // and keywords, so we only need to use the first one for filtering. FString SearchText = CurrentAction.Actions[0]->MenuDescription.ToString() + LINE_TERMINATOR + CurrentAction.Actions[0]->GetSearchTitle().ToString() + LINE_TERMINATOR + CurrentAction.Actions[0]->Keywords + LINE_TERMINATOR +CurrentAction.Actions[0]->Category; SearchText = SearchText.Replace( TEXT( " " ), TEXT( "" ) ); // Get the 'weight' of this in relation to the filter EachWeight = GetActionFilteredWeight( CurrentAction, FilterTerms, SanitizedFilterTerms ); FString EachTermSanitized; for (int32 FilterIndex = 0; (FilterIndex < FilterTerms.Num()) && bShowAction; ++FilterIndex) { const bool bMatchesTerm = ( SearchText.Contains( FilterTerms[FilterIndex] ) || ( SearchText.Contains( SanitizedFilterTerms[FilterIndex] ) == true ) ); bShowAction = bShowAction && bMatchesTerm; } } if (bShowAction) { // If this action has a greater relevance than others, cache its index. if( EachWeight > BestMatchCount ) { BestMatchCount = EachWeight; BestMatchIndex = CurTypeIndex; } FilteredRootAction->AddChild(CurrentAction); } } FilteredRootAction->SortChildren(bAlphaSortItems, /*bRecursive =*/true); TreeView->RequestTreeRefresh(); // Update the filtered list (needs to be done in a separate pass because the list is sorted as items are inserted) FilteredActionNodes.Empty(); FilteredRootAction->GetLeafNodes(FilteredActionNodes); // Get _all_ new nodes (flattened tree basically) TArray< TSharedPtr > AllNodes; FilteredRootAction->GetAllNodes(AllNodes); // If theres a BestMatchIndex find it in the actions nodes and select it (maybe this should check the current selected suggestion first ?) if( BestMatchIndex != INDEX_NONE ) { FGraphActionListBuilderBase::ActionGroup& FilterSelectAction = AllActions.GetAction( BestMatchIndex ); if( FilterSelectAction.Actions[0].IsValid() == true ) { for (int32 iNode = 0; iNode < FilteredActionNodes.Num() ; iNode++) { if( FilteredActionNodes[ iNode ].Get()->GetPrimaryAction() == FilterSelectAction.Actions[ 0 ] ) { SelectedSuggestion = iNode; } } } } // Make sure the selected suggestion stays within the filtered list if ((SelectedSuggestion >= 0) && (FilteredActionNodes.Num() > 0)) { //@TODO: Should try to actually maintain the highlight on the same item if it survived the filtering SelectedSuggestion = FMath::Clamp(SelectedSuggestion, 0, FilteredActionNodes.Num() - 1); MarkActiveSuggestion(); } else { SelectedSuggestion = INDEX_NONE; } if (ShouldExpandNodes()) { // Expand all FilteredRootAction->ExpandAllChildren(TreeView); } else { // Expand to match the old state RestoreExpansionState< TSharedPtr >(TreeView, AllNodes, OldExpansionState, CompareGraphActionNode); } } int32 SGraphActionMenu::GetActionFilteredWeight( const FGraphActionListBuilderBase::ActionGroup& InCurrentAction, const TArray& InFilterTerms, const TArray& InSanitizedFilterTerms ) { // The overall 'weight' int32 TotalWeight = 0; // Some simple weight figures to help find the most appropriate match int32 WholeMatchWeightMultiplier = 2; int32 DescriptionWeight = 5; int32 CategoryWeight = 3; int32 NodeTitleWeight = 3; // Helper array struct FArrayWithWeight { TArray< FString > Array; int32 Weight; }; // Setup an array of arrays so we can do a weighted search TArray< FArrayWithWeight > WeightedArrayList; FArrayWithWeight EachEntry; int32 Action = 0; if( InCurrentAction.Actions[Action].IsValid() == true ) { // Combine the actions string, separate with \n so terms don't run into each other, and remove the spaces (incase the user is searching for a variable) // In the case of groups containing multiple actions, they will have been created and added at the same place in the code, using the same description // and keywords, so we only need to use the first one for filtering. FString SearchText = InCurrentAction.Actions[0]->MenuDescription.ToString() + LINE_TERMINATOR + InCurrentAction.Actions[Action]->GetSearchTitle().ToString() + LINE_TERMINATOR + InCurrentAction.Actions[Action]->Keywords + LINE_TERMINATOR +InCurrentAction.Actions[Action]->Category; SearchText = SearchText.Replace( TEXT( " " ), TEXT( "" ) ); // First the keywords InCurrentAction.Actions[Action]->Keywords.ParseIntoArray( &EachEntry.Array, TEXT(" "), true ); EachEntry.Weight = 1; WeightedArrayList.Add( EachEntry ); // The description InCurrentAction.Actions[Action]->MenuDescription.ToString().ParseIntoArray( &EachEntry.Array, TEXT(" "), true ); EachEntry.Weight = DescriptionWeight; WeightedArrayList.Add( EachEntry ); // The node search title weight InCurrentAction.Actions[Action]->GetSearchTitle().ToString().ParseIntoArray( &EachEntry.Array, TEXT(" "), true ); EachEntry.Weight = NodeTitleWeight; WeightedArrayList.Add( EachEntry ); // The category InCurrentAction.Actions[Action]->Category.ParseIntoArray( &EachEntry.Array, TEXT(" "), true ); EachEntry.Weight = CategoryWeight; WeightedArrayList.Add( EachEntry ); // Now iterate through all the filter terms and calculate a 'weight' using the values and multipliers FString EachTerm; FString EachTermSanitized; for (int32 FilterIndex = 0; FilterIndex < InFilterTerms.Num(); ++FilterIndex) { EachTerm = InFilterTerms[FilterIndex]; EachTermSanitized = InSanitizedFilterTerms[FilterIndex]; if( SearchText.Contains( EachTerm ) ) { TotalWeight += 2; } else if( SearchText.Contains( EachTermSanitized ) ) { TotalWeight++; } // Now check the weighted lists (We could further improve the hit weight by checking consecutive word matches) int32 WeightPerList = 0; for (int32 iFindCount = 0; iFindCount < WeightedArrayList.Num() ; iFindCount++) { TArray& KeywordArray = WeightedArrayList[iFindCount].Array; int32 EachWeight = WeightedArrayList[iFindCount].Weight; int32 WholeMatchCount = 0; for (int32 iEachWord = 0; iEachWord < KeywordArray.Num() ; iEachWord++) { // If we get an exact match weight the find count to get exact matches higher priority if( KeywordArray[ iEachWord ] == EachTerm ) { WeightPerList += EachWeight * WholeMatchWeightMultiplier; WholeMatchCount++; } else if( KeywordArray[ iEachWord ].Contains( EachTerm ) ) { WeightPerList += EachWeight; } else if( KeywordArray[ iEachWord ] == EachTermSanitized ) { WeightPerList += ( EachWeight * WholeMatchWeightMultiplier ) / 2; WholeMatchCount++; } else if( KeywordArray[ iEachWord ].Contains( EachTermSanitized ) ) { WeightPerList += EachWeight / 2; } } // Increase the weight if theres a larger % of matches in the keyword list if( WholeMatchCount != 0 ) { int32 PercentAdjust = ( 100 / KeywordArray.Num() ) * WholeMatchCount; WeightPerList *= PercentAdjust; } TotalWeight += WeightPerList; } } } return TotalWeight; } // Returns true if the tree should be autoexpanded bool SGraphActionMenu::ShouldExpandNodes() const { // Expand all the categories that have filter results, or when there are only a few to show const bool bFilterActive = !GetFilterText().IsEmpty(); const bool bOnlyAFewTotal = AllActions.GetNumActions() < 10; return bFilterActive || bOnlyAFewTotal || bAutoExpandActionMenu; } bool SGraphActionMenu::CanRenameNode(TWeakPtr InNode) const { return !OnCanRenameSelectedAction.Execute(InNode); } void SGraphActionMenu::OnFilterTextChanged( const FText& InFilterText ) { // Reset the selection if the string is empty if( InFilterText.IsEmpty() == true ) { SelectedSuggestion = INDEX_NONE; } GenerateFilteredItems(false); } void SGraphActionMenu::OnFilterTextCommitted(const FText& InText, ETextCommit::Type CommitInfo) { if (CommitInfo == ETextCommit::OnEnter) { TryToSpawnActiveSuggestion(); } } bool SGraphActionMenu::TryToSpawnActiveSuggestion() { TArray< TSharedPtr > SelectionList = TreeView->GetSelectedItems(); if (SelectionList.Num() == 1) { // This isnt really a keypress - its Direct, but its always called from a keypress function. (Maybe pass the selectinfo in ?) OnItemSelected( SelectionList[0], ESelectInfo::OnKeyPress ); return true; } else if (FilteredActionNodes.Num() == 1) { OnItemSelected( FilteredActionNodes[0], ESelectInfo::OnKeyPress ); return true; } return false; } void SGraphActionMenu::OnGetChildrenForCategory( TSharedPtr InItem, TArray< TSharedPtr >& OutChildren ) { if (InItem->Children.Num()) { OutChildren = InItem->Children; } } void SGraphActionMenu::OnNameTextCommitted(const FText& NewText, ETextCommit::Type InTextCommit, TWeakPtr< FGraphActionNode > InAction ) { if(OnCategoryTextCommitted.IsBound()) { OnCategoryTextCommitted.Execute(NewText, InTextCommit, InAction); } } void SGraphActionMenu::OnItemScrolledIntoView( TSharedPtr InActionNode, const TSharedPtr& InWidget ) { if (InActionNode->IsRenameRequestPending()) { InActionNode->BroadcastRenameRequest(); } } TSharedRef SGraphActionMenu::MakeWidget( TSharedPtr InItem, const TSharedRef& OwnerTable, bool bIsReadOnly ) { // In the case of FGraphActionNodes that have multiple actions, all of the actions will // have the same text as they will have been created at the same point - only the actual // action itself will differ, which is why parts of this function only refer to InItem->Actions[0] // rather than iterating over the array // Create the widget but do not add any content, the widget is needed to pass the IsSelectedExclusively function down to the potential SInlineEditableTextBlock widget TSharedPtr< STableRow< TSharedPtr > > TableRow = SNew(STableRow< TSharedPtr >, OwnerTable) .OnDragDetected( this, &SGraphActionMenu::OnItemDragDetected ) .ShowSelection( !InItem->IsSeparator() ); TSharedPtr RowContent; if( InItem->IsActionNode() ) { check(InItem->HasValidAction()); FCreateWidgetForActionData CreateData(&InItem->OnRenameRequest()); CreateData.Action = InItem->GetPrimaryAction(); CreateData.HighlightText = TAttribute(this, &SGraphActionMenu::GetFilterText); CreateData.MouseButtonDownDelegate = FCreateWidgetMouseButtonDown::CreateSP( this, &SGraphActionMenu::OnMouseButtonDownEvent ); if(OnCreateWidgetForAction.IsBound()) { CreateData.IsRowSelectedDelegate = FIsSelected::CreateSP( TableRow.Get(), &STableRow< TSharedPtr >::IsSelectedExclusively ); CreateData.bIsReadOnly = bIsReadOnly; CreateData.bHandleMouseButtonDown = false; //Default to NOT using the delegate. OnCreateWidgetForAction can set to true if we need it RowContent = OnCreateWidgetForAction.Execute( &CreateData ); } else { RowContent = SNew(SDefaultGraphActionWidget, &CreateData); } } else if( InItem->IsCategoryNode() ) { TWeakPtr< FGraphActionNode > WeakItem = InItem; // Hook up the delegate for verifying the category action is read only or not SGraphActionCategoryWidget::FArguments ReadOnlyArgument; if(bIsReadOnly) { ReadOnlyArgument.IsReadOnly(bIsReadOnly); } else { ReadOnlyArgument.IsReadOnly(this, &SGraphActionMenu::CanRenameNode, WeakItem); } TSharedRef CategoryWidget = SNew(SGraphActionCategoryWidget, InItem) .HighlightText(this, &SGraphActionMenu::GetFilterText) .OnTextCommitted(this, &SGraphActionMenu::OnNameTextCommitted, TWeakPtr< FGraphActionNode >(InItem)) .IsSelected(TableRow.Get(), &STableRow< TSharedPtr >::IsSelectedExclusively) .IsReadOnly(ReadOnlyArgument._IsReadOnly); if(!bIsReadOnly) { InItem->OnRenameRequest().BindSP( CategoryWidget->InlineWidget.Pin().Get(), &SInlineEditableTextBlock::EnterEditingMode ); } RowContent = CategoryWidget; } else if( InItem->IsSeparator() ) { FText SectionTitle; if( OnGetSectionTitle.IsBound() ) { SectionTitle = OnGetSectionTitle.Execute(InItem->SectionID); } if( SectionTitle.IsEmpty() == true ) { RowContent = SNew( SVerticalBox ) .Visibility(EVisibility::HitTestInvisible) +SVerticalBox::Slot() .AutoHeight() // Add some empty space before the line, and a tiny bit after it .Padding( 0.0f, 5.f, 0.0f, 5.f ) [ SNew( SBorder ) // We'll use the border's padding to actually create the horizontal line .Padding(FEditorStyle::GetMargin(TEXT("Menu.Separator.Padding"))) // Separator graphic .BorderImage( FEditorStyle::GetBrush( TEXT( "Menu.Separator" ) ) ) ]; } else { RowContent = SNew( SVerticalBox ) .Visibility(EVisibility::HitTestInvisible) +SVerticalBox::Slot() .AutoHeight() .Padding( 0.0f, 2.f, 0.0f, 0.f ) [ SNew(STextBlock) .Text( SectionTitle ) .TextStyle( FEditorStyle::Get(), TEXT("Menu.Heading") ) ] +SVerticalBox::Slot() .AutoHeight() // Add some empty space before the line, and a tiny bit after it .Padding( 0.0f, 2.f, 0.0f, 5.f ) [ SNew( SBorder ) // We'll use the border's padding to actually create the horizontal line .Padding(FEditorStyle::GetMargin(TEXT("Menu.Separator.Padding"))) // Separator graphic .BorderImage( FEditorStyle::GetBrush( TEXT( "Menu.Separator" ) ) ) ]; } } TSharedPtr RowContainer; TableRow->SetContent ( SAssignNew(RowContainer, SHorizontalBox) ); TSharedPtr ExpanderWidget; if (OnCreateCustomRowExpander.IsBound()) { FCustomExpanderData CreateData; CreateData.TableRow = TableRow; CreateData.WidgetContainer = RowContainer; if (InItem->IsActionNode()) { check(InItem->HasValidAction()); CreateData.RowAction = InItem->GetPrimaryAction(); } ExpanderWidget = OnCreateCustomRowExpander.Execute(CreateData); } else { ExpanderWidget = SNew(SExpanderArrow, TableRow); } RowContainer->AddSlot() .AutoWidth() .VAlign(VAlign_Fill) .HAlign(HAlign_Right) [ ExpanderWidget.ToSharedRef() ]; RowContainer->AddSlot() .FillWidth(1.0) [ RowContent.ToSharedRef() ]; return TableRow.ToSharedRef(); } FText SGraphActionMenu::GetFilterText() const { // If there is an external source for the filter, use that text instead if(OnGetFilterText.IsBound()) { return OnGetFilterText.Execute(); } return FilterTextBox->GetText();; } void SGraphActionMenu::OnItemSelected( TSharedPtr< FGraphActionNode > InSelectedItem, ESelectInfo::Type SelectInfo ) { if (!bIgnoreUIUpdate) { // Filter out selection changes that should not trigger execution if( ( SelectInfo == ESelectInfo::OnMouseClick ) || ( SelectInfo == ESelectInfo::OnKeyPress ) || !InSelectedItem.IsValid()) { HandleSelection(InSelectedItem); } } } void SGraphActionMenu::OnItemDoubleClicked( TSharedPtr< FGraphActionNode > InClickedItem ) { if ( InClickedItem.IsValid() && !bIgnoreUIUpdate ) { if ( InClickedItem->IsActionNode() ) { OnActionDoubleClicked.ExecuteIfBound(InClickedItem->Actions); } else if (InClickedItem->Children.Num()) { TreeView->SetItemExpansion(InClickedItem, !TreeView->IsItemExpanded(InClickedItem)); } } } FReply SGraphActionMenu::OnItemDragDetected( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent ) { // Start a function-call drag event for any entry that can be called by kismet if (MouseEvent.IsMouseButtonDown(EKeys::LeftMouseButton)) { TArray< TSharedPtr > SelectedNodes = TreeView->GetSelectedItems(); if(SelectedNodes.Num() > 0) { TSharedPtr Node = SelectedNodes[0]; // Dragging a ctaegory if(Node.IsValid() && Node->IsCategoryNode()) { if(OnCategoryDragged.IsBound()) { return OnCategoryDragged.Execute(Node->GetCategoryPath(), MouseEvent); } } // Dragging an action else { if(OnActionDragged.IsBound()) { TArray< TSharedPtr > Actions; GetSelectedActions(Actions); return OnActionDragged.Execute(Actions, MouseEvent); } } } } return FReply::Unhandled(); } bool SGraphActionMenu::OnMouseButtonDownEvent( TWeakPtr InAction ) { bool bResult = false; if( (!bIgnoreUIUpdate) && InAction.IsValid() ) { TArray< TSharedPtr > SelectionList = TreeView->GetSelectedItems(); TSharedPtr SelectedNode; if (SelectionList.Num() == 1) { SelectedNode = SelectionList[0]; } else if (FilteredActionNodes.Num() == 1) { SelectedNode = FilteredActionNodes[0]; } if (SelectedNode.IsValid() && SelectedNode->HasValidAction()) { if( SelectedNode->GetPrimaryAction().Get() == InAction.Pin().Get() ) { bResult = HandleSelection( SelectedNode ); } } } return bResult; } FReply SGraphActionMenu::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& KeyEvent ) { int32 SelectionDelta = 0; // Escape dismisses the menu without placing a node if (KeyEvent.GetKey() == EKeys::Escape) { FSlateApplication::Get().DismissAllMenus(); return FReply::Handled(); } else if ((KeyEvent.GetKey() == EKeys::Enter) && !bIgnoreUIUpdate) { return TryToSpawnActiveSuggestion() ? FReply::Handled() : FReply::Unhandled(); } else if (FilteredActionNodes.Num() > 0) { // Up and down move thru the filtered node list if (KeyEvent.GetKey() == EKeys::Up) { SelectionDelta = -1; } else if (KeyEvent.GetKey() == EKeys::Down) { SelectionDelta = +1; } if (SelectionDelta != 0) { // If we have no selected suggestion then we need to use the items in the root to set the selection and set the focus if( SelectedSuggestion == INDEX_NONE ) { SelectedSuggestion = (SelectedSuggestion + SelectionDelta + FilteredRootAction->Children.Num()) % FilteredRootAction->Children.Num(); TGuardValue PreventSelectionFromTriggeringCommit(bIgnoreUIUpdate, true); TreeView->SetSelection(FilteredRootAction->Children[SelectedSuggestion], ESelectInfo::OnKeyPress); TreeView->RequestScrollIntoView(FilteredRootAction->Children[SelectedSuggestion]); return FReply::Handled().SetUserFocus(SharedThis(TreeView.Get()), EFocusCause::WindowActivate); } //Move up or down one, wrapping around SelectedSuggestion = (SelectedSuggestion + SelectionDelta + FilteredActionNodes.Num()) % FilteredActionNodes.Num(); MarkActiveSuggestion(); return FReply::Handled(); } } return FReply::Unhandled(); } void SGraphActionMenu::MarkActiveSuggestion() { TGuardValue PreventSelectionFromTriggeringCommit(bIgnoreUIUpdate, true); if (SelectedSuggestion >= 0) { TSharedPtr& ActionToSelect = FilteredActionNodes[SelectedSuggestion]; TreeView->SetSelection(ActionToSelect); TreeView->RequestScrollIntoView(ActionToSelect); } else { TreeView->ClearSelection(); } } void SGraphActionMenu::AddReferencedObjects( FReferenceCollector& Collector ) { for (int32 CurTypeIndex=0; CurTypeIndex < AllActions.GetNumActions(); ++CurTypeIndex) { FGraphActionListBuilderBase::ActionGroup& Action = AllActions.GetAction( CurTypeIndex ); for ( int32 ActionIndex = 0; ActionIndex < Action.Actions.Num(); ActionIndex++ ) { Action.Actions[ActionIndex]->AddReferencedObjects(Collector); } } } bool SGraphActionMenu::HandleSelection( TSharedPtr< FGraphActionNode > &InSelectedItem ) { bool bResult = false; if( OnActionSelected.IsBound() ) { if ( InSelectedItem.IsValid() && InSelectedItem->IsActionNode() ) { OnActionSelected.Execute(InSelectedItem->Actions); bResult = true; } else { OnActionSelected.Execute(TArray< TSharedPtr >()); bResult = true; } } return bResult; } ///////////////////////////////////////////////////// #undef LOCTEXT_NAMESPACE