// Copyright Epic Games, Inc. All Rights Reserved. #include "Widgets/Input/SSuggestionTextBox.h" #include "Layout/WidgetPath.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Input/SMenuAnchor.h" #include "Widgets/Views/SListView.h" /* SSuggestionTextBox structors *****************************************************************************/ SSuggestionTextBox::SSuggestionTextBox( ) : IgnoreUIUpdate(false) , SelectedSuggestion(-1) { } /* SSuggestionTextBox interface *****************************************************************************/ void SSuggestionTextBox::Construct( const FArguments& InArgs ) { SuggestionTextStyle = InArgs._SuggestionTextStyle; OnShowingHistory = InArgs._OnShowingHistory; OnShowingSuggestions = InArgs._OnShowingSuggestions; OnTextChanged = InArgs._OnTextChanged; OnTextCommitted = InArgs._OnTextCommitted; FSlateApplication::Get().OnFocusChanging().AddSP(this, &SSuggestionTextBox::OnGlobalFocusChanging); ChildSlot [ SAssignNew(MenuAnchor, SMenuAnchor) .Placement(MenuPlacement_ComboBox) [ SAssignNew(TextBox, SEditableTextBox) .BackgroundColor(InArgs._BackgroundColor) .ClearKeyboardFocusOnCommit(InArgs._ClearKeyboardFocusOnCommit) .ErrorReporting(InArgs._ErrorReporting) .Font(InArgs._Font) .ForegroundColor(InArgs._ForegroundColor) .HintText(InArgs._HintText) .IsCaretMovedWhenGainFocus(InArgs._IsCaretMovedWhenGainFocus) .MinDesiredWidth(InArgs._MinDesiredWidth) .RevertTextOnEscape(InArgs._RevertTextOnEscape) .SelectAllTextOnCommit(InArgs._SelectAllTextOnCommit) .SelectAllTextWhenFocused(InArgs._SelectAllTextWhenFocused) .Style(InArgs._TextStyle) .Text(InArgs._Text) .OnKeyDownHandler(this, &SSuggestionTextBox::OnKeyDown) .OnTextChanged(this, &SSuggestionTextBox::HandleTextBoxTextChanged) .OnTextCommitted(this, &SSuggestionTextBox::HandleTextBoxTextCommitted) ] .MenuContent ( SNew(SBorder) .BorderImage(InArgs._BackgroundImage) .Padding(FMargin(2)) [ SAssignNew(VerticalBox, SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .MaxHeight(InArgs._SuggestionListMaxHeight) [ SAssignNew(SuggestionListView, SListView< TSharedPtr >) .ListItemsSource(&Suggestions) .SelectionMode(ESelectionMode::Single) .OnGenerateRow(this, &SSuggestionTextBox::HandleSuggestionListViewGenerateRow) .OnSelectionChanged(this, &SSuggestionTextBox::HandleSuggestionListViewSelectionChanged) ] ] ) ]; } void SSuggestionTextBox::SetText( const TAttribute& InNewText ) { IgnoreUIUpdate = true; TextBox->SetText(InNewText); IgnoreUIUpdate = false; } /* SSuggestionTextBox implementation *****************************************************************************/ void SSuggestionTextBox::ClearSuggestions( ) { SelectedSuggestion = -1; MenuAnchor->SetIsOpen(false); Suggestions.Empty(); } void SSuggestionTextBox::FocusTextBox( ) { FWidgetPath TextBoxPath; FSlateApplication::Get().GeneratePathToWidgetUnchecked(TextBox.ToSharedRef(), TextBoxPath); FSlateApplication::Get().SetKeyboardFocus(TextBoxPath, EFocusCause::SetDirectly); } FString SSuggestionTextBox::GetSelectedSuggestionString( ) const { FString SuggestionString = *Suggestions[SelectedSuggestion]; return SuggestionString.Replace(TEXT("\t"), TEXT("")); } void SSuggestionTextBox::MarkActiveSuggestion( ) { IgnoreUIUpdate = true; if (SelectedSuggestion >= 0) { TSharedPtr Suggestion = Suggestions[SelectedSuggestion]; SuggestionListView->SetSelection(Suggestion); if (!SuggestionListView->IsItemVisible(Suggestion)) { SuggestionListView->RequestScrollIntoView(Suggestion); } TextBox->SetText(FText::FromString(GetSelectedSuggestionString())); } else { SuggestionListView->ClearSelection(); } IgnoreUIUpdate = false; } void SSuggestionTextBox::SetSuggestions( TArray& SuggestionStrings, bool InHistoryMode ) { FString SelectionText; // cache previously selected suggestion if ((SelectedSuggestion >= 0) && (SelectedSuggestion < Suggestions.Num())) { SelectionText = *Suggestions[SelectedSuggestion]; } SelectedSuggestion = -1; // refill suggestions Suggestions.Empty(); for (int32 i = 0; i < SuggestionStrings.Num(); ++i) { Suggestions.Add(MakeShareable(new FString(SuggestionStrings[i]))); if (SuggestionStrings[i] == SelectionText) { SelectedSuggestion = i; } } if (Suggestions.Num()) { // @todo Slate: make the window title not flicker when the box toggles visibility MenuAnchor->SetIsOpen(true, false); int32 SuggestionIndexToScrollIntoView = FMath::Max(0, SelectedSuggestion); SuggestionListView->RequestScrollIntoView(Suggestions[SuggestionIndexToScrollIntoView]); FocusTextBox(); } else { MenuAnchor->SetIsOpen(false); } } /* SWidget overrides *****************************************************************************/ void SSuggestionTextBox::OnFocusLost( const FFocusEvent& InFocusEvent ) { // MenuAnchor->SetIsOpen(false); } FReply SSuggestionTextBox::OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& KeyEvent ) { const FString& InputTextStr = TextBox->GetText().ToString(); if (MenuAnchor->IsOpen()) { if (KeyEvent.GetKey() == EKeys::Up) { // backward navigate the list of suggestions if (SelectedSuggestion < 0) { SelectedSuggestion = Suggestions.Num() - 1; } else { --SelectedSuggestion; } MarkActiveSuggestion(); return FReply::Handled(); } else if(KeyEvent.GetKey() == EKeys::Down) { // forward navigate the list of suggestions if (SelectedSuggestion < Suggestions.Num() - 1) { ++SelectedSuggestion; } else { SelectedSuggestion = -1; } MarkActiveSuggestion(); return FReply::Handled(); } else if (KeyEvent.GetKey() == EKeys::Tab) { // auto-complete the highlighted suggestion if (Suggestions.Num()) { if ((SelectedSuggestion >= 0) && (SelectedSuggestion < Suggestions.Num())) { MarkActiveSuggestion(); HandleTextBoxTextCommitted(TextBox->GetText(), ETextCommit::OnEnter); } else { SelectedSuggestion = 0; MarkActiveSuggestion(); } } return FReply::Handled(); } } else { if ((KeyEvent.GetKey() == EKeys::Up) || (KeyEvent.GetKey() == EKeys::Down)) { // show the input history if (OnShowingHistory.IsBound()) { TArray HistoryStrings; OnShowingHistory.ExecuteIfBound(HistoryStrings); if (HistoryStrings.Num() > 0) { SetSuggestions(HistoryStrings, true); if (KeyEvent.GetKey() == EKeys::Up) { SelectedSuggestion = HistoryStrings.Num() - 1; } else { SelectedSuggestion = 0; } MarkActiveSuggestion(); } } return FReply::Handled(); } } return FReply::Unhandled(); } bool SSuggestionTextBox::SupportsKeyboardFocus() const { return true; } /* SSuggestionTextBox callbacks *****************************************************************************/ TSharedRef SSuggestionTextBox::HandleSuggestionListViewGenerateRow( TSharedPtr Text, const TSharedRef& OwnerTable ) { FString Left, Right, Combined; if (Text->Split(TEXT("\t"), &Left, &Right)) { Combined = Left + Right; } else { Combined = *Text; } return SNew(STableRow >, OwnerTable) [ SNew(SBox) [ SNew(STextBlock) .HighlightText(this, &SSuggestionTextBox::HandleSuggestionListWidgetHighlightText) .TextStyle( SuggestionTextStyle ) .Text( FText::FromString(Combined) ) ] ]; } void SSuggestionTextBox::HandleSuggestionListViewSelectionChanged( TSharedPtr NewValue, ESelectInfo::Type SelectInfo ) { if (!IgnoreUIUpdate) { for (int32 i = 0; i < Suggestions.Num(); ++i) { if (NewValue == Suggestions[i]) { SelectedSuggestion = i; MarkActiveSuggestion(); FocusTextBox(); break; } } } } FText SSuggestionTextBox::HandleSuggestionListWidgetHighlightText( ) const { return TextBox->GetText(); } void SSuggestionTextBox::HandleTextBoxTextChanged( const FText& InText ) { OnTextChanged.ExecuteIfBound(InText); if (!IgnoreUIUpdate) { const FString& InputTextStr = TextBox->GetText().ToString(); if (!InputTextStr.IsEmpty() && OnShowingSuggestions.IsBound()) { TArray SuggestionStrings; OnShowingSuggestions.ExecuteIfBound(InText.ToString(), SuggestionStrings); for (int32 Index = 0; Index < SuggestionStrings.Num(); ++Index) { FString& StringRef = SuggestionStrings[Index]; StringRef = StringRef.Left(InputTextStr.Len()) + TEXT("\t") + StringRef.RightChop(InputTextStr.Len()); } SetSuggestions(SuggestionStrings, false); } else { ClearSuggestions(); } } } void SSuggestionTextBox::HandleTextBoxTextCommitted( const FText& InText, ETextCommit::Type CommitInfo ) { if (!MenuAnchor->IsOpen()) { OnTextCommitted.ExecuteIfBound(InText, CommitInfo); } if ((CommitInfo == ETextCommit::OnEnter) || (CommitInfo == ETextCommit::OnCleared)) { ClearSuggestions(); } } void SSuggestionTextBox::OnGlobalFocusChanging(const FFocusEvent& FocusEvent, const FWeakWidgetPath& OldFocusedWidgetPath, const TSharedPtr& OldFocusedWidget, const FWidgetPath& NewFocusedWidgetPath, const TSharedPtr& NewFocusedWidget) { if (TextBox.IsValid() && OldFocusedWidgetPath.ContainsWidget(TextBox.Get()) && FocusEvent.GetCause() == EFocusCause::Mouse) { // If the textbox has lost focus and the SuggestionList has not gained it, then we assume the user clicked somewhere else in the app and clear the SuggestionList. if (VerticalBox.IsValid() && !NewFocusedWidgetPath.ContainsWidget(VerticalBox.Get())) { ClearSuggestions(); OnTextCommitted.ExecuteIfBound(TextBox->GetText(), ETextCommit::Type::OnUserMovedFocus); } } }