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 #rnx ============================ MAJOR FEATURES & CHANGES ============================ Change 3967603 by Marc.Audy Fix force feedback events being played on the correct controller if the OffsetPlayerGamepadIds feature is in use #jira UE-56077 Change 3969399 by Phillip.Kavan Partially fix runtime failures for converted circular dependency test actors in a nativized QAGame. #jira UE-56287 Change 3970181 by Phillip.Kavan Improved handling of circular dependencies during Blueprint nativization between converted and unconverted assets. Change summary: - Removed FGatherConvertedClassDependencies::MarkUnconvertedClassAsNecessary(); no longer in use. - Modified FFindAssetsToInclude::MaybeIncludeObjectAsDependency() to remove the case that was marking assets for stub class generation; no longer needed. - Modified FBlueprintNativeCodeGenModule::IsTargetedForReplacement() to detect, report on and handle circular references between converted and unconverted assets (ensuring that both sides are converted). #jira UE-56292 Change 3970185 by Phillip.Kavan Fix initialization of special UMG struct types in nativized Blueprint ctor code. Change summary: - Modified FBackendHelperUMG::SpecialStructureConstructorUMG() to emit the proper constructor call based on the value of the 'ForcedTime' field. Also modified to emit updated constructor code for the FMovieSceneSegment struct's 'Range' member, which is now a TRange<> type. - Fixed a typo in the TBaseStructure<FInt32Range> case in FEmitDefaultValueHelper::SpecialStructureConstructor(). #jira UE-56487 Change 3971520 by mason.seay Adjusted widget anchor to fix screen positioning Change 3972611 by Mieszko.Zielinski Fixed some of the vislogger macros using old 'Actor' rather than new 'LogOwner' parameter #UE4 #jira UE-56978 Change 3973564 by Phillip.Kavan Fix missing dependency on linker response file. Prevents target being relinked when build environment changes. - Mirrored from //UE4/Dev-Core (3973331). #jira UE-56277 Change 3974308 by Phillip.Kavan Fix crash when choosing to delete multiple bookmarks selected in the Bookmarks tree view. Change summary: - Modified SBlueprintBookmarks::OnDeleteSelectedTreeViewItems() to make a local copy of the reference to the entry in the source array. - Modified FBlueprintEditor::RemoveBookmark() to save local config settings *after* modifying the persistent Bookmarks array (it was previously happening before). #jira UE-55598 Change 3975506 by Mason.Seay Code and blueprints for subobject testing Change 3977558 by Mason.Seay Checking in missed header file Change 3978137 by Ben.Zeigler #jira UE-57154 Fix crash where component instance data cache would duplicate external objects during the null component pre serialize Change 3978326 by Ben.Zeigler #jira UE-57062 Only look for class redirects for soft object paths if the string starts with /script, some of the class redirect match strings are overly broad and were catching assets Change 3978393 by Ben.Zeigler Fix FAssetData::IsUAsset comment to match what it actually does, old comment implied it was false for maps which is wrong Change 3978837 by Marc.Audy Make GetAutoEmitTermParameters and IsAutoCreateRefTerm static and optimize both Change 3978838 by Marc.Audy (4.19.1) PR #4480: Crash fix trying to open the console command on Windows (Contributed by bakjos) #jira UE-54908 #jira UE-54215 Change 3979156 by Marc.Audy PR #4615: Update WindDirectionalSourceComponent.h (Contributed by c4tnt) #jira UE-57071 Change 3982289 by Ben.Zeigler Fix several issues with unloaded blueprints and the class picker/viewer #jira UE-53646 Reimplement CR #4369 so setting a Class blueprint variable to a blueprint class will correctly allow unloaded blueprint children to be selected in the picker Added FName defines to FBlueprintTags for the asset registry tags used for unloaded blueprints, and removed the ParentClassPackage tag as it was redundant with ParentClass Fixed it so Soft Class Reference properties can be set back to none with the picker Removed FUnloadedClassDragDropOp as it did not work correctly and replaced uses with FAssetDragDrop, which was already used for content browser drags Changed internals of class viewer to properly store and use the paths to the actual generated class Change 3982684 by Ben.Zeigler Add DevelopmentAlwaysCook setting to the asset manager cook rule, this works like AlwaysCook in development and like NeverCook in production. DevelopmentCook works like Unknown in development Change 3982762 by Mieszko.Zielinski PR #4614: Fix UBTTask_RotateToFaceBBEntry when trying to face a position where i. (Contributed by Goutye) I've improved the original PR by relaxing condition constraints, and removing some unnecessary vector normalization (since FVecotr::CosineAngle2D normalizes the input parameters anyway). #jira UE-57070 Change 3982866 by Marc.Audy Defer Destroying an Actor if it is in the process of beginning play #jira UE-57091 Change 3982965 by Mieszko.Zielinski PR #3567: add API declarations to BlueprintNodeHelpers (Contributed by kayama-shift) Change 3987399 by Dan.Oconnor Remove unused ReloadObjectArc and unimplemented ArchiveReplaceArchetype #jira None Change 3987956 by Mieszko.Zielinski Fixed inconsistincies in BT category naming in BP #UE4 #jira UE-57262 Change 3988308 by Mieszko.Zielinski Fixed 'navmesh needs to be rebuild' displaying even after navigation building with navmesh auto building disabled #UE4 #jira UE-57286 Change 3989309 by Mieszko.Zielinski A support AI pluggin that links the aimodules #UE4 Change 3989311 by Mieszko.Zielinski Moved the HTNPlanner plugin into the newly created Plugins/AI subfolder #UE4 Change 3989506 by mason.seay WIP Navigation Test Map Change 3990826 by Phillip.Kavan Fix existing placements of nativized Blueprint actors with a nonzero (legacy) root component scene transform at the CDO level. This corrects a long-standing issue that was introduced back in 2947488. Change summary: - Modified AActor::PostSpawnInitialize() to zero out RelativeLocation and RelativeRotation when spawning actors with a native scene root inherited from a converted Blueprint class. - Modified FNonativeComponentData to remove the HandledAsSpecialProperty() override for skipping the RelativeLocation and RelativeRotation properties on root components when emitting code to initialize the CDO for a converted Actor BP with a non-native root component. #jira UE-50452 Change 3993174 by mason.seay Cleared out unnecessary BP logic Change 3994370 by Ben.Zeigler Remove manual AI includes, the AI support plugin correctly pulls in those references now Change 3995399 by Marc.Audy When destroying the world clear out level collections #jira UE-57257 Change 3995731 by Marc.Audy Fix UEngine::CommitMapChange issues with new streaming level state machine * Level needs to be added to array before determining current state * Should be adding, not setting streaming levels from fake world #jira UE-57424 Change 3996917 by Ben.Zeigler ObjectWriter should not reset the soft object weak pointer, it should be a const operation Change 3996921 by Ben.Zeigler #jira UE-57288 Improve ensures for invalid primary asset types, it only complains if type is set and gives a better error Change 3997164 by Dan.Oconnor Don't purge classes until they are recompiled and relink all child classes immediately after a blueprint generated class has changed. This allows iterative compilation logic to behave as it did before the compilation manager #jira UE-55667 Change 3997674 by paulo.souza Reverting camera setup changes from the renaming Change 3997901 by mason.seay Updates to subobject test bp's Change 3998028 by mason.seay Move logic to Event Graph Change 3998051 by Mason.Seay Adding QAGame ClassGroup to actively tested QAGame components for organization Change 3999438 by Dan.Oconnor Fix deterministic cooking issues by avoiding use of MakeUniqueObjectName when instantiating component templates #jira FORT-81409 Change 4000265 by Mason.Seay Checking in QAGame classes again to (hopefully) fix property issues Change 4000339 by Marc.Audy PR #4652: AttachComponent Message Fix (Contributed by nonlin) #jira UE-57656 Change 4001338 by Dan.Oconnor Fix reinstancing bug introduced in 3591939 which could cause some reinstanced objects to have references within them replaced. If the object in question was an actor it could take ownership of components owned by another actor and destroy them, resulting in a crash when the other actor tried to use its components #jira UE-57335 Change 4001400 by Marc.Audy Per https://udn.unrealengine.com/questions/407745/childactorcomponent-and-modifications-to-component.html don't redirect or reset loaders when renaming components as part of the component instance data cache. Change 4001530 by Marc.Audy Use a clean default object to calculate the empty archive size. Change 4002791 by Marc.Audy Fix ensure when loading sublevel that has been redirected #jira UE-57516 Change 4002812 by mason.seay Added BP code to test Watching Sets and Maps Change 4002947 by mason.seay More code for watch values testing Change4003059by paulo.souza Fixes to volumetric lighting and sphere capture bakes Change4003509by Dan.Oconnor Reset bIsFakingTouched and bIsGameFakingTouch when the game viewport is unregistered. This prevents the PIE session from tainting editor session behavior when UInputSettings::bUseMouseForTouch is set #jira UE-57724 Change 4005596 by Phillip.Kavan Fix random crashes when running multiple global blueprint search tabs in parallel just after editor launch. Change summary: - Refactored global FiB APIs to use thread-safe shared pointer types when referencing imaginary Blueprint data, which is a shared resource. - Modified FFindInBlueprintSearchManager::ContinueSearchQuery() to ensure that parsing imaginary Blueprint data only happens on a single thread. #jira UE-57046 Change 4005657 by Dan.Oconnor Don't attach OldRoot component if it's not outered to the current actor #jira UE-55300 Change 4005709 by Phillip.Kavan Ensure rather than assert on failed child promotion when destroying a non-root scene component within an Actor's scene component hierarchy. #jira UE-52921 Change 4005825 by paulo.souza New camera angle + touch rotation tests + "Action RPG" logos Change 4006058 by Sam.Deiter Removing this as it is not needed. Change 3980745 by Marc.Audy Apply CL# 3977198 from Dev-Core #author Ben.Marsh Remove INI file override for editor stack size on Windows. This is rarely valid since editor targets share build products with other games by deafult. Fix to add linker response file as prerequisite exposed targets overriding this as a bug. [CL 4006923 by Dan Oconnor in Main branch]
1481 lines
41 KiB
C++
1481 lines
41 KiB
C++
// Copyright 1998-2018 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 "Misc/ConfigCacheIni.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 "Framework/Application/SlateApplication.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 "Classes/EditorStyleSettings.h"
|
|
#include "EngineGlobals.h"
|
|
#include "Editor.h"
|
|
#include "Toolkits/GlobalEditorCommonCommands.h"
|
|
#include "Engine/LocalPlayer.h"
|
|
#include "GameFramework/GameStateBase.h"
|
|
#include "Widgets/Input/SSearchBox.h"
|
|
#include "Features/IModularFeatures.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "SOutputLog"
|
|
|
|
FName FConsoleCommandExecutor::StaticName()
|
|
{
|
|
static const FName CmdExecName = TEXT("Cmd");
|
|
return CmdExecName;
|
|
}
|
|
|
|
FName FConsoleCommandExecutor::GetName() const
|
|
{
|
|
return StaticName();
|
|
}
|
|
|
|
FText FConsoleCommandExecutor::GetDisplayName() const
|
|
{
|
|
return LOCTEXT("ConsoleCommandExecutorDisplayName", "Cmd");
|
|
}
|
|
|
|
FText FConsoleCommandExecutor::GetDescription() const
|
|
{
|
|
return LOCTEXT("ConsoleCommandExecutorDescription", "Execute Unreal Console Commands");
|
|
}
|
|
|
|
FText FConsoleCommandExecutor::GetHintText() const
|
|
{
|
|
return LOCTEXT("ConsoleCommandExecutorHintText", "Enter Console Command");
|
|
}
|
|
|
|
void FConsoleCommandExecutor::GetAutoCompleteSuggestions(const TCHAR* Input, TArray<FString>& Out)
|
|
{
|
|
auto OnConsoleVariable = [&Out](const TCHAR *Name, IConsoleObject* CVar)
|
|
{
|
|
#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;
|
|
}
|
|
|
|
Out.Add(Name);
|
|
};
|
|
|
|
IConsoleManager::Get().ForEachConsoleObjectThatContains(FConsoleObjectVisitor::CreateLambda(OnConsoleVariable), Input);
|
|
}
|
|
|
|
void FConsoleCommandExecutor::GetExecHistory(TArray<FString>& Out)
|
|
{
|
|
IConsoleManager::Get().GetConsoleHistory(TEXT(""), Out);
|
|
}
|
|
|
|
bool FConsoleCommandExecutor::Exec(const TCHAR* Input)
|
|
{
|
|
IConsoleManager::Get().AddConsoleHistoryEntry(TEXT(""), Input);
|
|
|
|
bool bWasHandled = false;
|
|
UWorld* World = nullptr;
|
|
UWorld* OldWorld = nullptr;
|
|
|
|
// 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, Input, *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(Input, *GLog, nullptr))
|
|
{
|
|
bWasHandled = true;
|
|
}
|
|
else if (GameState && GameState->ProcessConsoleExec(Input, *GLog, nullptr))
|
|
{
|
|
bWasHandled = true;
|
|
}
|
|
}
|
|
|
|
if (!bWasHandled)
|
|
{
|
|
if (GIsEditor)
|
|
{
|
|
bWasHandled = GEditor->Exec(World, Input, *GLog);
|
|
}
|
|
else
|
|
{
|
|
bWasHandled = GEngine->Exec(World, Input, *GLog);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore the old world of there was one
|
|
if (OldWorld)
|
|
{
|
|
RestoreEditorWorld(OldWorld);
|
|
}
|
|
|
|
return bWasHandled;
|
|
}
|
|
|
|
bool FConsoleCommandExecutor::AllowHotKeyClose() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
bool FConsoleCommandExecutor::AllowMultiLine() const
|
|
{
|
|
return false;
|
|
}
|
|
|
|
/** 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;
|
|
};
|
|
|
|
SConsoleInputBox::SConsoleInputBox()
|
|
: bIgnoreUIUpdate(false)
|
|
, bHasTicked(false)
|
|
{
|
|
}
|
|
|
|
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
void SConsoleInputBox::Construct(const FArguments& InArgs)
|
|
{
|
|
OnConsoleCommandExecuted = InArgs._OnConsoleCommandExecuted;
|
|
ConsoleCommandCustomExec = InArgs._ConsoleCommandCustomExec;
|
|
OnCloseConsole = InArgs._OnCloseConsole;
|
|
|
|
PreferredCommandExecutorName = FConsoleCommandExecutor::StaticName();
|
|
if (!ConsoleCommandCustomExec.IsBound()) // custom execs always show the default executor in the UI (which has the selector disabled)
|
|
{
|
|
FString PreferredCommandExecutorStr;
|
|
if (GConfig->GetString(TEXT("OutputLog"), TEXT("PreferredCommandExecutor"), PreferredCommandExecutorStr, GEditorPerProjectIni))
|
|
{
|
|
PreferredCommandExecutorName = *PreferredCommandExecutorStr;
|
|
}
|
|
}
|
|
SyncActiveCommandExecutor();
|
|
|
|
IModularFeatures::Get().OnModularFeatureRegistered().AddSP(this, &SConsoleInputBox::OnCommandExecutorRegistered);
|
|
IModularFeatures::Get().OnModularFeatureUnregistered().AddSP(this, &SConsoleInputBox::OnCommandExecutorUnregistered);
|
|
|
|
ChildSlot
|
|
[
|
|
SAssignNew( SuggestionBox, SMenuAnchor )
|
|
.Placement( InArgs._SuggestionListPlacement )
|
|
[
|
|
SNew(SHorizontalBox)
|
|
|
|
+SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.Padding(FMargin(0.0f, 0.0f, 4.0f, 0.0f))
|
|
[
|
|
SNew(SComboButton)
|
|
.IsEnabled(this, &SConsoleInputBox::IsCommandExecutorMenuEnabled)
|
|
.ComboButtonStyle(FEditorStyle::Get(), "GenericFilters.ComboButtonStyle")
|
|
.ForegroundColor(FLinearColor::White)
|
|
.ContentPadding(0)
|
|
.OnGetMenuContent(this, &SConsoleInputBox::GetCommandExecutorMenuContent)
|
|
.ButtonContent()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SConsoleInputBox::GetActiveCommandExecutorDisplayName)
|
|
]
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
[
|
|
SAssignNew(InputText, SMultiLineEditableTextBox)
|
|
.Font(FEditorStyle::Get().GetWidgetStyle<FTextBlockStyle>("Log.Normal").Font)
|
|
.HintText(this, &SConsoleInputBox::GetActiveCommandExecutorHintText)
|
|
.AllowMultiLine(this, &SConsoleInputBox::GetActiveCommandExecutorAllowMultiLine)
|
|
.OnTextCommitted(this, &SConsoleInputBox::OnTextCommitted)
|
|
.OnTextChanged(this, &SConsoleInputBox::OnTextChanged)
|
|
.OnKeyCharHandler(this, &SConsoleInputBox::OnKeyCharHandler)
|
|
.OnKeyDownHandler(this, &SConsoleInputBox::OnKeyDownHandler)
|
|
.OnIsTypedCharValid(FOnIsTypedCharValid::CreateLambda([](const TCHAR InCh) { return true; })) // allow tabs to be typed into the field
|
|
.ClearKeyboardFocusOnCommit(false)
|
|
.ModiferKeyForNewLine(EModifierKey::Shift)
|
|
]
|
|
]
|
|
.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
|
|
.MinDesiredWidth(300)
|
|
.MaxDesiredWidth(this, &SConsoleInputBox::GetSelectionListMaxWidth)
|
|
[
|
|
SAssignNew(SuggestionListView, SListView< TSharedPtr<FString> >)
|
|
.ListItemsSource(&Suggestions.SuggestionsList)
|
|
.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 )
|
|
{
|
|
bHasTicked = true;
|
|
|
|
if (!GIntraFrameDebuggingGameThread && !IsEnabled())
|
|
{
|
|
SetEnabled(true);
|
|
}
|
|
else if (GIntraFrameDebuggingGameThread && IsEnabled())
|
|
{
|
|
SetEnabled(false);
|
|
}
|
|
}
|
|
|
|
|
|
void SConsoleInputBox::SuggestionSelectionChanged(TSharedPtr<FString> NewValue, ESelectInfo::Type SelectInfo)
|
|
{
|
|
if(bIgnoreUIUpdate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Suggestions.SelectedSuggestion = Suggestions.SuggestionsList.IndexOfByPredicate([&NewValue](const TSharedPtr<FString>& InSuggestion)
|
|
{
|
|
return InSuggestion == NewValue;
|
|
});
|
|
|
|
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 );
|
|
}
|
|
|
|
FOptionalSize SConsoleInputBox::GetSelectionListMaxWidth() const
|
|
{
|
|
// Limit the width of the suggestions list to the work area that this widget currently resides on
|
|
const FSlateRect WidgetRect(GetCachedGeometry().GetAbsolutePosition(), GetCachedGeometry().GetAbsolutePosition() + GetCachedGeometry().GetAbsoluteSize());
|
|
const FSlateRect WidgetWorkArea = FSlateApplication::Get().GetWorkArea(WidgetRect);
|
|
return FMath::Max(300.0f, WidgetWorkArea.GetSize().X - 12.0f);
|
|
}
|
|
|
|
TSharedRef<ITableRow> SConsoleInputBox::MakeSuggestionListItemWidget(TSharedPtr<FString> Text, const TSharedRef<STableViewBase>& OwnerTable)
|
|
{
|
|
check(Text.IsValid());
|
|
|
|
FString SanitizedText = *Text;
|
|
SanitizedText.ReplaceInline(TEXT("\r\n"), TEXT("\n"), ESearchCase::CaseSensitive);
|
|
SanitizedText.ReplaceInline(TEXT("\r"), TEXT(" "), ESearchCase::CaseSensitive);
|
|
SanitizedText.ReplaceInline(TEXT("\n"), TEXT(" "), ESearchCase::CaseSensitive);
|
|
|
|
return
|
|
SNew(STableRow< TSharedPtr<FString> >, OwnerTable)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(FText::FromString(SanitizedText))
|
|
.TextStyle(FEditorStyle::Get(), "Log.Normal")
|
|
.HighlightText(Suggestions.SuggestionsHighlight)
|
|
];
|
|
}
|
|
|
|
void SConsoleInputBox::OnTextChanged(const FText& InText)
|
|
{
|
|
if(bIgnoreUIUpdate)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FString& InputTextStr = InputText->GetText().ToString();
|
|
if(!InputTextStr.IsEmpty())
|
|
{
|
|
TArray<FString> AutoCompleteList;
|
|
|
|
if (ActiveCommandExecutor)
|
|
{
|
|
ActiveCommandExecutor->GetAutoCompleteSuggestions(*InputTextStr, AutoCompleteList);
|
|
}
|
|
|
|
AutoCompleteList.Sort();
|
|
SetSuggestions(AutoCompleteList, FText::FromString(InputTextStr));
|
|
}
|
|
else
|
|
{
|
|
ClearSuggestions();
|
|
}
|
|
}
|
|
|
|
void SConsoleInputBox::OnTextCommitted( const FText& InText, ETextCommit::Type CommitInfo)
|
|
{
|
|
if (CommitInfo == ETextCommit::OnEnter)
|
|
{
|
|
if (!InText.IsEmpty())
|
|
{
|
|
// 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());
|
|
ClearSuggestions();
|
|
bIgnoreUIUpdate = false;
|
|
|
|
// Exec!
|
|
if (ConsoleCommandCustomExec.IsBound())
|
|
{
|
|
IConsoleManager::Get().AddConsoleHistoryEntry(TEXT(""), *ExecString);
|
|
ConsoleCommandCustomExec.Execute(ExecString);
|
|
}
|
|
else if (ActiveCommandExecutor)
|
|
{
|
|
ActiveCommandExecutor->Exec(*ExecString);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearSuggestions();
|
|
}
|
|
|
|
OnConsoleCommandExecuted.ExecuteIfBound();
|
|
}
|
|
}
|
|
|
|
FReply SConsoleInputBox::OnPreviewKeyDown(const FGeometry& MyGeometry, const FKeyEvent& KeyEvent)
|
|
{
|
|
if(SuggestionBox->IsOpen())
|
|
{
|
|
if(KeyEvent.GetKey() == EKeys::Up || KeyEvent.GetKey() == EKeys::Down)
|
|
{
|
|
Suggestions.StepSelectedSuggestion(KeyEvent.GetKey() == EKeys::Up ? -1 : +1);
|
|
MarkActiveSuggestion();
|
|
|
|
return FReply::Handled();
|
|
}
|
|
else if (KeyEvent.GetKey() == EKeys::Tab)
|
|
{
|
|
if (Suggestions.HasSuggestions())
|
|
{
|
|
if (Suggestions.HasSelectedSuggestion())
|
|
{
|
|
MarkActiveSuggestion();
|
|
OnTextCommitted(InputText->GetText(), ETextCommit::OnEnter);
|
|
}
|
|
else
|
|
{
|
|
Suggestions.SelectedSuggestion = 0;
|
|
MarkActiveSuggestion();
|
|
}
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
else if (KeyEvent.GetKey() == EKeys::Escape)
|
|
{
|
|
SuggestionBox->SetIsOpen(false);
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(KeyEvent.GetKey() == EKeys::Up)
|
|
{
|
|
// If the command field isn't empty we need you to have pressed Control+Up to summon the history (to make sure you're not just using caret navigation)
|
|
const bool bIsMultiLine = GetActiveCommandExecutorAllowMultiLine();
|
|
const bool bShowHistory = InputText->GetText().IsEmpty() || KeyEvent.IsControlDown();
|
|
if (bShowHistory)
|
|
{
|
|
TArray<FString> History;
|
|
if (ActiveCommandExecutor)
|
|
{
|
|
ActiveCommandExecutor->GetExecHistory(History);
|
|
}
|
|
|
|
SetSuggestions(History, FText::GetEmpty());
|
|
|
|
if(Suggestions.HasSuggestions())
|
|
{
|
|
Suggestions.StepSelectedSuggestion(-1);
|
|
MarkActiveSuggestion();
|
|
}
|
|
}
|
|
|
|
// Need to always handle this for single-line controls to avoid them invoking widget navigation
|
|
if (!bIsMultiLine || bShowHistory)
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
else if (KeyEvent.GetKey() == EKeys::Escape)
|
|
{
|
|
if (InputText->GetText().IsEmpty())
|
|
{
|
|
OnCloseConsole.ExecuteIfBound();
|
|
}
|
|
else
|
|
{
|
|
// Clear the console input area
|
|
bIgnoreUIUpdate = true;
|
|
InputText->SetText(FText::GetEmpty());
|
|
bIgnoreUIUpdate = false;
|
|
|
|
ClearSuggestions();
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SConsoleInputBox::SetSuggestions(TArray<FString>& Elements, FText Highlight)
|
|
{
|
|
FString SelectionText;
|
|
if (Suggestions.HasSelectedSuggestion())
|
|
{
|
|
SelectionText = *Suggestions.GetSelectedSuggestion();
|
|
}
|
|
|
|
Suggestions.Reset();
|
|
Suggestions.SuggestionsHighlight = Highlight;
|
|
|
|
for(int32 i = 0; i < Elements.Num(); ++i)
|
|
{
|
|
Suggestions.SuggestionsList.Add(MakeShared<FString>(Elements[i]));
|
|
|
|
if (Elements[i] == SelectionText)
|
|
{
|
|
Suggestions.SelectedSuggestion = i;
|
|
}
|
|
}
|
|
SuggestionListView->RequestListRefresh();
|
|
|
|
if(Suggestions.HasSuggestions())
|
|
{
|
|
// Ideally if the selection box is open the output window is not changing it's window title (flickers)
|
|
SuggestionBox->SetIsOpen(true, false);
|
|
if (Suggestions.HasSelectedSuggestion())
|
|
{
|
|
SuggestionListView->RequestScrollIntoView(Suggestions.GetSelectedSuggestion());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SuggestionBox->SetIsOpen(false);
|
|
}
|
|
}
|
|
|
|
void SConsoleInputBox::OnFocusLost( const FFocusEvent& InFocusEvent )
|
|
{
|
|
// SuggestionBox->SetIsOpen(false);
|
|
}
|
|
|
|
void SConsoleInputBox::MarkActiveSuggestion()
|
|
{
|
|
bIgnoreUIUpdate = true;
|
|
if (Suggestions.HasSelectedSuggestion())
|
|
{
|
|
TSharedPtr<FString> SelectedSuggestion = Suggestions.GetSelectedSuggestion();
|
|
|
|
SuggestionListView->SetSelection(SelectedSuggestion);
|
|
SuggestionListView->RequestScrollIntoView(SelectedSuggestion); // Ideally this would only scroll if outside of the view
|
|
|
|
InputText->SetText(FText::FromString(*SelectedSuggestion));
|
|
}
|
|
else
|
|
{
|
|
SuggestionListView->ClearSelection();
|
|
}
|
|
bIgnoreUIUpdate = false;
|
|
}
|
|
|
|
void SConsoleInputBox::ClearSuggestions()
|
|
{
|
|
SuggestionBox->SetIsOpen(false);
|
|
Suggestions.Reset();
|
|
}
|
|
|
|
void SConsoleInputBox::OnCommandExecutorRegistered(const FName& Type, class IModularFeature* ModularFeature)
|
|
{
|
|
if (Type == IConsoleCommandExecutor::ModularFeatureName())
|
|
{
|
|
SyncActiveCommandExecutor();
|
|
}
|
|
}
|
|
|
|
void SConsoleInputBox::OnCommandExecutorUnregistered(const FName& Type, class IModularFeature* ModularFeature)
|
|
{
|
|
if (Type == IConsoleCommandExecutor::ModularFeatureName() && ModularFeature == ActiveCommandExecutor)
|
|
{
|
|
SyncActiveCommandExecutor();
|
|
}
|
|
}
|
|
|
|
void SConsoleInputBox::SyncActiveCommandExecutor()
|
|
{
|
|
TArray<IConsoleCommandExecutor*> CommandExecutors = IModularFeatures::Get().GetModularFeatureImplementations<IConsoleCommandExecutor>(IConsoleCommandExecutor::ModularFeatureName());
|
|
ActiveCommandExecutor = nullptr;
|
|
|
|
// First try and match from the active name
|
|
for (IConsoleCommandExecutor* CommandExecutor : CommandExecutors)
|
|
{
|
|
if (CommandExecutor->GetName() == PreferredCommandExecutorName)
|
|
{
|
|
ActiveCommandExecutor = CommandExecutor;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Failing that, fallback to the default name
|
|
if (!ActiveCommandExecutor && PreferredCommandExecutorName != FConsoleCommandExecutor::StaticName())
|
|
{
|
|
for (IConsoleCommandExecutor* CommandExecutor : CommandExecutors)
|
|
{
|
|
if (CommandExecutor->GetName() == FConsoleCommandExecutor::StaticName())
|
|
{
|
|
ActiveCommandExecutor = CommandExecutor;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SConsoleInputBox::SetActiveCommandExecutor(const FName InExecName)
|
|
{
|
|
GConfig->SetString(TEXT("OutputLog"), TEXT("PreferredCommandExecutor"), *InExecName.ToString(), GEditorPerProjectIni);
|
|
PreferredCommandExecutorName = InExecName;
|
|
SyncActiveCommandExecutor();
|
|
}
|
|
|
|
FText SConsoleInputBox::GetActiveCommandExecutorDisplayName() const
|
|
{
|
|
if (ActiveCommandExecutor)
|
|
{
|
|
return ActiveCommandExecutor->GetDisplayName();
|
|
}
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
FText SConsoleInputBox::GetActiveCommandExecutorHintText() const
|
|
{
|
|
if (ActiveCommandExecutor)
|
|
{
|
|
return ActiveCommandExecutor->GetHintText();
|
|
}
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
bool SConsoleInputBox::GetActiveCommandExecutorAllowMultiLine() const
|
|
{
|
|
if (ActiveCommandExecutor)
|
|
{
|
|
return ActiveCommandExecutor->AllowMultiLine();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SConsoleInputBox::IsCommandExecutorMenuEnabled() const
|
|
{
|
|
return !ConsoleCommandCustomExec.IsBound(); // custom execs always show the default executor in the UI (which has the selector disabled)
|
|
}
|
|
|
|
TSharedRef<SWidget> SConsoleInputBox::GetCommandExecutorMenuContent()
|
|
{
|
|
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
|
|
|
|
MenuBuilder.BeginSection("CmdExecEntries");
|
|
{
|
|
TArray<IConsoleCommandExecutor*> CommandExecutors = IModularFeatures::Get().GetModularFeatureImplementations<IConsoleCommandExecutor>(IConsoleCommandExecutor::ModularFeatureName());
|
|
CommandExecutors.Sort([](IConsoleCommandExecutor& LHS, IConsoleCommandExecutor& RHS)
|
|
{
|
|
return LHS.GetDisplayName().CompareTo(RHS.GetDisplayName()) < 0;
|
|
});
|
|
|
|
for (const IConsoleCommandExecutor* CommandExecutor : CommandExecutors)
|
|
{
|
|
const bool bIsActiveCmdExec = ActiveCommandExecutor == CommandExecutor;
|
|
|
|
MenuBuilder.AddMenuEntry(
|
|
CommandExecutor->GetDisplayName(),
|
|
CommandExecutor->GetDescription(),
|
|
FSlateIcon(),
|
|
FUIAction(
|
|
FExecuteAction::CreateSP(this, &SConsoleInputBox::SetActiveCommandExecutor, CommandExecutor->GetName()),
|
|
FCanExecuteAction::CreateLambda([] { return true; }),
|
|
FIsActionChecked::CreateLambda([bIsActiveCmdExec] { return bIsActiveCmdExec; })
|
|
),
|
|
NAME_None,
|
|
EUserInterfaceActionType::RadioButton
|
|
);
|
|
}
|
|
}
|
|
MenuBuilder.EndSection();
|
|
|
|
return MenuBuilder.MakeWidget();
|
|
}
|
|
|
|
FReply SConsoleInputBox::OnKeyDownHandler(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
|
|
{
|
|
const FInputChord OpenConsoleChord = *FGlobalEditorCommonCommands::Get().OpenConsoleCommandBox->GetActiveChord(EMultipleKeyBindingIndex::Primary);
|
|
const FInputChord InputChord = FInputChord(InKeyEvent.GetKey(), EModifierKey::FromBools(InKeyEvent.IsControlDown(), InKeyEvent.IsAltDown(), InKeyEvent.IsShiftDown(), InKeyEvent.IsCommandDown()));
|
|
|
|
// Intercept the "open console" key
|
|
if ((!ActiveCommandExecutor || ActiveCommandExecutor->AllowHotKeyClose())
|
|
&& OpenConsoleChord == InputChord)
|
|
{
|
|
OnCloseConsole.ExecuteIfBound();
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
FReply SConsoleInputBox::OnKeyCharHandler(const FGeometry& MyGeometry, const FCharacterEvent& InCharacterEvent)
|
|
{
|
|
// A printable key may be used to open the console, so consume all characters before our first Tick
|
|
if (!bHasTicked)
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
const FInputChord OpenConsoleChord = *FGlobalEditorCommonCommands::Get().OpenConsoleCommandBox->GetActiveChord(EMultipleKeyBindingIndex::Primary);
|
|
|
|
const uint32* KeyCode = nullptr;
|
|
const uint32* CharCode = nullptr;
|
|
FInputKeyManager::Get().GetCodesFromKey(OpenConsoleChord.Key, KeyCode, CharCode);
|
|
if (CharCode == nullptr)
|
|
{
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
// Intercept the "open console" key
|
|
if ((!ActiveCommandExecutor || ActiveCommandExecutor->AllowHotKeyClose())
|
|
&& InCharacterEvent.GetCharacter() == (TCHAR)*CharCode
|
|
&& OpenConsoleChord.NeedsControl() == InCharacterEvent.IsControlDown()
|
|
&& OpenConsoleChord.NeedsAlt() == InCharacterEvent.IsAltDown()
|
|
&& OpenConsoleChord.NeedsShift() == InCharacterEvent.IsShiftDown()
|
|
&& OpenConsoleChord.NeedsCommand() == InCharacterEvent.IsCommandDown())
|
|
{
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
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);
|
|
|
|
// Add new message categories to the filter's available log categories
|
|
for (const auto& NewMessage : NewMessages)
|
|
{
|
|
Filter->AddAvailableLogCategory(NewMessage->Category);
|
|
}
|
|
|
|
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 )
|
|
{
|
|
// Build list of available log categories from historical logs
|
|
for (const auto& Message : InArgs._Messages)
|
|
{
|
|
Filter.AddAvailableLogCategory(Message->Category);
|
|
}
|
|
|
|
MessagesTextMarshaller = FOutputLogTextLayoutMarshaller::Create(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(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(), "GenericFilters.ComboButtonStyle")
|
|
.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(), "GenericFilters.TextStyle")
|
|
.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(), "GenericFilters.TextStyle")
|
|
.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(SBox)
|
|
.MaxDesiredHeight(180.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);
|
|
// Remove itself on crash (crashmalloc has limited memory and echoing logs here at that point is useless).
|
|
FCoreDelegates::OnHandleSystemError.AddRaw(this, &SOutputLog::OnCrash);
|
|
|
|
bIsUserScrolled = false;
|
|
RequestForceScroll();
|
|
}
|
|
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
|
|
SOutputLog::~SOutputLog()
|
|
{
|
|
if (GLog != nullptr)
|
|
{
|
|
GLog->RemoveOutputDevice(this);
|
|
}
|
|
FCoreDelegates::OnHandleSystemError.RemoveAll(this);
|
|
}
|
|
|
|
void SOutputLog::OnCrash()
|
|
{
|
|
if (GLog != nullptr)
|
|
{
|
|
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
|
|
{
|
|
// Get the style for this message. When piping output from child processes (eg. when cooking through the editor), we want to highlight messages
|
|
// according to their original verbosity, so also check for "Error:" and "Warning:" substrings. This is consistent with how the build system processes logs.
|
|
FName Style;
|
|
if (Category == NAME_Cmd)
|
|
{
|
|
Style = FName(TEXT("Log.Command"));
|
|
}
|
|
else if (Verbosity == ELogVerbosity::Error || FCString::Stristr(V, TEXT("Error:")) != nullptr)
|
|
{
|
|
Style = FName(TEXT("Log.Error"));
|
|
}
|
|
else if (Verbosity == ELogVerbosity::Warning || FCString::Stristr(V, TEXT("Warning:")) != nullptr)
|
|
{
|
|
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(MakeShared<FLogMessage>(MakeShared<FString>(MessagePrefix + HardWrapLine), Verbosity, Category, Style));
|
|
}
|
|
else
|
|
{
|
|
HardWrapLineLen = FMath::Min(HardWrapLen, Line.Len() - CurrentStartIndex);
|
|
FString HardWrapLine = Line.Mid(CurrentStartIndex, HardWrapLineLen);
|
|
|
|
OutMessages.Add(MakeShared<FLogMessage>(MakeShared<FString>(MoveTemp(HardWrapLine)), Verbosity, Category, 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();
|
|
|
|
// Apply the new search text
|
|
MessagesTextBox->BeginSearch(InFilterText);
|
|
}
|
|
|
|
void SOutputLog::OnFilterTextCommitted(const FText& InFilterText, ETextCommit::Type InCommitType)
|
|
{
|
|
OnFilterTextChanged(InFilterText);
|
|
}
|
|
|
|
TSharedRef<SWidget> SOutputLog::MakeAddFilterMenu()
|
|
{
|
|
FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr);
|
|
|
|
MenuBuilder.BeginSection("OutputLogVerbosityEntries", LOCTEXT("OutputLogVerbosityHeading", "Verbosity"));
|
|
{
|
|
MenuBuilder.AddMenuEntry(
|
|
LOCTEXT("ShowMessages", "Messages"),
|
|
LOCTEXT("ShowMessages_Tooltip", "Filter the Output Log to show messages"),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::VerbosityLogs_Execute),
|
|
FCanExecuteAction::CreateLambda([] { return true; }),
|
|
FIsActionChecked::CreateSP(this, &SOutputLog::VerbosityLogs_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::VerbosityWarnings_Execute),
|
|
FCanExecuteAction::CreateLambda([] { return true; }),
|
|
FIsActionChecked::CreateSP(this, &SOutputLog::VerbosityWarnings_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::VerbosityErrors_Execute),
|
|
FCanExecuteAction::CreateLambda([] { return true; }),
|
|
FIsActionChecked::CreateSP(this, &SOutputLog::VerbosityErrors_IsChecked)),
|
|
NAME_None,
|
|
EUserInterfaceActionType::ToggleButton
|
|
);
|
|
}
|
|
MenuBuilder.EndSection();
|
|
|
|
MenuBuilder.BeginSection("OutputLogMiscEntries", LOCTEXT("OutputLogMiscHeading", "Miscellaneous"));
|
|
{
|
|
MenuBuilder.AddSubMenu(
|
|
LOCTEXT("Categories", "Categories"),
|
|
LOCTEXT("SelectCategoriesToolTip", "Select Categories to display."),
|
|
FNewMenuDelegate::CreateSP(this, &SOutputLog::MakeSelectCategoriesSubMenu)
|
|
);
|
|
}
|
|
|
|
return MenuBuilder.MakeWidget();
|
|
}
|
|
|
|
void SOutputLog::MakeSelectCategoriesSubMenu(FMenuBuilder& MenuBuilder)
|
|
{
|
|
MenuBuilder.BeginSection("OutputLogCategoriesEntries");
|
|
{
|
|
MenuBuilder.AddMenuEntry(
|
|
LOCTEXT("ShowAllCategories", "Show All"),
|
|
LOCTEXT("ShowAllCategories_Tooltip", "Filter the Output Log to show all categories"),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::CategoriesShowAll_Execute),
|
|
FCanExecuteAction::CreateLambda([] { return true; }),
|
|
FIsActionChecked::CreateSP(this, &SOutputLog::CategoriesShowAll_IsChecked)),
|
|
NAME_None,
|
|
EUserInterfaceActionType::ToggleButton
|
|
);
|
|
|
|
for (const FName Category : Filter.GetAvailableLogCategories())
|
|
{
|
|
MenuBuilder.AddMenuEntry(
|
|
FText::AsCultureInvariant(Category.ToString()),
|
|
FText::Format(LOCTEXT("Category_Tooltip", "Filter the Output Log to show category: {0}"), FText::AsCultureInvariant(Category.ToString())),
|
|
FSlateIcon(),
|
|
FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::CategoriesSingle_Execute, Category),
|
|
FCanExecuteAction::CreateLambda([] { return true; }),
|
|
FIsActionChecked::CreateSP(this, &SOutputLog::CategoriesSingle_IsChecked, Category)),
|
|
NAME_None,
|
|
EUserInterfaceActionType::ToggleButton
|
|
);
|
|
}
|
|
}
|
|
MenuBuilder.EndSection();
|
|
}
|
|
|
|
bool SOutputLog::VerbosityLogs_IsChecked() const
|
|
{
|
|
return Filter.bShowLogs;
|
|
}
|
|
|
|
bool SOutputLog::VerbosityWarnings_IsChecked() const
|
|
{
|
|
return Filter.bShowWarnings;
|
|
}
|
|
|
|
bool SOutputLog::VerbosityErrors_IsChecked() const
|
|
{
|
|
return Filter.bShowErrors;
|
|
}
|
|
|
|
void SOutputLog::VerbosityLogs_Execute()
|
|
{
|
|
Filter.bShowLogs = !Filter.bShowLogs;
|
|
|
|
// Flag the messages count as dirty
|
|
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
|
|
|
|
Refresh();
|
|
}
|
|
|
|
void SOutputLog::VerbosityWarnings_Execute()
|
|
{
|
|
Filter.bShowWarnings = !Filter.bShowWarnings;
|
|
|
|
// Flag the messages count as dirty
|
|
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
|
|
|
|
Refresh();
|
|
}
|
|
|
|
void SOutputLog::VerbosityErrors_Execute()
|
|
{
|
|
Filter.bShowErrors = !Filter.bShowErrors;
|
|
|
|
// Flag the messages count as dirty
|
|
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
|
|
|
|
Refresh();
|
|
}
|
|
|
|
bool SOutputLog::CategoriesShowAll_IsChecked() const
|
|
{
|
|
return Filter.bShowAllCategories;
|
|
}
|
|
|
|
bool SOutputLog::CategoriesSingle_IsChecked(FName InName) const
|
|
{
|
|
return Filter.IsLogCategoryEnabled(InName);
|
|
}
|
|
|
|
void SOutputLog::CategoriesShowAll_Execute()
|
|
{
|
|
Filter.bShowAllCategories = !Filter.bShowAllCategories;
|
|
|
|
Filter.ClearSelectedLogCategories();
|
|
if (Filter.bShowAllCategories)
|
|
{
|
|
for (const auto& AvailableCategory : Filter.GetAvailableLogCategories())
|
|
{
|
|
Filter.ToggleLogCategory(AvailableCategory);
|
|
}
|
|
}
|
|
|
|
// Flag the messages count as dirty
|
|
MessagesTextMarshaller->MarkMessagesCacheAsDirty();
|
|
|
|
Refresh();
|
|
}
|
|
|
|
void SOutputLog::CategoriesSingle_Execute(FName InName)
|
|
{
|
|
Filter.ToggleLogCategory(InName);
|
|
|
|
// 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 by Category
|
|
{
|
|
if (!IsLogCategoryEnabled(Message->Category))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Filter search phrase
|
|
{
|
|
if (!TextFilterExpressionEvaluator.TestTextFilter(FLogFilter_TextFilterExpressionContext(*Message)))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FLogFilter::AddAvailableLogCategory(FName& LogCategory)
|
|
{
|
|
// Use an insert-sort to keep AvailableLogCategories alphabetically sorted
|
|
int32 InsertIndex = 0;
|
|
for (InsertIndex = AvailableLogCategories.Num()-1; InsertIndex >= 0; --InsertIndex)
|
|
{
|
|
FName CheckCategory = AvailableLogCategories[InsertIndex];
|
|
// No duplicates
|
|
if (CheckCategory == LogCategory)
|
|
{
|
|
return;
|
|
}
|
|
else if (CheckCategory.Compare(LogCategory) < 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
AvailableLogCategories.Insert(LogCategory, InsertIndex+1);
|
|
if (bShowAllCategories)
|
|
{
|
|
ToggleLogCategory(LogCategory);
|
|
}
|
|
}
|
|
|
|
void FLogFilter::ToggleLogCategory(const FName& LogCategory)
|
|
{
|
|
int32 FoundIndex = SelectedLogCategories.Find(LogCategory);
|
|
if (FoundIndex == INDEX_NONE)
|
|
{
|
|
SelectedLogCategories.Add(LogCategory);
|
|
}
|
|
else
|
|
{
|
|
SelectedLogCategories.RemoveAt(FoundIndex, /*Count=*/1, /*bAllowShrinking=*/false);
|
|
}
|
|
}
|
|
|
|
bool FLogFilter::IsLogCategoryEnabled(const FName& LogCategory) const
|
|
{
|
|
return SelectedLogCategories.Contains(LogCategory);
|
|
}
|
|
|
|
void FLogFilter::ClearSelectedLogCategories()
|
|
{
|
|
// No need to churn memory each time the selected categories are cleared
|
|
SelectedLogCategories.Reset(SelectedLogCategories.GetAllocatedSize());
|
|
}
|
|
#undef LOCTEXT_NAMESPACE
|