You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
========================== MAJOR FEATURES + CHANGES ========================== Change 2835191 on 2016/01/19 by Nick.Whiting Invert the y-axis on the SteamVR controllers to match the convention of the engine and the rest of the gamepads #jira UE-22705 Change 2835686 on 2016/01/20 by Gareth.Martin Fixed landscape material instances not being updated if holes are painted on a landscape that doesn't have the landscape visibility mask node in the material and then the visibility mask node is added to the material later. #jira UE-18187 Change 2835767 on 2016/01/20 by Richard.Hinckley #jira UE-25499 Added a cursor to TopDown template (C++ version) to match the BP version. Change 2835772 on 2016/01/20 by Richard.Hinckley #jira UE-25499 Adding the material asset for the C++ TopDown template's cursor. Change 2835811 on 2016/01/20 by Taizyd.Korambayil #jira UE-25699 Added Validity Checks in BP logic, unchecked CDO for Pixel Ship, to Fix Log Warnings #jira UE-25704 Adjusted Matinee to happen at Box Location #jira UE-25688 Adjusted Player Starts #jira UE-25693 Adjusted Player Starts Change 2835863 on 2016/01/20 by Gareth.Martin Fixed crash in the landscape ramp and mirror tools if the streaming level containing the landscape is hidden (or possibly if the landscape actor is deleted) #jira UE-24883 Change 2835889 on 2016/01/20 by Taizyd.Korambayil #jira UE-25698 Enabled V-sync, also fixed up player Respawn Issue Change 2835995 on 2016/01/20 by Jamie.Dale The output log now hard-wraps lines to prevent long lines causing performance issues #jira UE-24187 Change 2836052 on 2016/01/20 by Taizyd.Korambayil #jira UE-25675 Added Blocking Volume to prevent Player from Falling off map #jira UE-25676 Added Blocking Volumes so that the Player doesn't get stucl at awkward corners under the Bridge Change 2836137 on 2016/01/20 by Chad.Taylor Vehicle and VehicleAdv template content fixes for new VR camera #jira UE-25507 Change 2836166 on 2016/01/20 by Gareth.Martin Fixed hiding a streaming level containing a landscape causing the landscape editor to switch to the "New Landscape" tool instead of exiting #jira UE-25093 Change 2836174 on 2016/01/20 by Chad.Taylor IHeadMountedDisplay crash fix associated with accessing a dangling pointer. #jira UE-25272 Change 2836179 on 2016/01/20 by Jamie.Dale Optimized FShapedGlyphSequence reverse look-up There's now a reverse look-up map of cluster indices to their glyph data in order to avoid brute force looping #jira UE-24187 Change 2836286 on 2016/01/20 by Chris.Babcock Update Qualcomm TextureConverter for OSX #jira UE-22092 #ue4 #android Change 2836328 on 2016/01/20 by Nick.Darnell Fixing a problem with widget components crashing on destruction with the render commands to pre/post render for window render commands needing access to the policy, but it potentially being deleted. Inserting a NoOp command that keeps the shared ptr alive through the RHI render process. #jira UE-25752 Change 2836342 on 2016/01/20 by Nick.Darnell Depending on shutdown order, the Slate Renderer may go away, and then render data handles may not be collected correctly because they are trying to reference a pointer that's no longer valid and cause a crash on exit. The correct approach would be to have render handles actually have a pointer back to who owns them, in this case the RHI Resource Manager, which is still alive and well at this point in the pipeline. Then if the resource manager is collected, it forces all handles to get cleaned up correctly, or if the handles are collected first, they can be sure they've got a valid pointer back to the resource manager. #jira UE-25753 Change 2836358 on 2016/01/20 by Taizyd.Korambayil #jira UE-25710 Replaced Deprecated Nodes Change 2836510 on 2016/01/20 by Taizyd.Korambayil #jira UE-25718 Adjsuted BP to make pointer decal rotate in the direction of surface Change 2836564 on 2016/01/20 by Taizyd.Korambayil #jira UE-25716 Added bool to store last Moved Direction Change 2836697 on 2016/01/20 by Taizyd.Korambayil #jira UE-25740 Removed unused VR Nodes to remove Log errors on Mac Change 2836725 on 2016/01/20 by Peter.Sauerbrei workaround for thread race when trying to release the TargetDeviceService endpoint after an unclaim message is sent #jira UE-25123 Change 2836782 on 2016/01/20 by Jamie.Dale Added FTextLayout::AddLines This is similar to AddLine, however it allows you to add multiple lines in a single call, thus avoiding the re-justification cost associated with each call to AddLine. AddLine has also been changed to take the same structure type as AddLines (which takes an array of these structures), and the existing version of AddLine has been deprecated. #jira UE-24187 Change 2836801 on 2016/01/20 by Jeff.Campeau [CL 2857187 by Matthew Griffin in Main branch]
837 lines
22 KiB
C++
837 lines
22 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"
|
|
/** 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)
|
|
{
|
|
return MakeShareable(new FOutputLogTextLayoutMarshaller(MoveTemp(InMessages)));
|
|
}
|
|
|
|
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
|
|
{
|
|
MakeDirty();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FOutputLogTextLayoutMarshaller::AppendMessageToTextLayout(const TSharedPtr<FLogMessage>& InMessage)
|
|
{
|
|
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)
|
|
{
|
|
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();
|
|
}
|
|
|
|
int32 FOutputLogTextLayoutMarshaller::GetNumMessages() const
|
|
{
|
|
return Messages.Num();
|
|
}
|
|
|
|
FOutputLogTextLayoutMarshaller::FOutputLogTextLayoutMarshaller(TArray< TSharedPtr<FLogMessage> > InMessages)
|
|
: Messages(MoveTemp(InMessages))
|
|
, TextLayout(nullptr)
|
|
{
|
|
}
|
|
|
|
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
void SOutputLog::Construct( const FArguments& InArgs )
|
|
{
|
|
MessagesTextMarshaller = FOutputLogTextLayoutMarshaller::Create(MoveTemp(InArgs._Messages));
|
|
|
|
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)
|
|
|
|
// 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 = FOutputDevice::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)), 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))), 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 )
|
|
{
|
|
MessagesTextBox->ScrollTo(FTextLocation(MessagesTextMarshaller->GetNumMessages() - 1));
|
|
}
|
|
}
|
|
}
|
|
|
|
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->GetNumMessages() > 0)
|
|
{
|
|
MessagesTextBox->ScrollTo(FTextLocation(MessagesTextMarshaller->GetNumMessages() - 1));
|
|
bIsUserScrolled = false;
|
|
}
|
|
}
|