Files
UnrealEngineUWP/Engine/Source/Developer/OutputLog/Private/SOutputLog.cpp
Unrealbot 3f463d80d4 Branch snapshot for CL 2959226
[CL 2959226 in Main branch]
2016-04-28 13:50:05 -04:00

1141 lines
29 KiB
C++

// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
#include "OutputLogPrivatePCH.h"
#include "SOutputLog.h"
#include "SScrollBorder.h"
#include "GameFramework/GameMode.h"
#include "Engine/LocalPlayer.h"
#include "GameFramework/GameState.h"
#include "SSearchBox.h"
#define LOCTEXT_NAMESPACE "SOutputLog"
/** Expression context to test the given messages against the current text filter */
class FLogFilter_TextFilterExpressionContext : public ITextFilterExpressionContext
{
public:
explicit FLogFilter_TextFilterExpressionContext(const FLogMessage& InMessage) : Message(&InMessage) {}
/** Test the given value against the strings extracted from the current item */
virtual bool TestBasicStringExpression(const FTextFilterString& InValue, const ETextFilterTextComparisonMode InTextComparisonMode) const override { return TextFilterUtils::TestBasicStringExpression(*Message->Message, InValue, InTextComparisonMode); }
/**
* Perform a complex expression test for the current item
* No complex expressions in this case - always returns false
*/
virtual bool TestComplexExpression(const FName& InKey, const FTextFilterString& InValue, const ETextFilterComparisonOperation InComparisonOperation, const ETextFilterTextComparisonMode InTextComparisonMode) const override { return false; }
private:
/** Message that is being filtered */
const FLogMessage* Message;
};
/** Custom console editable text box whose only purpose is to prevent some keys from being typed */
class SConsoleEditableTextBox : public SEditableTextBox
{
public:
SLATE_BEGIN_ARGS( SConsoleEditableTextBox ) {}
/** Hint text that appears when there is no text in the text box */
SLATE_ATTRIBUTE(FText, HintText)
/** Called whenever the text is changed interactively by the user */
SLATE_EVENT(FOnTextChanged, OnTextChanged)
/** Called whenever the text is committed. This happens when the user presses enter or the text box loses focus. */
SLATE_EVENT(FOnTextCommitted, OnTextCommitted)
SLATE_END_ARGS()
void Construct( const FArguments& InArgs )
{
SetStyle(&FCoreStyle::Get().GetWidgetStyle< FEditableTextBoxStyle >("NormalEditableTextBox"));
SBorder::Construct(SBorder::FArguments()
.BorderImage(this, &SConsoleEditableTextBox::GetConsoleBorder)
.BorderBackgroundColor(Style->BackgroundColor)
.ForegroundColor(Style->ForegroundColor)
.Padding(Style->Padding)
[
SAssignNew( EditableText, SConsoleEditableText )
.HintText( InArgs._HintText )
.OnTextChanged( InArgs._OnTextChanged )
.OnTextCommitted( InArgs._OnTextCommitted )
] );
}
private:
class SConsoleEditableText : public SEditableText
{
public:
SLATE_BEGIN_ARGS( SConsoleEditableText ) {}
/** The text that appears when there is nothing typed into the search box */
SLATE_ATTRIBUTE(FText, HintText)
/** Called whenever the text is changed interactively by the user */
SLATE_EVENT(FOnTextChanged, OnTextChanged)
/** Called whenever the text is committed. This happens when the user presses enter or the text box loses focus. */
SLATE_EVENT(FOnTextCommitted, OnTextCommitted)
SLATE_END_ARGS()
void Construct( const FArguments& InArgs )
{
SEditableText::Construct
(
SEditableText::FArguments()
.HintText( InArgs._HintText )
.OnTextChanged( InArgs._OnTextChanged )
.OnTextCommitted( InArgs._OnTextCommitted )
.ClearKeyboardFocusOnCommit( false )
.IsCaretMovedWhenGainFocus( false )
.MinDesiredWidth( 400.0f )
);
}
virtual FReply OnKeyDown( const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent )
{
// Special case handling. Intercept the tilde key. It is not suitable for typing in the console
if( InKeyEvent.GetKey() == EKeys::Tilde )
{
return FReply::Unhandled();
}
else
{
return SEditableText::OnKeyDown( MyGeometry, InKeyEvent );
}
}
virtual FReply OnKeyChar( const FGeometry& MyGeometry, const FCharacterEvent& InCharacterEvent )
{
// Special case handling. Intercept the tilde key. It is not suitable for typing in the console
if( InCharacterEvent.GetCharacter() != 0x60 )
{
return SEditableText::OnKeyChar( MyGeometry, InCharacterEvent );
}
else
{
return FReply::Unhandled();
}
}
};
/** @return Border image for the text box based on the hovered and focused state */
const FSlateBrush* GetConsoleBorder() const
{
if (EditableText->HasKeyboardFocus())
{
return &Style->BackgroundImageFocused;
}
else
{
if (EditableText->IsHovered())
{
return &Style->BackgroundImageHovered;
}
else
{
return &Style->BackgroundImageNormal;
}
}
}
};
SConsoleInputBox::SConsoleInputBox()
: SelectedSuggestion(-1)
, bIgnoreUIUpdate(false)
{
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SConsoleInputBox::Construct( const FArguments& InArgs )
{
OnConsoleCommandExecuted = InArgs._OnConsoleCommandExecuted;
ConsoleCommandCustomExec = InArgs._ConsoleCommandCustomExec;
ChildSlot
[
SAssignNew( SuggestionBox, SMenuAnchor )
.Placement( InArgs._SuggestionListPlacement )
[
SAssignNew(InputText, SConsoleEditableTextBox)
.OnTextCommitted(this, &SConsoleInputBox::OnTextCommitted)
.HintText( NSLOCTEXT( "ConsoleInputBox", "TypeInConsoleHint", "Enter console command" ) )
.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<FString> >)
.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)
]
]
)
];
}
void SConsoleInputBox::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
{
if (!GIntraFrameDebuggingGameThread && !IsEnabled())
{
SetEnabled(true);
}
else if (GIntraFrameDebuggingGameThread && IsEnabled())
{
SetEnabled(false);
}
}
void SConsoleInputBox::SuggestionSelectionChanged(TSharedPtr<FString> 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, EFocusCause::SetDirectly );
break;
}
}
}
TSharedRef<ITableRow> SConsoleInputBox::MakeSuggestionListItemWidget(TSharedPtr<FString> Text, const TSharedRef<STableViewBase>& 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<FString> >, OwnerTable)
[
SNew(SBox)
.WidthOverride(300) // to enforce some minimum width, ideally we define the minimum, not a fixed width
[
SNew(STextBlock)
.Text(FText::FromString(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<FString>& 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<FString> AutoCompleteList;
// console variables
{
IConsoleManager::Get().ForEachConsoleObject(
FConsoleObjectVisitor::CreateStatic< TArray<FString>& >(
&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!
if (ConsoleCommandCustomExec.IsBound())
{
ConsoleCommandCustomExec.Execute(ExecString);
}
else
{
bool bWasHandled = false;
UWorld* World = NULL;
UWorld* OldWorld = NULL;
// The play world needs to handle these commands if it exists
if( GIsEditor && GEditor->PlayWorld && !GIsPlayInEditorWorld )
{
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();
OnConsoleCommandExecuted.ExecuteIfBound();
}
}
FReply SConsoleInputBox::OnPreviewKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent)
{
if(SuggestionBox->IsOpen())
{
if(KeyEvent.GetKey() == EKeys::Up || KeyEvent.GetKey() == EKeys::Down)
{
if(KeyEvent.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(KeyEvent.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 (KeyEvent.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(KeyEvent.GetKey() == EKeys::Up)
{
TArray<FString> 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<FString>& 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());
}
else
{
SuggestionBox->SetIsOpen(false);
}
}
void SConsoleInputBox::OnFocusLost( const FFocusEvent& InFocusEvent )
{
// 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;
}
TSharedRef< FOutputLogTextLayoutMarshaller > FOutputLogTextLayoutMarshaller::Create(TArray< TSharedPtr<FLogMessage> > InMessages, FLogFilter* InFilter)
{
return MakeShareable(new FOutputLogTextLayoutMarshaller(MoveTemp(InMessages), InFilter));
}
FOutputLogTextLayoutMarshaller::~FOutputLogTextLayoutMarshaller()
{
}
void FOutputLogTextLayoutMarshaller::SetText(const FString& SourceString, FTextLayout& TargetTextLayout)
{
TextLayout = &TargetTextLayout;
AppendMessagesToTextLayout(Messages);
}
void FOutputLogTextLayoutMarshaller::GetText(FString& TargetString, const FTextLayout& SourceTextLayout)
{
SourceTextLayout.GetAsText(TargetString);
}
bool FOutputLogTextLayoutMarshaller::AppendMessage(const TCHAR* InText, const ELogVerbosity::Type InVerbosity, const FName& InCategory)
{
TArray< TSharedPtr<FLogMessage> > NewMessages;
if(SOutputLog::CreateLogMessages(InText, InVerbosity, InCategory, NewMessages))
{
const bool bWasEmpty = Messages.Num() == 0;
Messages.Append(NewMessages);
if(TextLayout)
{
// If we were previously empty, then we'd have inserted a dummy empty line into the document
// We need to remove this line now as it would cause the message indices to get out-of-sync with the line numbers, which would break auto-scrolling
if(bWasEmpty)
{
TextLayout->ClearLines();
}
// If we've already been given a text layout, then append these new messages rather than force a refresh of the entire document
AppendMessagesToTextLayout(NewMessages);
}
else
{
MarkMessagesCacheAsDirty();
MakeDirty();
}
return true;
}
return false;
}
void FOutputLogTextLayoutMarshaller::AppendMessageToTextLayout(const TSharedPtr<FLogMessage>& InMessage)
{
if (!Filter->IsMessageAllowed(InMessage))
{
return;
}
// Increment the cached count
CachedNumMessages++;
const FTextBlockStyle& MessageTextStyle = FEditorStyle::Get().GetWidgetStyle<FTextBlockStyle>(InMessage->Style);
TSharedRef<FString> LineText = InMessage->Message;
TArray<TSharedRef<IRun>> Runs;
Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle));
TextLayout->AddLine(FSlateTextLayout::FNewLineData(MoveTemp(LineText), MoveTemp(Runs)));
}
void FOutputLogTextLayoutMarshaller::AppendMessagesToTextLayout(const TArray<TSharedPtr<FLogMessage>>& InMessages)
{
TArray<FTextLayout::FNewLineData> LinesToAdd;
LinesToAdd.Reserve(InMessages.Num());
for (const auto& CurrentMessage : InMessages)
{
if (!Filter->IsMessageAllowed(CurrentMessage))
{
continue;
}
// Increment the cached count
CachedNumMessages++;
const FTextBlockStyle& MessageTextStyle = FEditorStyle::Get().GetWidgetStyle<FTextBlockStyle>(CurrentMessage->Style);
TSharedRef<FString> LineText = CurrentMessage->Message;
TArray<TSharedRef<IRun>> Runs;
Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle));
LinesToAdd.Emplace(MoveTemp(LineText), MoveTemp(Runs));
}
TextLayout->AddLines(LinesToAdd);
}
void FOutputLogTextLayoutMarshaller::ClearMessages()
{
Messages.Empty();
MakeDirty();
}
void FOutputLogTextLayoutMarshaller::CountMessages()
{
// Do not re-count if not dirty
if (!bNumMessagesCacheDirty)
{
return;
}
CachedNumMessages = 0;
for (const auto& CurrentMessage : Messages)
{
if (Filter->IsMessageAllowed(CurrentMessage))
{
CachedNumMessages++;
}
}
// Cache re-built, remove dirty flag
bNumMessagesCacheDirty = false;
}
int32 FOutputLogTextLayoutMarshaller::GetNumMessages() const
{
return Messages.Num();
}
int32 FOutputLogTextLayoutMarshaller::GetNumFilteredMessages()
{
// No need to filter the messages if the filter is not set
if (!Filter->IsFilterSet())
{
return GetNumMessages();
}
// Re-count messages if filter changed before we refresh
if (bNumMessagesCacheDirty)
{
CountMessages();
}
return CachedNumMessages;
}
void FOutputLogTextLayoutMarshaller::MarkMessagesCacheAsDirty()
{
bNumMessagesCacheDirty = true;
}
FOutputLogTextLayoutMarshaller::FOutputLogTextLayoutMarshaller(TArray< TSharedPtr<FLogMessage> > InMessages, FLogFilter* InFilter)
: Messages(MoveTemp(InMessages))
, Filter(InFilter)
, TextLayout(nullptr)
{
}
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SOutputLog::Construct( const FArguments& InArgs )
{
MessagesTextMarshaller = FOutputLogTextLayoutMarshaller::Create(MoveTemp(InArgs._Messages), &Filter);
MessagesTextBox = SNew(SMultiLineEditableTextBox)
.Style(FEditorStyle::Get(), "Log.TextBox")
.TextStyle(FEditorStyle::Get(), "Log.Normal")
.ForegroundColor(FLinearColor::Gray)
.Marshaller(MessagesTextMarshaller)
.IsReadOnly(true)
.AlwaysShowScrollbars(true)
.OnVScrollBarUserScrolled(this, &SOutputLog::OnUserScrolled)
.ContextMenuExtender(this, &SOutputLog::ExtendTextBoxMenu);
ChildSlot
[
SNew(SVerticalBox)
// Console output and filters
+SVerticalBox::Slot()
[
SNew(SBorder)
.Padding(3)
.BorderImage(FEditorStyle::GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
// Output Log Filter
+SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(0.0f, 0.0f, 0.0f, 4.0f))
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SComboButton)
.ComboButtonStyle(FEditorStyle::Get(), "OutputLog.Filters.Style")
.ForegroundColor(FLinearColor::White)
.ContentPadding(0)
.ToolTipText(LOCTEXT("AddFilterToolTip", "Add an output log filter."))
.OnGetMenuContent(this, &SOutputLog::MakeAddFilterMenu)
.HasDownArrow(true)
.ContentPadding(FMargin(1, 0))
.ButtonContent()
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
[
SNew(STextBlock)
.TextStyle(FEditorStyle::Get(), "OutputLog.Filters.Text")
.Font(FEditorStyle::Get().GetFontStyle("FontAwesome.9"))
.Text(FText::FromString(FString(TEXT("\xf0b0"))) /*fa-filter*/)
]
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(2, 0, 0, 0)
[
SNew(STextBlock)
.TextStyle(FEditorStyle::Get(), "OutputLog.Filters.Text")
.Text(LOCTEXT("Filters", "Filters"))
]
]
]
+SHorizontalBox::Slot()
.Padding(4, 1, 0, 0)
[
SAssignNew(FilterTextBox, SSearchBox)
.HintText(LOCTEXT("SearchLogHint", "Search Log"))
.OnTextChanged(this, &SOutputLog::OnFilterTextChanged)
.DelayChangeNotificationsWhileTyping(true)
]
]
// Output log area
+SVerticalBox::Slot()
.FillHeight(1)
[
MessagesTextBox.ToSharedRef()
]
]
]
// The console input box
+SVerticalBox::Slot()
.AutoHeight()
.Padding(FMargin(0.0f, 4.0f, 0.0f, 0.0f))
[
SNew(SConsoleInputBox)
.OnConsoleCommandExecuted(this, &SOutputLog::OnConsoleCommandExecuted)
// Always place suggestions above the input line for the output log widget
.SuggestionListPlacement(MenuPlacement_AboveAnchor)
]
];
GLog->AddOutputDevice(this);
bIsUserScrolled = false;
RequestForceScroll();
}
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<FLogMessage> >& OutMessages )
{
if (Verbosity == ELogVerbosity::SetColor)
{
// Skip Color Events
return false;
}
else
{
FName Style;
if (Category == NAME_Cmd)
{
Style = FName(TEXT("Log.Command"));
}
else if (Verbosity == ELogVerbosity::Error)
{
Style = FName(TEXT("Log.Error"));
}
else if (Verbosity == ELogVerbosity::Warning)
{
Style = FName(TEXT("Log.Warning"));
}
else
{
Style = FName(TEXT("Log.Normal"));
}
// Determine how to format timestamps
static ELogTimes::Type LogTimestampMode = ELogTimes::None;
if (UObjectInitialized() && !GExitPurge)
{
// Logging can happen very late during shutdown, even after the UObject system has been torn down, hence the init check above
LogTimestampMode = GetDefault<UEditorStyleSettings>()->LogTimestampMode;
}
const int32 OldNumMessages = OutMessages.Num();
// handle multiline strings by breaking them apart by line
TArray<FTextRange> LineRanges;
FString CurrentLogDump = V;
FTextRange::CalculateLineRangesFromString(CurrentLogDump, LineRanges);
bool bIsFirstLineInMessage = true;
for (const FTextRange& LineRange : LineRanges)
{
if (!LineRange.IsEmpty())
{
FString Line = CurrentLogDump.Mid(LineRange.BeginIndex, LineRange.Len());
Line = Line.ConvertTabsToSpaces(4);
// Hard-wrap lines to avoid them being too long
static const int32 HardWrapLen = 360;
for (int32 CurrentStartIndex = 0; CurrentStartIndex < Line.Len();)
{
int32 HardWrapLineLen = 0;
if (bIsFirstLineInMessage)
{
FString MessagePrefix = FOutputDeviceHelper::FormatLogLine(Verbosity, Category, nullptr, LogTimestampMode);
HardWrapLineLen = FMath::Min(HardWrapLen - MessagePrefix.Len(), Line.Len() - CurrentStartIndex);
FString HardWrapLine = Line.Mid(CurrentStartIndex, HardWrapLineLen);
OutMessages.Add(MakeShareable(new FLogMessage(MakeShareable(new FString(MessagePrefix + HardWrapLine)), Verbosity, Style)));
}
else
{
HardWrapLineLen = FMath::Min(HardWrapLen, Line.Len() - CurrentStartIndex);
FString HardWrapLine = Line.Mid(CurrentStartIndex, HardWrapLineLen);
OutMessages.Add(MakeShareable(new FLogMessage(MakeShareable(new FString(MoveTemp(HardWrapLine))), Verbosity, Style)));
}
bIsFirstLineInMessage = false;
CurrentStartIndex += HardWrapLineLen;
}
}
}
return OldNumMessages != OutMessages.Num();
}
}
void SOutputLog::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category)
{
if ( MessagesTextMarshaller->AppendMessage(V, Verbosity, Category) )
{
// Don't scroll to the bottom automatically when the user is scrolling the view or has scrolled it away from the bottom.
if( !bIsUserScrolled )
{
RequestForceScroll();
}
}
}
void SOutputLog::ExtendTextBoxMenu(FMenuBuilder& Builder)
{
FUIAction ClearOutputLogAction(
FExecuteAction::CreateRaw( this, &SOutputLog::OnClearLog ),
FCanExecuteAction::CreateSP( this, &SOutputLog::CanClearLog )
);
Builder.AddMenuEntry(
NSLOCTEXT("OutputLog", "ClearLogLabel", "Clear Log"),
NSLOCTEXT("OutputLog", "ClearLogTooltip", "Clears all log messages"),
FSlateIcon(),
ClearOutputLogAction
);
}
void SOutputLog::OnClearLog()
{
// Make sure the cursor is back at the start of the log before we clear it
MessagesTextBox->GoTo(FTextLocation(0));
MessagesTextMarshaller->ClearMessages();
MessagesTextBox->Refresh();
bIsUserScrolled = false;
}
void SOutputLog::OnUserScrolled(float ScrollOffset)
{
bIsUserScrolled = !FMath::IsNearlyEqual(ScrollOffset, 1.0f);
}
bool SOutputLog::CanClearLog() const
{
return MessagesTextMarshaller->GetNumMessages() > 0;
}
void SOutputLog::OnConsoleCommandExecuted()
{
RequestForceScroll();
}
void SOutputLog::RequestForceScroll()
{
if (MessagesTextMarshaller->GetNumFilteredMessages() > 0)
{
MessagesTextBox->ScrollTo(FTextLocation(MessagesTextMarshaller->GetNumFilteredMessages() - 1));
bIsUserScrolled = false;
}
}
void SOutputLog::Refresh()
{
// Re-count messages if filter changed before we refresh
MessagesTextMarshaller->CountMessages();
MessagesTextBox->GoTo(FTextLocation(0));
MessagesTextMarshaller->MakeDirty();
MessagesTextBox->Refresh();
RequestForceScroll();
}
void SOutputLog::OnFilterTextChanged(const FText& InFilterText)
{
// Flag the messages count as dirty
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
// Set filter phrases
Filter.SetFilterText(InFilterText);
// Report possible syntax errors back to the user
FilterTextBox->SetError(Filter.GetSyntaxErrors());
// Repopulate the list to show only what has not been filtered out.
Refresh();
}
TSharedRef<SWidget> SOutputLog::MakeAddFilterMenu()
{
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
FillVerbosityEntries(MenuBuilder);
return MenuBuilder.MakeWidget();
}
void SOutputLog::FillVerbosityEntries(FMenuBuilder& MenuBuilder)
{
MenuBuilder.BeginSection("OutputLogVerbosityEntries");
{
MenuBuilder.AddMenuEntry(
LOCTEXT("ShowMessages", "Messages"),
LOCTEXT("ShowMessages_Tooltip", "Filter the Output Log to show messages"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::MenuLogs_Execute),
FCanExecuteAction::CreateSP(this, &SOutputLog::Menu_CanExecute),
FIsActionChecked::CreateSP(this, &SOutputLog::MenuLogs_IsChecked)),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
MenuBuilder.AddMenuEntry(
LOCTEXT("ShowWarnings", "Warnings"),
LOCTEXT("ShowWarnings_Tooltip", "Filter the Output Log to show warnings"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::MenuWarnings_Execute),
FCanExecuteAction::CreateSP(this, &SOutputLog::Menu_CanExecute),
FIsActionChecked::CreateSP(this, &SOutputLog::MenuWarnings_IsChecked)),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
MenuBuilder.AddMenuEntry(
LOCTEXT("ShowErrors", "Errors"),
LOCTEXT("ShowErrors_Tooltip", "Filter the Output Log to show errors"),
FSlateIcon(),
FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::MenuErrors_Execute),
FCanExecuteAction::CreateSP(this, &SOutputLog::Menu_CanExecute),
FIsActionChecked::CreateSP(this, &SOutputLog::MenuErrors_IsChecked)),
NAME_None,
EUserInterfaceActionType::ToggleButton
);
}
MenuBuilder.EndSection();
}
bool SOutputLog::Menu_CanExecute() const
{
return true;
}
bool SOutputLog::MenuLogs_IsChecked() const
{
return Filter.bShowLogs;
}
bool SOutputLog::MenuWarnings_IsChecked() const
{
return Filter.bShowWarnings;
}
bool SOutputLog::MenuErrors_IsChecked() const
{
return Filter.bShowErrors;
}
void SOutputLog::MenuLogs_Execute()
{
Filter.bShowLogs = !Filter.bShowLogs;
// Flag the messages count as dirty
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
Refresh();
}
void SOutputLog::MenuWarnings_Execute()
{
Filter.bShowWarnings = !Filter.bShowWarnings;
// Flag the messages count as dirty
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
Refresh();
}
void SOutputLog::MenuErrors_Execute()
{
Filter.bShowErrors = !Filter.bShowErrors;
// Flag the messages count as dirty
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
Refresh();
}
bool FLogFilter::IsMessageAllowed(const TSharedPtr<FLogMessage>& Message)
{
// Filter Verbosity
{
if (Message->Verbosity == ELogVerbosity::Error && !bShowErrors)
{
return false;
}
if (Message->Verbosity == ELogVerbosity::Warning && !bShowWarnings)
{
return false;
}
if (Message->Verbosity != ELogVerbosity::Error && Message->Verbosity != ELogVerbosity::Warning && !bShowLogs)
{
return false;
}
}
// Filter search phrase
{
if (!TextFilterExpressionEvaluator.TestTextFilter(FLogFilter_TextFilterExpressionContext(*Message)))
{
return false;
}
}
return true;
}