You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#lockdown Nick.Penwarden #rb none ========================== MAJOR FEATURES + CHANGES ========================== Change 3209340 on 2016/11/23 by Ben.Marsh Convert UE4 codebase to an "include what you use" model - where every header just includes the dependencies it needs, rather than every source file including large monolithic headers like Engine.h and UnrealEd.h. Measured full rebuild times around 2x faster using XGE on Windows, and improvements of 25% or more for incremental builds and full rebuilds on most other platforms. * Every header now includes everything it needs to compile. * There's a CoreMinimal.h header that gets you a set of ubiquitous types from Core (eg. FString, FName, TArray, FVector, etc...). Most headers now include this first. * There's a CoreTypes.h header that sets up primitive UE4 types and build macros (int32, PLATFORM_WIN64, etc...). All headers in Core include this first, as does CoreMinimal.h. * Every .cpp file includes its matching .h file first. * This helps validate that each header is including everything it needs to compile. * No engine code includes a monolithic header such as Engine.h or UnrealEd.h any more. * You will get a warning if you try to include one of these from the engine. They still exist for compatibility with game projects and do not produce warnings when included there. * There have only been minor changes to our internal games down to accommodate these changes. The intent is for this to be as seamless as possible. * No engine code explicitly includes a precompiled header any more. * We still use PCHs, but they're force-included on the compiler command line by UnrealBuildTool instead. This lets us tune what they contain without breaking any existing include dependencies. * PCHs are generated by a tool to get a statistical amount of coverage for the source files using it, and I've seeded the new shared PCHs to contain any header included by > 15% of source files. Tool used to generate this transform is at Engine\Source\Programs\IncludeTool. [CL 3209342 by Ben Marsh in Main branch]
1197 lines
31 KiB
C++
1197 lines
31 KiB
C++
// Copyright 1998-2016 Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "SOutputLog.h"
|
|
#include "Framework/Text/TextRange.h"
|
|
#include "Framework/Text/IRun.h"
|
|
#include "Framework/Text/TextLayout.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "Misc/OutputDeviceHelper.h"
|
|
#include "SlateOptMacros.h"
|
|
#include "Textures/SlateIcon.h"
|
|
#include "Framework/Commands/UIAction.h"
|
|
#include "Framework/Text/SlateTextLayout.h"
|
|
#include "Framework/Text/SlateTextRun.h"
|
|
#include "Widgets/Text/STextBlock.h"
|
|
#include "Widgets/Input/SMenuAnchor.h"
|
|
#include "Framework/MultiBox/MultiBoxBuilder.h"
|
|
#include "Widgets/Input/SMultiLineEditableTextBox.h"
|
|
#include "Widgets/Input/SComboButton.h"
|
|
#include "Widgets/Views/SListView.h"
|
|
#include "EditorStyleSet.h"
|
|
#include "EditorStyleSettings.h"
|
|
#include "EngineGlobals.h"
|
|
#include "Editor.h"
|
|
#include "Engine/LocalPlayer.h"
|
|
#include "GameFramework/GameStateBase.h"
|
|
#include "Widgets/Input/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)
|
|
]
|
|
]
|
|
)
|
|
];
|
|
}
|
|
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
|
|
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, Mid, Right, TempRight, Combined;
|
|
|
|
if(Text->Split(TEXT("\t"), &Left, &TempRight))
|
|
{
|
|
if (TempRight.Split(TEXT("\t"), &Mid, &Right))
|
|
{
|
|
Combined = Left + Mid + Right;
|
|
}
|
|
else
|
|
{
|
|
Combined = Left + Right;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Combined = *Text;
|
|
}
|
|
|
|
FText HighlightText = FText::FromString(Mid);
|
|
|
|
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().ForEachConsoleObjectThatContains(
|
|
FConsoleObjectVisitor::CreateStatic< TArray<FString>& >(
|
|
&FConsoleVariableAutoCompleteVisitor::OnConsoleVariable,
|
|
AutoCompleteList ), *InputTextStr);
|
|
}
|
|
|
|
AutoCompleteList.Sort();
|
|
|
|
for(uint32 i = 0; i < (uint32)AutoCompleteList.Num(); ++i)
|
|
{
|
|
FString &ref = AutoCompleteList[i];
|
|
int32 Start = ref.Find(InputTextStr);
|
|
|
|
if (Start != INDEX_NONE)
|
|
{
|
|
ref = ref.Left(Start) + TEXT("\t") + ref.Mid(Start, InputTextStr.Len()) + TEXT("\t") + ref.RightChop(Start + 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 )
|
|
{
|
|
AGameModeBase* const GameMode = World->GetAuthGameMode();
|
|
AGameStateBase* const GameState = World->GetGameState();
|
|
if( GameMode && GameMode->ProcessConsoleExec( *ExecString, *GLog, NULL ) )
|
|
{
|
|
bWasHandled = true;
|
|
}
|
|
else if (GameState && 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 if we're not rebuilding the log
|
|
if ( !IsDirty() )
|
|
{
|
|
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());
|
|
|
|
int32 NumAddedMessages = 0;
|
|
|
|
for (const auto& CurrentMessage : InMessages)
|
|
{
|
|
if (!Filter->IsMessageAllowed(CurrentMessage))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
++NumAddedMessages;
|
|
|
|
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));
|
|
}
|
|
|
|
// Increment the cached message count if the log is not being rebuilt
|
|
if ( !IsDirty() )
|
|
{
|
|
CachedNumMessages += NumAddedMessages;
|
|
}
|
|
|
|
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))
|
|
, CachedNumMessages(0)
|
|
, 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)
|
|
.OnTextCommitted(this, &SOutputLog::OnFilterTextCommitted)
|
|
.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 = ScrollOffset < 1.0 && !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)
|
|
{
|
|
if (Filter.GetFilterText().ToString().Equals(InFilterText.ToString(), ESearchCase::CaseSensitive))
|
|
{
|
|
// nothing to do
|
|
return;
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
void SOutputLog::OnFilterTextCommitted(const FText& InFilterText, ETextCommit::Type InCommitType)
|
|
{
|
|
OnFilterTextChanged(InFilterText);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|