// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "OutputLogPrivatePCH.h" #include "SOutputLog.h" #include "OutputLogActions.h" #include "SScrollBorder.h" SConsoleInputBox::SConsoleInputBox() : bIgnoreUIUpdate(false) , SelectedSuggestion(-1) { } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SConsoleInputBox::Construct( const FArguments& InArgs ) { ChildSlot [ SAssignNew( SuggestionBox, SMenuAnchor ) .Placement( InArgs._SuggestionListPlacement ) [ SAssignNew(InputText, SEditableTextBox) .OnTextCommitted(this, &SConsoleInputBox::OnTextCommitted) .HintText( NSLOCTEXT( "ConsoleInputBox", "TypeInConsoleHint", "Enter console command" ) ) .ClearKeyboardFocusOnCommit(false) .IsCaretMovedWhenGainFocus(false) .OnTextChanged(this, &SConsoleInputBox::OnTextChanged) ] .MenuContent ( SNew(SBorder) .BorderImage(FEditorStyle::GetBrush("Menu.Background")) .Padding( FMargin(2) ) [ SNew(SBox) .HeightOverride(250) // avoids flickering, ideally this would be adaptive to the content without flickering [ SAssignNew(SuggestionListView, SListView< TSharedPtr >) .ListItemsSource(&Suggestions) .SelectionMode( ESelectionMode::Single ) // Ideally the mouse over would not highlight while keyboard controls the UI .OnGenerateRow(this, &SConsoleInputBox::MakeSuggestionListItemWidget) .OnSelectionChanged(this, &SConsoleInputBox::SuggestionSelectionChanged) .ItemHeight(18) ] ] ) ]; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION void SConsoleInputBox::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) { SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); if (!GIntraFrameDebuggingGameThread && !IsEnabled()) { SetEnabled(true); } else if (GIntraFrameDebuggingGameThread && IsEnabled()) { SetEnabled(false); } } void SConsoleInputBox::SuggestionSelectionChanged(TSharedPtr NewValue, ESelectInfo::Type SelectInfo) { if(bIgnoreUIUpdate) { return; } for(int32 i = 0; i < Suggestions.Num(); ++i) { if(NewValue == Suggestions[i]) { SelectedSuggestion = i; MarkActiveSuggestion(); // If the user selected this suggestion by clicking on it, then go ahead and close the suggestion // box as they've chosen the suggestion they're interested in. if( SelectInfo == ESelectInfo::OnMouseClick ) { SuggestionBox->SetIsOpen( false ); } // Ideally this would set the focus back to the edit control // FWidgetPath WidgetToFocusPath; // FSlateApplication::Get().GeneratePathToWidgetUnchecked( InputText.ToSharedRef(), WidgetToFocusPath ); // FSlateApplication::Get().SetKeyboardFocus( WidgetToFocusPath, EKeyboardFocusCause::SetDirectly ); break; } } } TSharedRef SConsoleInputBox::MakeSuggestionListItemWidget(TSharedPtr Text, const TSharedRef& OwnerTable) { check(Text.IsValid()); FString Left, Right, Combined; if(Text->Split(TEXT("\t"), &Left, &Right)) { Combined = Left + Right; } else { Combined = *Text; } FText HighlightText = FText::FromString(Left); return SNew(STableRow< TSharedPtr >, OwnerTable) [ SNew(SBox) .WidthOverride(300) // to enforce some minimum width, ideally we define the minimum, not a fixed width [ SNew(STextBlock) .Text(Combined) .TextStyle( FEditorStyle::Get(), "Log.Normal") .HighlightText(HighlightText) ] ]; } class FConsoleVariableAutoCompleteVisitor { public: // @param Name must not be 0 // @param CVar must not be 0 static void OnConsoleVariable(const TCHAR *Name, IConsoleObject* CVar,TArray& Sink) { #if (UE_BUILD_SHIPPING || UE_BUILD_TEST) if(CVar->TestFlags(ECVF_Cheat)) { return; } #endif // (UE_BUILD_SHIPPING || UE_BUILD_TEST) if(CVar->TestFlags(ECVF_Unregistered)) { return; } Sink.Add(Name); } }; void SConsoleInputBox::OnTextChanged(const FText& InText) { if(bIgnoreUIUpdate) { return; } const FString& InputTextStr = InputText->GetText().ToString(); if(!InputTextStr.IsEmpty()) { TArray AutoCompleteList; // console variables { IConsoleManager::Get().ForEachConsoleObject( FConsoleObjectVisitor::CreateStatic< TArray& >( &FConsoleVariableAutoCompleteVisitor::OnConsoleVariable, AutoCompleteList ), *InputTextStr); } AutoCompleteList.Sort(); for(uint32 i = 0; i < (uint32)AutoCompleteList.Num(); ++i) { FString &ref = AutoCompleteList[i]; ref = ref.Left(InputTextStr.Len()) + TEXT("\t") + ref.RightChop(InputTextStr.Len()); } SetSuggestions(AutoCompleteList, false); } else { ClearSuggestions(); } } void SConsoleInputBox::OnTextCommitted( const FText& InText, ETextCommit::Type CommitInfo) { if (CommitInfo == ETextCommit::OnEnter) { if (!InText.IsEmpty()) { IConsoleManager::Get().AddConsoleHistoryEntry( *InText.ToString() ); // Copy the exec text string out so we can clear the widget's contents. If the exec command spawns // a new window it can cause the text box to lose focus, which will result in this function being // re-entered. We want to make sure the text string is empty on re-entry, so we'll clear it out const FString ExecString = InText.ToString(); // Clear the console input area bIgnoreUIUpdate = true; InputText->SetText(FText::GetEmpty()); bIgnoreUIUpdate = false; // Exec! { bool bWasHandled = false; UWorld* World = NULL; UWorld* OldWorld = NULL; // The play world needs to handle these commands if it exists if( GIsEditor && GEditor->PlayWorld ) { World = GEditor->PlayWorld; OldWorld = SetPlayInEditorWorld( GEditor->PlayWorld ); } ULocalPlayer* Player = GEngine->GetDebugLocalPlayer(); if( Player ) { UWorld* PlayerWorld = Player->GetWorld(); if( !World ) { World = PlayerWorld; } bWasHandled = Player->Exec( PlayerWorld, *ExecString, *GLog ); } if( !World ) { World = GEditor->GetEditorWorldContext().World(); } if( World ) { if( !bWasHandled ) { AGameMode* const GameMode = World->GetAuthGameMode(); if( GameMode && GameMode->ProcessConsoleExec( *ExecString, *GLog, NULL ) ) { bWasHandled = true; } else if (World->GameState && World->GameState->ProcessConsoleExec(*ExecString, *GLog, NULL)) { bWasHandled = true; } } if( !bWasHandled && !Player) { if( GIsEditor ) { bWasHandled = GEditor->Exec( World, *ExecString, *GLog ); } else { bWasHandled = GEngine->Exec( World, *ExecString, *GLog ); } } } // Restore the old world of there was one if( OldWorld ) { RestoreEditorWorld( OldWorld ); } } } ClearSuggestions(); } } FReply SConsoleInputBox::OnKeyDown( const FGeometry& MyGeometry, const FKeyboardEvent& KeyboardEvent ) { if(SuggestionBox->IsOpen()) { if(KeyboardEvent.GetKey() == EKeys::Up || KeyboardEvent.GetKey() == EKeys::Down) { if(KeyboardEvent.GetKey() == EKeys::Up) { if(SelectedSuggestion < 0) { // from edit control to end of list SelectedSuggestion = Suggestions.Num() - 1; } else { // got one up, possibly back to edit control --SelectedSuggestion; } } if(KeyboardEvent.GetKey() == EKeys::Down) { if(SelectedSuggestion < Suggestions.Num() - 1) { // go one down, possibly from edit control to top ++SelectedSuggestion; } else { // back to edit control SelectedSuggestion = -1; } } MarkActiveSuggestion(); return FReply::Handled(); } else if (KeyboardEvent.GetKey() == EKeys::Tab) { if (Suggestions.Num()) { if (SelectedSuggestion >= 0 && SelectedSuggestion < Suggestions.Num()) { MarkActiveSuggestion(); OnTextCommitted(InputText->GetText(), ETextCommit::OnEnter); } else { SelectedSuggestion = 0; MarkActiveSuggestion(); } } return FReply::Handled(); } } else { if(KeyboardEvent.GetKey() == EKeys::Up) { TArray History; IConsoleManager::Get().GetConsoleHistory(History); SetSuggestions(History, true); if(Suggestions.Num()) { SelectedSuggestion = Suggestions.Num() - 1; MarkActiveSuggestion(); } return FReply::Handled(); } } return FReply::Unhandled(); } void SConsoleInputBox::SetSuggestions(TArray& Elements, bool bInHistoryMode) { FString SelectionText; if (SelectedSuggestion >= 0 && SelectedSuggestion < Suggestions.Num()) { SelectionText = *Suggestions[SelectedSuggestion]; } SelectedSuggestion = -1; Suggestions.Empty(); SelectedSuggestion = -1; for(uint32 i = 0; i < (uint32)Elements.Num(); ++i) { Suggestions.Add(MakeShareable(new FString(Elements[i]))); if (Elements[i] == SelectionText) { SelectedSuggestion = i; } } if(Suggestions.Num()) { // Ideally if the selection box is open the output window is not changing it's window title (flickers) SuggestionBox->SetIsOpen(true, false); SuggestionListView->RequestScrollIntoView(Suggestions.Last()); // Force the textbox back into focus. FWidgetPath WidgetToFocusPath; FSlateApplication::Get().GeneratePathToWidgetUnchecked( InputText.ToSharedRef(), WidgetToFocusPath ); FSlateApplication::Get().SetKeyboardFocus( WidgetToFocusPath, EKeyboardFocusCause::SetDirectly ); } else { SuggestionBox->SetIsOpen(false); } } void SConsoleInputBox::OnKeyboardFocusLost( const FKeyboardFocusEvent& InKeyboardFocusEvent ) { // SuggestionBox->SetIsOpen(false); } void SConsoleInputBox::MarkActiveSuggestion() { bIgnoreUIUpdate = true; if(SelectedSuggestion >= 0) { SuggestionListView->SetSelection(Suggestions[SelectedSuggestion]); SuggestionListView->RequestScrollIntoView(Suggestions[SelectedSuggestion]); // Ideally this would only scroll if outside of the view InputText->SetText(FText::FromString(GetSelectionText())); } else { SuggestionListView->ClearSelection(); } bIgnoreUIUpdate = false; } void SConsoleInputBox::ClearSuggestions() { SelectedSuggestion = -1; SuggestionBox->SetIsOpen(false); Suggestions.Empty(); } FString SConsoleInputBox::GetSelectionText() const { FString ret = *Suggestions[SelectedSuggestion]; ret = ret.Replace(TEXT("\t"), TEXT("")); return ret; } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SOutputLog::Construct( const FArguments& InArgs ) { Messages = InArgs._Messages; OutputLogActions = MakeShareable( new FUICommandList ); OutputLogScrollBar = SNew( SScrollBar ); const FOutputLogCommandsImpl& Commands = FOutputLogCommands::Get(); OutputLogActions->MapAction( Commands.CopyOutputLog, FExecuteAction::CreateRaw( this, &SOutputLog::OnCopy ), FCanExecuteAction::CreateSP( this, &SOutputLog::CanCopy )); OutputLogActions->MapAction( Commands.SelectAllInOutputLog, FExecuteAction::CreateRaw( this, &SOutputLog::OnSelectAll ), FCanExecuteAction::CreateSP( this, &SOutputLog::CanSelectAll )); OutputLogActions->MapAction( Commands.SelectNoneInOutputLog, FExecuteAction::CreateRaw( this, &SOutputLog::OnSelectNone ), FCanExecuteAction::CreateSP( this, &SOutputLog::CanSelectNone )); OutputLogActions->MapAction( Commands.ClearOutputLog, FExecuteAction::CreateRaw( this, &SOutputLog::OnClearLog ), FCanExecuteAction::CreateSP( this, &SOutputLog::CanClearLog )); MessageListView = SNew(SListView< TSharedPtr >) // Ideally we start appending the items at the bottom, not at the top .ListItemsSource(&Messages) .OnGenerateRow(this, &SOutputLog::MakeLogListItemWidget) .SelectionMode(ESelectionMode::Multi) .ItemHeight(14) .OnContextMenuOpening(this, &SOutputLog::BuildMenuWidget) .ExternalScrollbar(OutputLogScrollBar); ChildSlot [ SNew(SVerticalBox) // Output log area +SVerticalBox::Slot() .FillHeight(1) [ SNew( SHorizontalBox ) +SHorizontalBox::Slot() .FillWidth(1) [ SNew(SScrollBorder, MessageListView.ToSharedRef()) [ MessageListView.ToSharedRef() ] ] +SHorizontalBox::Slot() .AutoWidth() [ SNew( SBox ) .WidthOverride( FOptionalSize( 16 ) ) [ SNew(SVerticalBox) +SVerticalBox::Slot() .FillHeight(1) [ // Output log area OutputLogScrollBar->AsShared() ] ] ] ] // The console input box +SVerticalBox::Slot() .AutoHeight() [ SNew( SConsoleInputBox ) // Always place suggestions above the input line for the output log widget .SuggestionListPlacement( MenuPlacement_AboveAnchor ) ] ]; GLog->AddOutputDevice(this); // If there's already been messages logged, scroll down to the last one if (Messages.Num() > 0) { MessageListView->RequestScrollIntoView(Messages.Last()); } } END_SLATE_FUNCTION_BUILD_OPTIMIZATION SOutputLog::~SOutputLog() { if (GLog != NULL) { GLog->RemoveOutputDevice(this); } } bool SOutputLog::CreateLogMessages( const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category, TArray< TSharedPtr >& OutMessages ) { if (Verbosity == ELogVerbosity::SetColor) { // Skip Color Events return false; } else { FName Style; if (Category == NAME_Cmd) { Style = FName(TEXT("LogTableRow.Command")); } else if (Verbosity == ELogVerbosity::Error) { Style = FName(TEXT("LogTableRow.Error")); } else if (Verbosity == ELogVerbosity::Warning) { Style = FName(TEXT("LogTableRow.Warning")); } else { Style = FName(TEXT("LogTableRow.Normal")); } // handle multiline strings by breaking them apart by line TArray< FString > MessageLines; FString CurrentLogDump = V; CurrentLogDump.ParseIntoArray(&MessageLines, TEXT("\n"), false); for (int32 i = 0; i < MessageLines.Num(); ++i) { FString Line = MessageLines[i]; if (Line.EndsWith(TEXT("\r"))) { Line = Line.LeftChop(1); } Line = Line.ConvertTabsToSpaces(4); OutMessages.Add(MakeShareable(new FLogMessage((i == 0) ? FOutputDevice::FormatLogLine(Verbosity, Category, *Line) : Line, Style))); } return (MessageLines.Num() > 0); } } FReply SOutputLog::OnKeyDown( const FGeometry& MyGeometry, const FKeyboardEvent& InKeyboardEvent ) { FReply Reply = FReply::Unhandled(); if( OutputLogActions->ProcessCommandBindings( InKeyboardEvent ) ) { // handle the event if a command was processed Reply = FReply::Handled(); } return Reply; } void SOutputLog::Serialize( const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category ) { if (CreateLogMessages(V, Verbosity, Category, Messages)) { MessageListView->RequestListRefresh(); // Don't scroll to the bottom automatically when the user is scrolling the view or has scrolled it away from the bottom. if( OutputLogScrollBar->DistanceFromBottom() <= 0.f ) { MessageListView->RequestScrollIntoView(Messages.Last()); } } } TSharedRef SOutputLog::MakeLogListItemWidget(TSharedPtr Message, const TSharedRef& OwnerTable) { check(Message.IsValid()); return SNew(STableRow< TSharedPtr >, OwnerTable) .Style( FEditorStyle::Get(), Message->Style ) [ SNew(SHorizontalBox) //.ToolTipText(Message->Message) showing the line as tooltip is a workaround to show very long lines as we don't have horizontal scrolling yet +SHorizontalBox::Slot().AutoWidth() .Padding(0) [ SNew(STextBlock) .Text(Message->Message) .TextStyle( FEditorStyle::Get(), TEXT("Log.Normal") ) ] ]; } TSharedPtr SOutputLog::BuildMenuWidget() { FOutputLogModule& OutputLogModule = FModuleManager::GetModuleChecked( TEXT("OutputLog") ); FMenuBuilder MenuBuilder( true, OutputLogActions ); { MenuBuilder.BeginSection("OutputLogEdit"); { MenuBuilder.AddMenuEntry(FOutputLogCommands::Get().CopyOutputLog); MenuBuilder.AddMenuEntry(FOutputLogCommands::Get().SelectAllInOutputLog); MenuBuilder.AddMenuEntry(FOutputLogCommands::Get().SelectNoneInOutputLog); } MenuBuilder.EndSection(); MenuBuilder.AddMenuEntry(FOutputLogCommands::Get().ClearOutputLog); } return MenuBuilder.MakeWidget(); } void SOutputLog::OnCopy() { TArray> SelectedItems = MessageListView->GetSelectedItems(); if (SelectedItems.Num()) { // Make sure the selected range is sorted in descending index order SelectedItems.Sort([this](const TSharedPtr& Item1, const TSharedPtr& Item2) -> bool { const int32 Item1Index = Messages.IndexOfByKey(Item1); const int32 Item2Index = Messages.IndexOfByKey(Item2); return Item1Index < Item2Index; }); FString SelectedText; for (int32 SelectedItemIdx = 0; SelectedItemIdx < SelectedItems.Num(); SelectedItemIdx++) { SelectedText += SelectedItems[SelectedItemIdx]->Message + LINE_TERMINATOR; } SelectedText = SelectedText.LeftChop(FCString::Strlen(LINE_TERMINATOR)); // Copy text to clipboard FPlatformMisc::ClipboardCopy( *SelectedText ); } } bool SOutputLog::CanCopy() const { const int32 NumSelected = MessageListView->GetNumItemsSelected(); return NumSelected != 0; } void SOutputLog::OnClearLog() { Messages.Empty(); MessageListView->RequestListRefresh(); } bool SOutputLog::CanClearLog() const { return Messages.Num() != 0; } void SOutputLog::OnSelectAll() { for (int32 ItemIdx = 0; ItemIdx < Messages.Num(); ItemIdx++) { if (!MessageListView->IsItemSelected(Messages[ItemIdx])) { MessageListView->SetItemSelection(Messages[ItemIdx], true); } } } bool SOutputLog::CanSelectAll() const { const int32 NumSelected = MessageListView->GetNumItemsSelected(); return Messages.Num() && NumSelected != Messages.Num(); } void SOutputLog::OnSelectNone() { for (int32 ItemIdx = 0; ItemIdx < Messages.Num(); ItemIdx++) { if (MessageListView->IsItemSelected(Messages[ItemIdx])) { MessageListView->SetItemSelection(Messages[ItemIdx], false); } } } bool SOutputLog::CanSelectNone() const { const int32 NumSelected = MessageListView->GetNumItemsSelected(); return Messages.Num() && NumSelected; }