// Copyright Epic Games, Inc. All Rights Reserved. #include "SOutputLog.h" #include "ConsoleSettings.h" #include "Framework/Commands/UICommandInfo.h" #include "Framework/Text/IRun.h" #include "Framework/Text/TextLayout.h" #include "Misc/ConfigCacheIni.h" #include "Misc/OutputDeviceHelper.h" #include "Misc/ScopeLock.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 "Internationalization/BreakIterator.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 "Widgets/Input/SSearchBox.h" #include "Widgets/Input/SCheckBox.h" #include "Widgets/Images/SImage.h" #include "Widgets/Input/SNumericEntryBox.h" #include "Widgets/Layout/SSpacer.h" #include "Features/IModularFeatures.h" #include "Misc/CoreDelegates.h" #include "HAL/PlatformOutputDevices.h" #include "HAL/FileManager.h" #include "Widgets/Input/SButton.h" #include "Framework/Docking/TabManager.h" #include "Widgets/Docking/SDockTab.h" #include "OutputLogModule.h" #include "Widgets/Text/SlateEditableTextTypes.h" #include "OutputLogSettings.h" #include "OutputLogStyle.h" #include "OutputLogMenuContext.h" #include "ToolMenus.h" #include "Widgets/Input/SSegmentedControl.h" #define LOCTEXT_NAMESPACE "SOutputLog" class FCategoryLineHighlighter : public ISlateLineHighlighter { public: static TSharedRef Create() { return MakeShareable(new FCategoryLineHighlighter()); } virtual int32 OnPaint(const FPaintArgs& Args, const FTextLayout::FLineView& Line, const FVector2D Offset, const float Width, const FTextBlockStyle& DefaultStyle, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override { const FVector2D Location(Line.Offset.X + Offset.X, Line.Offset.Y + Offset.Y); // If we've not been set to an explicit color, calculate a suitable one from the linked color FLinearColor SelectionBackgroundColorAndOpacity = DefaultStyle.SelectedBackgroundColor.GetColor(InWidgetStyle);// *InWidgetStyle.GetColorAndOpacityTint(); SelectionBackgroundColorAndOpacity.A *= 0.2f; // The block size and offset values are pre-scaled, so we need to account for that when converting the block offsets into paint geometry const float InverseScale = Inverse(AllottedGeometry.Scale); if (Width > 0.0f) { // Draw the actual highlight rectangle FSlateDrawElement::MakeBox( OutDrawElements, ++LayerId, AllottedGeometry.ToPaintGeometry(TransformVector(InverseScale, FVector2D(Width, FMath::Max(Line.Size.Y, Line.TextHeight))), FSlateLayoutTransform(TransformPoint(InverseScale, Location))), &DefaultStyle.HighlightShape, bParentEnabled /*&& bHasKeyboardFocus*/ ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect, SelectionBackgroundColorAndOpacity ); } return LayerId; } protected: FCategoryLineHighlighter() { } }; class FCategoryBadgeHighlighter : public ISlateLineHighlighter { public: static TSharedRef Create(const FLinearColor& InBadgeColor) { return MakeShareable(new FCategoryBadgeHighlighter(InBadgeColor)); } virtual int32 OnPaint(const FPaintArgs& Args, const FTextLayout::FLineView& Line, const FVector2D Offset, const float Width, const FTextBlockStyle& DefaultStyle, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override { const FVector2D Location(Line.Offset.X + Offset.X, Line.Offset.Y + Offset.Y); // The block size and offset values are pre-scaled, so we need to account for that when converting the block offsets into paint geometry const float InverseScale = Inverse(AllottedGeometry.Scale); if (Width > 0.0f) { // Draw the actual highlight rectangle FSlateDrawElement::MakeBox( OutDrawElements, ++LayerId, AllottedGeometry.ToPaintGeometry(TransformVector(InverseScale, FVector2D(Width, FMath::Max(Line.Size.Y, Line.TextHeight))), FSlateLayoutTransform(TransformPoint(InverseScale, Location))), &DefaultStyle.HighlightShape, bParentEnabled ? ESlateDrawEffect::None : ESlateDrawEffect::DisabledEffect, BadgeColor ); } return LayerId; } protected: FLinearColor BadgeColor; FCategoryBadgeHighlighter(const FLinearColor& InBadgeColor) : BadgeColor(InBadgeColor) { } }; /** Expression context to test the given messages against the current text filter */ class FLogFilter_TextFilterExpressionContextOutputLog : public ITextFilterExpressionContext { public: explicit FLogFilter_TextFilterExpressionContextOutputLog(const FOutputLogMessage& 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 FOutputLogMessage* 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; 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); EPopupMethod PopupMethod = GIsEditor ? EPopupMethod::CreateNewWindow : EPopupMethod::UseCurrentWindow; ChildSlot [ SAssignNew( SuggestionBox, SMenuAnchor ) .Method(PopupMethod) .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(FOutputLogStyle::Get(), "SimpleComboButton") .ContentPadding(0.f) .OnGetMenuContent(this, &SConsoleInputBox::GetCommandExecutorMenuContent) .ButtonContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(2.0f) .HAlign(HAlign_Left) .VAlign(VAlign_Center) .AutoWidth() [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FOutputLogStyle::Get().GetBrush("DebugConsole.Icon")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f) [ SNew(STextBlock) .Text(this, &SConsoleInputBox::GetActiveCommandExecutorDisplayName) ] ] ] +SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(SBox) .MinDesiredWidth(300.f) .MaxDesiredWidth(600.f) [ SAssignNew(InputText, SMultiLineEditableTextBox) .Font(FOutputLogStyle::Get().GetWidgetStyle("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) .ToolTipText(this, &SConsoleInputBox::GetInputHelpText) ] ] ] .MenuContent ( SNew(SBorder) .BorderImage(FOutputLogStyle::Get().GetBrush("Menu.Background")) .Padding( FMargin(2) ) [ SNew(SBox) .HeightOverride(250.f) // avoids flickering, ideally this would be adaptive to the content without flickering .MinDesiredWidth(300.f) .MaxDesiredWidth(this, &SConsoleInputBox::GetSelectionListMaxWidth) [ SAssignNew(SuggestionListView, SListView< TSharedPtr >) .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) ] ] ) ]; // Don't let tooltips appear on top of the text box since it hampers visibility while typing the command : InputText->EnableToolTipForceField(true); SuggestionListView->EnableToolTipForceField(true); } 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 NewValue, ESelectInfo::Type SelectInfo) { if(bIgnoreUIUpdate) { return; } Suggestions.SelectedSuggestion = Suggestions.SuggestionsList.IndexOfByPredicate([&NewValue](const TSharedPtr& 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 ); // Jump the caret to the end of the newly auto-completed line. This makes it so that selecting // an option doesn't leave the cursor in the middle of the suggestion (which makes it hard to // ctrl-back out, or to type "?" for help, etc.) InputText->GoTo(ETextLocation::EndOfDocument); } // 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 SConsoleInputBox::MakeSuggestionListItemWidget(TSharedPtr Suggestion, const TSharedRef& OwnerTable) { check(Suggestion.IsValid()); FString SanitizedText = Suggestion->Name; 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 >, OwnerTable) [ SNew(STextBlock) .Text(FText::FromString(SanitizedText)) .TextStyle(FOutputLogStyle::Get(), "Log.Normal") .HighlightText(Suggestions.SuggestionsHighlight) .ColorAndOpacity(FSlateColor::UseForeground()) .ToolTipText(FText::FromString(Suggestion->Help)) ]; } void SConsoleInputBox::OnTextChanged(const FText& InText) { if(bIgnoreUIUpdate) { return; } const FString& InputTextStr = InputText->GetText().ToString(); if(!InputTextStr.IsEmpty()) { TArray AutoCompleteList; if (ActiveCommandExecutor) { ActiveCommandExecutor->GetSuggestedCompletions(*InputTextStr, AutoCompleteList); } else { auto OnConsoleVariable = [&AutoCompleteList](const TCHAR *Name, IConsoleObject* CVar) { if (CVar->IsEnabled()) { AutoCompleteList.Add(FConsoleSuggestion(Name, CVar->GetDetailedHelp().ToString())); } }; IConsoleManager::Get().ForEachConsoleObjectThatContains(FConsoleObjectVisitor::CreateLambda(OnConsoleVariable), *InputTextStr); //AutoCompleteList.Append(GetDefault()->GetFilteredManualAutoCompleteCommands(InputTextStr)); } AutoCompleteList.Sort([InputTextStr](const FConsoleSuggestion& A, const FConsoleSuggestion& B) { if (A.Name.StartsWith(InputTextStr)) { if (!B.Name.StartsWith(InputTextStr)) { return true; } } else { if (B.Name.StartsWith(InputTextStr)) { return false; } } return A.Name < B.Name; }); 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()) { Suggestions.StepSelectedSuggestion(KeyEvent.IsShiftDown() ? -1 : +1); } else { Suggestions.SelectedSuggestion = 0; } MarkActiveSuggestion(); } bConsumeTab = true; return FReply::Handled(); } else if (KeyEvent.GetKey() == EKeys::Escape) { SuggestionBox->SetIsOpen(false); return FReply::Handled(); } } else { const FInputChord KeyEventAsInputChord = FInputChord(KeyEvent.GetKey(), EModifierKey::FromBools(KeyEvent.IsControlDown(), KeyEvent.IsAltDown(), KeyEvent.IsShiftDown(), KeyEvent.IsCommandDown())); 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) { IConsoleManager& ConsoleManager = IConsoleManager::Get(); TArray HistoryNames; if (ActiveCommandExecutor) { ActiveCommandExecutor->GetExecHistory(HistoryNames); } else { ConsoleManager.GetConsoleHistory(TEXT(""), HistoryNames); } TArray History; for (const FString& Name : HistoryNames) { FString HelpString; // Try to find a console object for this history entry in order to retrieve a help string if possible : const TCHAR* NamePtr = *Name; if (IConsoleObject* CObj = ConsoleManager.FindConsoleObject(*FParse::Token(NamePtr, /*UseEscape = */false), /*bTrackFrequentCalls = */false); CObj && CObj->IsEnabled()) { HelpString = CObj->GetDetailedHelp().ToString(); } History.Add(FConsoleSuggestion(Name, HelpString)); } 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(); } else if (ActiveCommandExecutor && ActiveCommandExecutor->GetIterateExecutorHotKey() == KeyEventAsInputChord) { MakeNextCommandExecutorActive(); return FReply::Handled(); } } return FReply::Unhandled(); } void SConsoleInputBox::SetSuggestions(TArray& Elements, FText Highlight) { TOptional SelectionText; if (Suggestions.HasSelectedSuggestion()) { SelectionText = Suggestions.GetSelectedSuggestion()->Name; } Suggestions.Reset(); Suggestions.SuggestionsHighlight = Highlight; for(int32 i = 0; i < Elements.Num(); ++i) { Suggestions.SuggestionsList.Add(MakeShared(Elements[i])); if (SelectionText.IsSet() && Elements[i].Name == SelectionText.GetValue()) { 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 { SuggestionListView->ScrollToTop(); } } else { SuggestionBox->SetIsOpen(false); } } void SConsoleInputBox::OnFocusLost( const FFocusEvent& InFocusEvent ) { // SuggestionBox->SetIsOpen(false); } void SConsoleInputBox::MarkActiveSuggestion() { bIgnoreUIUpdate = true; if (Suggestions.HasSelectedSuggestion()) { TSharedPtr SelectedSuggestion = Suggestions.GetSelectedSuggestion(); SuggestionListView->SetSelection(SelectedSuggestion); SuggestionListView->RequestScrollIntoView(SelectedSuggestion); // Ideally this would only scroll if outside of the view InputText->SetText(FText::FromString(SelectedSuggestion->Name)); } 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 CommandExecutors = IModularFeatures::Get().GetModularFeatureImplementations(IConsoleCommandExecutor::ModularFeatureName()); ActiveCommandExecutor = nullptr; if (CommandExecutors.IsValidIndex(0)) { ActiveCommandExecutor = CommandExecutors[0]; } // to swap to a preferred executor, try and match from the active name for (IConsoleCommandExecutor* CommandExecutor : CommandExecutors) { if (CommandExecutor->GetName() == PreferredCommandExecutorName) { 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; } FText SConsoleInputBox::GetInputHelpText() const { const FString& InputTextStr = InputText->GetText().ToString(); if (!InputTextStr.IsEmpty()) { // Try to find a console object for this entry in order to retrieve a help string if possible : IConsoleManager& ConsoleManager = IConsoleManager::Get(); const TCHAR* InputTextStrPtr = *InputTextStr; if (IConsoleObject* CObj = ConsoleManager.FindConsoleObject(*FParse::Token(InputTextStrPtr, /*UseEscape = */false), /*bTrackFrequentCalls = */false); CObj && CObj->IsEnabled()) { return CObj->GetDetailedHelp(); } } return FText::GetEmpty(); } bool SConsoleInputBox::IsCommandExecutorMenuEnabled() const { return !ConsoleCommandCustomExec.IsBound(); // custom execs always show the default executor in the UI (which has the selector disabled) } void SConsoleInputBox::MakeNextCommandExecutorActive() { // Sorted so the iteration order matches the displayed order. TArray CommandExecutors = IModularFeatures::Get().GetModularFeatureImplementations(IConsoleCommandExecutor::ModularFeatureName()); CommandExecutors.Sort([](IConsoleCommandExecutor& LHS, IConsoleCommandExecutor& RHS) { return LHS.GetDisplayName().CompareTo(RHS.GetDisplayName()) < 0; }); int32 CurrentIndex = CommandExecutors.IndexOfByKey(ActiveCommandExecutor); if (CurrentIndex >= 0) { CurrentIndex++; if (CurrentIndex >= CommandExecutors.Num()) { CurrentIndex = 0; } SetActiveCommandExecutor(CommandExecutors[CurrentIndex]->GetName()); } } TSharedRef SConsoleInputBox::GetCommandExecutorMenuContent() { static const FName MenuName = "OutputLog.ConsoleInputBox.CmdExecMenu"; if (!UToolMenus::Get()->IsMenuRegistered(MenuName)) { UToolMenu* Menu = UToolMenus::Get()->RegisterMenu(MenuName); Menu->bShouldCloseWindowAfterMenuSelection = true; Menu->AddDynamicSection("DynamicCmdExecEntries", FNewToolMenuDelegate::CreateLambda([](UToolMenu* InMenu) { if (UConsoleInputBoxMenuContext* Context = InMenu->FindContext()) { if (TSharedPtr This = Context->GetConsoleInputBox()) { TArray CommandExecutors = IModularFeatures::Get().GetModularFeatureImplementations(IConsoleCommandExecutor::ModularFeatureName()); CommandExecutors.Sort([](IConsoleCommandExecutor& LHS, IConsoleCommandExecutor& RHS) { return LHS.GetDisplayName().CompareTo(RHS.GetDisplayName()) < 0; }); FToolMenuSection& Section = InMenu->AddSection("CmdExecEntries"); for (const IConsoleCommandExecutor* CommandExecutor : CommandExecutors) { const bool bIsActiveCmdExec = This->ActiveCommandExecutor == CommandExecutor; Section.AddMenuEntry( CommandExecutor->GetName(), CommandExecutor->GetDisplayName(), CommandExecutor->GetDescription(), FSlateIcon(), FUIAction( FExecuteAction::CreateSP(This.Get(), &SConsoleInputBox::SetActiveCommandExecutor, CommandExecutor->GetName()), FCanExecuteAction::CreateLambda([] { return true; }), FIsActionChecked::CreateLambda([bIsActiveCmdExec] { return bIsActiveCmdExec; }) ), EUserInterfaceActionType::Check ); } } } })); } UConsoleInputBoxMenuContext* MenuContext = NewObject(); MenuContext->Init(SharedThis(this)); FToolMenuContext ToolMenuContext(MenuContext); return UToolMenus::Get()->GenerateWidget(MenuName, ToolMenuContext); } FReply SConsoleInputBox::OnKeyDownHandler(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) { 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() && ActiveCommandExecutor->GetHotKey() == InputChord)) { SuggestionBox->SetIsOpen(false); 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(); } // Intercept tab if used for auto-complete if (InCharacterEvent.GetCharacter() == '\t' && bConsumeTab) { bConsumeTab = false; return FReply::Handled(); } if (InCharacterEvent.GetModifierKeys().AnyModifiersDown() && InCharacterEvent.GetCharacter() == ' ') { // Ignore space bar + a modifier key. It should not type a space as this is used by other keyboard shortcuts return FReply::Handled(); } FInputChord OpenConsoleChord; if (ActiveCommandExecutor && ActiveCommandExecutor->AllowHotKeyClose()) { OpenConsoleChord = ActiveCommandExecutor->GetHotKey(); 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 (InCharacterEvent.GetCharacter() == (TCHAR)*CharCode && OpenConsoleChord.NeedsControl() == InCharacterEvent.IsControlDown() && OpenConsoleChord.NeedsAlt() == InCharacterEvent.IsAltDown() && OpenConsoleChord.NeedsShift() == InCharacterEvent.IsShiftDown() && OpenConsoleChord.NeedsCommand() == InCharacterEvent.IsCommandDown()) { return FReply::Handled(); } else { return FReply::Unhandled(); } } else { return FReply::Unhandled(); } } TSharedRef< FOutputLogTextLayoutMarshaller > FOutputLogTextLayoutMarshaller::Create(TArray< TSharedPtr > InMessages, FOutputLogFilter* InFilter) { return MakeShareable(new FOutputLogTextLayoutMarshaller(MoveTemp(InMessages), InFilter)); } FOutputLogTextLayoutMarshaller::~FOutputLogTextLayoutMarshaller() { } void FOutputLogTextLayoutMarshaller::SetText(const FString& SourceString, FTextLayout& TargetTextLayout) { TextLayout = &TargetTextLayout; NextPendingMessageIndex = 0; SubmitPendingMessages(); } void FOutputLogTextLayoutMarshaller::GetText(FString& TargetString, const FTextLayout& SourceTextLayout) { SourceTextLayout.GetAsText(TargetString); } bool FOutputLogTextLayoutMarshaller::AppendPendingMessage(const TCHAR* InText, const ELogVerbosity::Type InVerbosity, const FName& InCategory) { // We don't want to skip adding messages, so just try to acquire the lock FScopeLock PendingMessagesAccess(&PendingMessagesCriticalSection); return SOutputLog::CreateLogMessages(InText, InVerbosity, InCategory, PendingMessages); } bool FOutputLogTextLayoutMarshaller::SubmitPendingMessages() { // We can always submit messages next tick. So only try to lock, if not possible return. if (PendingMessagesCriticalSection.TryLock()) { Messages.Append(MoveTemp(PendingMessages)); PendingMessages.Reset(); PendingMessagesCriticalSection.Unlock(); } else { return false; } if (Messages.IsValidIndex(NextPendingMessageIndex)) { const int32 CurrentMessagesCount = Messages.Num(); AppendPendingMessagesToTextLayout(); NextPendingMessageIndex = CurrentMessagesCount; return true; } return false; } float FOutputLogTextLayoutMarshaller::GetCategoryHue(FName CategoryName) { if (float* pResult = CategoryHueMap.Find(CategoryName)) { return *pResult; } else { FRandomStream RNG(GetTypeHash(CategoryName)); const float Hue = (float)RNG.FRandRange(0.0, 360.0); CategoryHueMap.Add(CategoryName, Hue); return Hue; } } void FOutputLogTextLayoutMarshaller::AppendPendingMessagesToTextLayout() { const int32 CurrentMessagesCount = Messages.Num(); const int32 NumPendingMessages = CurrentMessagesCount - NextPendingMessageIndex; if (NumPendingMessages == 0) { return; } 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 const bool bWasEmpty = GetNumMessages() == 0; if (bWasEmpty) { TextLayout->ClearLines(); } } else { MarkMessagesCacheAsDirty(); MakeDirty(); } const ELogCategoryColorizationMode CategoryColorizationMode = GetDefault()->CategoryColorizationMode; TArray LinesToAdd; LinesToAdd.Reserve(NumPendingMessages); TArray Highlights; int32 NumAddedMessages = 0; auto ComputeCategoryColor = [this](const FTextBlockStyle& OriginalStyle, const FName MessageCategory) { FTextBlockStyle Result = OriginalStyle; FLinearColor HSV = OriginalStyle.ColorAndOpacity.GetSpecifiedColor().LinearRGBToHSV(); HSV.R = GetCategoryHue(MessageCategory); HSV.G = FMath::Max(0.4f, HSV.G); Result.ColorAndOpacity = HSV.HSVToLinearRGB(); return Result; }; for (int32 MessageIndex = NextPendingMessageIndex; MessageIndex < CurrentMessagesCount; ++MessageIndex) { const TSharedPtr Message = Messages[MessageIndex]; const int32 LineIndex = TextLayout->GetLineModels().Num() + NumAddedMessages; if (!Message) { continue; } Filter->AddAvailableLogCategory(Message->Category); if (!Filter->IsMessageAllowed(Message)) { continue; } ++NumAddedMessages; const FTextBlockStyle& MessageTextStyle = FOutputLogStyle::Get().GetWidgetStyle(Message->Style); TSharedRef LineText = Message->Message; TArray> Runs; switch (CategoryColorizationMode) { case ELogCategoryColorizationMode::None: Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle)); break; case ELogCategoryColorizationMode::ColorizeWholeLine: { const bool bUseCategoryColor = (Message->Verbosity > ELogVerbosity::Warning); Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, bUseCategoryColor ? ComputeCategoryColor(MessageTextStyle, Message->Category) : MessageTextStyle)); } break; case ELogCategoryColorizationMode::ColorizeCategoryOnly: { if (Message->CategoryStartIndex >= 0) { const int32 CategoryStartIndex = Message->CategoryStartIndex; const int32 CategoryStopIndex = CategoryStartIndex + (int32)Message->Category.GetStringLength() + 1; if (CategoryStartIndex > 0) { Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle, FTextRange(0, CategoryStartIndex))); } Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, ComputeCategoryColor(MessageTextStyle, Message->Category), FTextRange(CategoryStartIndex, CategoryStopIndex))); Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle, FTextRange(CategoryStopIndex, LineText->Len()))); } else { Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle)); } } break; case ELogCategoryColorizationMode::ColorizeCategoryAsBadge: { if (Message->CategoryStartIndex >= 0) { const int32 CategoryStartIndex = Message->CategoryStartIndex; const int32 CategoryStopIndex = CategoryStartIndex + (int32)Message->Category.GetStringLength(); FTextBlockStyle BadgeStyle = ComputeCategoryColor(MessageTextStyle, Message->Category); Highlights.Emplace(LineIndex, FTextRange(CategoryStartIndex, CategoryStopIndex), /*Zorder=*/ -20, FCategoryBadgeHighlighter::Create(BadgeStyle.ColorAndOpacity.GetSpecifiedColor())); BadgeStyle.ColorAndOpacity = FLinearColor::Black; if (CategoryStartIndex > 0) { Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle, FTextRange(0, CategoryStartIndex))); } Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, BadgeStyle, FTextRange(CategoryStartIndex, CategoryStopIndex))); Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle, FTextRange(CategoryStopIndex, LineText->Len()))); } else { Runs.Add(FSlateTextRun::Create(FRunInfo(), LineText, MessageTextStyle)); } } break; } if (!Message->Category.IsNone() && (Message->Category == CategoryToHighlight)) { Highlights.Emplace(LineIndex, FTextRange(0, LineText->Len()), /*Zorder=*/ -5, FCategoryLineHighlighter::Create()); } 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); for (const FTextLineHighlight& Highlight : Highlights) { TextLayout->AddLineHighlight(Highlight); } } void FOutputLogTextLayoutMarshaller::ClearMessages() { NextPendingMessageIndex = 0; Messages.Empty(); bNumMessagesCacheDirty = true; MakeDirty(); } void FOutputLogTextLayoutMarshaller::CountMessages() { // Do not re-count if not dirty if (!bNumMessagesCacheDirty) { return; } CachedNumMessages = 0; for (int32 MessageIndex = 0; MessageIndex < NextPendingMessageIndex; ++MessageIndex) { const TSharedPtr CurrentMessage = Messages[MessageIndex]; if (Filter->IsMessageAllowed(CurrentMessage)) { CachedNumMessages++; } } // Cache re-built, remove dirty flag bNumMessagesCacheDirty = false; } int32 FOutputLogTextLayoutMarshaller::GetNumMessages() const { const int32 NumPendingMessages = Messages.Num() - NextPendingMessageIndex; return Messages.Num() - NumPendingMessages; } int32 FOutputLogTextLayoutMarshaller::GetNumFilteredMessages() { // Re-count messages if filter changed before we refresh if (bNumMessagesCacheDirty) { CountMessages(); } return CachedNumMessages; } int32 FOutputLogTextLayoutMarshaller::GetNumCachedMessages() { // Re-count messages if filter changed before we refresh if (bNumMessagesCacheDirty) { CountMessages(); } return CachedNumMessages; } void FOutputLogTextLayoutMarshaller::MarkMessagesCacheAsDirty() { bNumMessagesCacheDirty = true; } FName FOutputLogTextLayoutMarshaller::GetCategoryForLocation(const FTextLocation Location) const { if (TextLayout) { TSharedRef WordBreakIterator{ FBreakIterator::CreateWordBreakIterator() }; int32 LineIndex = Location.GetLineIndex(); // A Message may be split across multiple lines in the TextLayout, so work backwards to find the Category on the first line of the message. while (TextLayout->GetLineModels().IsValidIndex(LineIndex)) { const FTextLayout::FLineModel& LineModel = TextLayout->GetLineModels()[LineIndex]; WordBreakIterator->SetStringRef(&LineModel.Text.Get()); int32 PreviousBreak = WordBreakIterator->ResetToBeginning(); int32 CurrentBreak = 0; // Iterate words starting from the beginning of the line, as the Category is one of the first words in a message. while ((CurrentBreak = WordBreakIterator->MoveToNext()) != INDEX_NONE) { FTextSelection Selection{ FTextLocation(LineIndex, CurrentBreak), FTextLocation(LineIndex, PreviousBreak) }; FString SelectedText; TextLayout->GetSelectionAsText(SelectedText, Selection); FName PossibleCategory(SelectedText, FNAME_Find); if (!PossibleCategory.IsNone() && Filter->IsLogCategoryAvailable(PossibleCategory)) { return PossibleCategory; } PreviousBreak = CurrentBreak; } WordBreakIterator->ClearString(); LineIndex--; } } return NAME_None; } FTextLocation FOutputLogTextLayoutMarshaller::GetTextLocationAt(const FVector2D& Relative) const { return TextLayout ? TextLayout->GetTextLocationAt(Relative) : FTextLocation(INDEX_NONE, INDEX_NONE); } FOutputLogTextLayoutMarshaller::FOutputLogTextLayoutMarshaller(TArray< TSharedPtr > InMessages, FOutputLogFilter* InFilter) : Messages(MoveTemp(InMessages)) , NextPendingMessageIndex(0) , CachedNumMessages(0) , Filter(InFilter) , TextLayout(nullptr) { } ////////////////////////////////////////////////////////////////////////// namespace { const FName SettingsMenuName("OutputLog.SettingsMenu"); const FName SettingsWordWrapEntryName("WordWrapEnable"); const FName SettingsTimestampsSubMenuName("TimestampsSubMenu"); const FName SettingsClearOnPIEEntryName("ClearOnPIE"); const FName SettingsSeparatorName("Separator"); const FName SettingsBrowseLogDirectoryEntryName("BrowseLogDirectory"); const FName SettingsOpenLogExternalEntryName("OpenLogExternal"); } BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION void SOutputLog::Construct( const FArguments& InArgs, bool bCreateDrawerDockButton) { bShouldCreateDrawerDockButton = bCreateDrawerDockButton; BuildInitialLogCategoryFilter(InArgs); bShouldShowLoggingLimitMenu = InArgs._EnableLoggingLimitMenu; bEnableLoggingLimit = InArgs._LoggingLineLimit.IsSet(); LoggingLineLimit = InArgs._LoggingLineLimit.Get(10000); MessagesTextMarshaller = FOutputLogTextLayoutMarshaller::Create(InArgs._Messages, &Filter); MessagesTextBox = SNew(SMultiLineEditableTextBox) .Style(FOutputLogStyle::Get(), "Log.TextBox") .Marshaller(MessagesTextMarshaller) .IsReadOnly(true) .AlwaysShowScrollbars(true) .AutoWrapText(this, &SOutputLog::IsWordWrapEnabled) .OnVScrollBarUserScrolled(this, &SOutputLog::OnUserScrolled) .ContextMenuExtender(this, &SOutputLog::ExtendTextBoxMenu); // We take the settings bit flags passed in, and register a corresponding runtime tool menu profile. const FName SettingsMenuProfileName = GetSettingsMenuProfileForFlags(InArgs._SettingsMenuFlags); ChildSlot .Padding(3) [ SNew(SVerticalBox) // Output Log Filter +SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 4.0f, 0.0f, 4.0f)) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .Padding(0, 0, 4, 0) .FillWidth(.65f) [ SAssignNew(FilterTextBox, SSearchBox) .HintText(LOCTEXT("SearchLogHint", "Search Log")) .OnTextChanged(this, &SOutputLog::OnFilterTextChanged) .OnTextCommitted(this, &SOutputLog::OnFilterTextCommitted) .DelayChangeNotificationsWhileTyping(true) ] +SHorizontalBox::Slot() .AutoWidth() .HAlign(HAlign_Left) [ SNew(SComboButton) .ComboButtonStyle(FOutputLogStyle::Get(), "SimpleComboButton") .ToolTipText(LOCTEXT("AddFilterToolTip", "Add an output log filter.")) .OnGetMenuContent(this, &SOutputLog::MakeAddFilterMenu) .ButtonContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FOutputLogStyle::Get().GetBrush("Icons.Filter")) .ColorAndOpacity(FSlateColor::UseForeground()) ] +SHorizontalBox::Slot() .AutoWidth() .Padding(2, 0, 0, 0) [ SNew(STextBlock) .Text(LOCTEXT("Filters", "Filters")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] ] +SHorizontalBox::Slot() .HAlign(HAlign_Left) .VAlign(VAlign_Center) .Padding(4, 0) [ SNew(STextBlock) .Text(LOCTEXT("LogLineLimitReached", "Log line limit reached. Clear log to continue.")) .ColorAndOpacity(FSlateColor(FLinearColor::Yellow)) .Visibility(MakeAttributeLambda([this]() { if (!bEnableLoggingLimit || MessagesTextMarshaller->GetNumCachedMessages() < LoggingLineLimit) { return EVisibility::Hidden; } return EVisibility::Visible; })) ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) .VAlign(VAlign_Center) .Padding(4, 0) [ CreateDrawerDockButton() ] + SHorizontalBox::Slot() .HAlign(HAlign_Right) .AutoWidth() [ SNew(SComboButton) .ComboButtonStyle(FOutputLogStyle::Get(), "SimpleComboButton") .OnGetMenuContent(this, &SOutputLog::GetSettingsMenuContent, SettingsMenuProfileName) .ButtonContent() [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ SNew(SImage) .Image(FOutputLogStyle::Get().GetBrush("Icons.Settings")) .ColorAndOpacity(FSlateColor::UseForeground()) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(2, 0, 0, 0) .VAlign(VAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("SettingsButton", "Settings")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ] ] ] // Output log area +SVerticalBox::Slot() .FillHeight(1) [ MessagesTextBox.ToSharedRef() ] // The console input box +SVerticalBox::Slot() .AutoHeight() [ SAssignNew(ConsoleInputBox, SConsoleInputBox) .Visibility(MakeAttributeLambda([]() { return FOutputLogModule::Get().ShouldHideConsole() ? EVisibility::Collapsed : EVisibility::Visible; })) .OnConsoleCommandExecuted(this, &SOutputLog::OnConsoleCommandExecuted) .OnCloseConsole(InArgs._OnCloseConsole) // Always place suggestions above the input line for the output log widget .SuggestionListPlacement(MenuPlacement_AboveAnchor) ] ]; GLog->AddOutputDevice(this); #if WITH_EDITOR // Listen for style changes UOutputLogSettings* Settings = GetMutableDefault(); SettingsWatchHandle = Settings->OnSettingChanged().AddRaw(this, &SOutputLog::HandleSettingChanged); #endif bIsUserScrolled = false; RequestForceScroll(); OnClearLogDelegate = InArgs._OnClearLog; } END_SLATE_FUNCTION_BUILD_OPTIMIZATION SOutputLog::~SOutputLog() { if (GLog != nullptr) { GLog->RemoveOutputDevice(this); } #if WITH_EDITOR if (UObjectInitialized() && !GExitPurge) { UOutputLogSettings* Settings = GetMutableDefault(); Settings->OnSettingChanged().Remove(SettingsWatchHandle); } #endif } void SOutputLog::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) { if (MessagesTextMarshaller->SubmitPendingMessages()) { // Don't scroll to the bottom automatically when the user is scrolling the view or has scrolled it away from the bottom. RequestForceScroll(true); } SCompoundWidget::Tick(AllottedGeometry, InCurrentTime, InDeltaTime); } static const FName NAME_StyleLogCommand(TEXT("Log.Command")); static const FName NAME_StyleLogError(TEXT("Log.Error")); static const FName NAME_StyleLogWarning(TEXT("Log.Warning")); static const FName NAME_StyleLogNormal(TEXT("Log.Normal")); bool SOutputLog::CreateLogMessages( const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category, TArray< TSharedPtr >& OutMessages ) { if (Verbosity == ELogVerbosity::SetColor) { // Skip Color Events return false; } else { // Get the style for this message. When piping output from child processes (e.g., 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 = NAME_StyleLogCommand; } else if (Verbosity == ELogVerbosity::Error || FCString::Stristr(V, TEXT("Error:")) != nullptr) { Style = NAME_StyleLogError; } else if (Verbosity == ELogVerbosity::Warning || FCString::Stristr(V, TEXT("Warning:")) != nullptr) { Style = NAME_StyleLogWarning; } else { Style = NAME_StyleLogNormal; } // 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()->LogTimestampMode; } const int32 OldNumMessages = OutMessages.Num(); // handle multiline strings by breaking them apart by line TArray 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 = 600; for (int32 CurrentStartIndex = 0; CurrentStartIndex < Line.Len();) { int32 HardWrapLineLen = 0; if (bIsFirstLineInMessage) { int32 CategoryStartIndex; const FString MessagePrefix = FOutputDeviceHelper::FormatLogLine(Verbosity, Category, nullptr, LogTimestampMode, -1.0, /*out*/ &CategoryStartIndex); HardWrapLineLen = FMath::Min(HardWrapLen - MessagePrefix.Len(), Line.Len() - CurrentStartIndex); const FString HardWrapLine = Line.Mid(CurrentStartIndex, HardWrapLineLen); OutMessages.Add(MakeShared(MakeShared(MessagePrefix + HardWrapLine), Verbosity, Category, Style, CategoryStartIndex)); } else { HardWrapLineLen = FMath::Min(HardWrapLen, Line.Len() - CurrentStartIndex); FString HardWrapLine = Line.Mid(CurrentStartIndex, HardWrapLineLen); OutMessages.Add(MakeShared(MakeShared(MoveTemp(HardWrapLine)), Verbosity, Category, Style, INDEX_NONE)); } bIsFirstLineInMessage = false; CurrentStartIndex += HardWrapLineLen; } } } return OldNumMessages != OutMessages.Num(); } } void SOutputLog::Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category) { if (!bEnableLoggingLimit || MessagesTextMarshaller->GetNumCachedMessages() < LoggingLineLimit) { MessagesTextMarshaller->AppendPendingMessage(V, Verbosity, Category); } } TSharedRef SOutputLog::MakeLogLimitMenuItem() { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ SNew(STextBlock) .Text(LOCTEXT("LimitLog", "Logging Limit")) ] +SHorizontalBox::Slot() .FillWidth(1.f) [ SNew(SSpacer) ] +SHorizontalBox::Slot() .HAlign(HAlign_Right) [ SNew(SNumericEntryBox) .AllowSpin(true) .Justification(ETextJustify::Right) .MinDesiredValueWidth(100) .MaxSliderValue(100000) .OnValueChanged_Lambda([this](int32 NewValue){ if (NewValue > 100) { LoggingLineLimit = NewValue; } }) .Value_Lambda([this](){ return LoggingLineLimit; }) ]; } 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 ); Builder.AddMenuEntry( FUIAction( FExecuteAction::CreateLambda([this](){ bEnableLoggingLimit = !bEnableLoggingLimit; }), FCanExecuteAction::CreateLambda([] { return true; }), FIsActionChecked::CreateLambda([this] { return bEnableLoggingLimit; }), FIsActionButtonVisible::CreateLambda([this] { return bShouldShowLoggingLimitMenu; }) ), MakeLogLimitMenuItem(), NAME_None, LOCTEXT("LimitLogToolTip", "Limits Logging to specified number of lines."), EUserInterfaceActionType::ToggleButton); const FVector2D CursorPos = FSlateApplication::Get().GetCursorPos(); const FVector2D RelativeCursorPos = MessagesTextBox->GetTickSpaceGeometry().AbsoluteToLocal(CursorPos); const FTextLocation CursorTextLocation = MessagesTextMarshaller->GetTextLocationAt(RelativeCursorPos); if (CursorTextLocation.IsValid()) { const FName CategoryName = MessagesTextMarshaller->GetCategoryForLocation(CursorTextLocation); if (!CategoryName.IsNone()) { Builder.BeginSection(NAME_None, FText::Format(LOCTEXT("CategoryActionsSectionHeading", "Category {0}"), FText::FromName(CategoryName))); if (CategoryName == MessagesTextMarshaller->GetCategoryToHighlight()) { FUIAction StopHighlightingCategoryAction( FExecuteAction::CreateRaw(this, &SOutputLog::OnHighlightCategory, FName()) ); Builder.AddMenuEntry( LOCTEXT("StopHighlightCategoryAction", "Remove category highlights"), LOCTEXT("StopHighlightCategoryActionTooltip", "Stop highlighting all messages for this category"), FSlateIcon(), StopHighlightingCategoryAction ); } else { FUIAction HighlightCategoryAction( FExecuteAction::CreateRaw(this, &SOutputLog::OnHighlightCategory, CategoryName) ); Builder.AddMenuEntry( FText::Format(LOCTEXT("HighlightCategoryAction", "Highlight category {0}"), FText::FromName(CategoryName)), LOCTEXT("HighlightCategoryActionTooltip", "Highlights all messages for this category"), FSlateIcon(), HighlightCategoryAction ); } Builder.EndSection(); } } } 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; [[maybe_unused]] bool bOnClearLogDelegateExecuted = OnClearLogDelegate.ExecuteIfBound(); } void SOutputLog::OnHighlightCategory(FName NewCategoryToHighlight) { MessagesTextMarshaller->SetCategoryToHighlight(NewCategoryToHighlight); RefreshAllPreservingLocation(); } void SOutputLog::HandleSettingChanged(FName ChangedSettingName) { RefreshAllPreservingLocation(); } void SOutputLog::RefreshAllPreservingLocation() { const FTextLocation LastCursorTextLocation = MessagesTextBox->GetCursorLocation(); MessagesTextMarshaller->MarkMessagesCacheAsDirty(); MessagesTextMarshaller->MakeDirty(); MessagesTextBox->Refresh(); //@TODO: Without this, the window will scroll if the last 'normally clicked location' is not on screen // (even with the right-click set cursor pos fix, the refresh will scroll you back to the top of the screen // until you left click, or to where you last left clicked otherwise if off screen; spooky...) // Ideally we could read the current location or fix the bug where a refresh causes a scroll MessagesTextBox->GoTo(LastCursorTextLocation); } void SOutputLog::OnUserScrolled(float ScrollOffset) { bIsUserScrolled = ScrollOffset < 1.0 && !FMath::IsNearlyEqual(ScrollOffset, 1.0f); } bool SOutputLog::CanClearLog() const { return MessagesTextMarshaller->GetNumMessages() > 0; } void SOutputLog::FocusConsoleCommandBox() { FSlateApplication::Get().SetKeyboardFocus(ConsoleInputBox->GetEditableTextBox(), EFocusCause::SetDirectly); } void SOutputLog::OnConsoleCommandExecuted() { // Submit pending messages when executing a command to keep the log feeling responsive to input MessagesTextMarshaller->SubmitPendingMessages(); RequestForceScroll(); } void SOutputLog::RequestForceScroll(bool bIfUserHasNotScrolledUp) { if (MessagesTextMarshaller->GetNumFilteredMessages() > 0 && (!bIfUserHasNotScrolledUp || !bIsUserScrolled)) { MessagesTextBox->ScrollTo(ETextLocation::EndOfDocument); 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(); } bool SOutputLog::IsWordWrapEnabled() const { const UOutputLogSettings* Settings = GetDefault(); return Settings ? Settings->bEnableOutputLogWordWrap : false; } void SOutputLog::SetWordWrapEnabled(ECheckBoxState InValue) { const bool bWordWrapEnabled = (InValue == ECheckBoxState::Checked); UOutputLogSettings* Settings = GetMutableDefault(); if (Settings) { Settings->bEnableOutputLogWordWrap = bWordWrapEnabled; Settings->SaveConfig(); } RequestForceScroll(true); } ELogTimes::Type SOutputLog::GetSelectedTimestampMode() { const UOutputLogSettings* Settings = GetDefault(); return Settings->LogTimestampMode; } bool SOutputLog::IsSelectedTimestampMode(ELogTimes::Type NewType) { return GetSelectedTimestampMode() == NewType; } void SOutputLog::AddTimestampMenuSection(FMenuBuilder& Menu) { Menu.BeginSection("LoggingTimestampSection"); { const UEnum* Enum = StaticEnum(); for (int CurrentTimeStampType = 0; CurrentTimeStampType < Enum->NumEnums() - 1; CurrentTimeStampType++) { ELogTimes::Type TimeStampType = static_cast(CurrentTimeStampType); FText Tooltip; #if WITH_EDITOR Tooltip = Enum->GetToolTipTextByIndex(CurrentTimeStampType); #endif // WITH_EDITOR Menu.AddMenuEntry(Enum->GetDisplayNameTextByIndex(CurrentTimeStampType), Tooltip, FSlateIcon(), FUIAction( FExecuteAction::CreateLambda([this, TimeStampType] { SetTimestampMode(TimeStampType); }), FCanExecuteAction::CreateLambda([] { return true; }), FIsActionChecked::CreateLambda([this, TimeStampType] { return IsSelectedTimestampMode(TimeStampType); }) ), NAME_None, EUserInterfaceActionType::RadioButton); } } Menu.EndSection(); } void SOutputLog::SetTimestampMode(ELogTimes::Type InValue) { UOutputLogSettings* Settings = GetMutableDefault(); if (Settings) { Settings->LogTimestampMode = InValue; Settings->SaveConfig(); } RequestForceScroll(true); } #if WITH_EDITOR bool SOutputLog::IsClearOnPIEEnabled() const { const UOutputLogSettings* Settings = GetDefault(); return Settings ? Settings->bEnableOutputLogClearOnPIE : false; } void SOutputLog::SetClearOnPIE(ECheckBoxState InValue) { const bool bClearOnPIEEnabled = (InValue == ECheckBoxState::Checked); UOutputLogSettings* Settings = GetMutableDefault(); if (Settings) { Settings->bEnableOutputLogClearOnPIE = bClearOnPIEEnabled; Settings->SaveConfig(); } } #endif void SOutputLog::BuildInitialLogCategoryFilter(const FArguments& InArgs) { for (const auto& Message : InArgs._Messages) { const bool bIsDeselectedByDefault = InArgs._AllowInitialLogCategory.IsBound() && !InArgs._AllowInitialLogCategory.Execute(Message->Category); Filter.AddAvailableLogCategory(Message->Category, bIsDeselectedByDefault ? false : TOptional()); } for (auto DefaultCategorySelectionIt = InArgs._DefaultCategorySelection.CreateConstIterator(); DefaultCategorySelectionIt; ++DefaultCategorySelectionIt) { Filter.SetLogCategoryEnabled(DefaultCategorySelectionIt->Key, DefaultCategorySelectionIt->Value); } } 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 SOutputLog::MakeAddFilterMenu() { FMenuBuilder MenuBuilder(/*bInShouldCloseWindowAfterMenuSelection=*/true, nullptr); MenuBuilder.BeginSection("OutputLogVerbosityEntries", LOCTEXT("OutputLogVerbosityHeading", "Verbosity")); { const FText AllLabel = LOCTEXT("AllLabel", "All"); const FText EnabledLabel = LOCTEXT("EnabledLabel", "Filtered"); const FText NoneLabel = LOCTEXT("NoneLabel", "None"); MenuBuilder.AddWidget( SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .VAlign(VAlign_Center) .Padding(12, 0, 4, 0) [ SNew(STextBlock) .Text(LOCTEXT("Messages", "Messages")) .TextStyle(FAppStyle::Get(), "Menu.Label") .ColorAndOpacity(FLinearColor::White) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4, 0, 12, 2) [ SNew(SSegmentedControl) .Value(this, &SOutputLog::GetMessagesFilter) .OnValueChanged(this, &SOutputLog::OnMessagesFilterChanged) + SSegmentedControl::Slot(ELogLevelFilter::None) .Text(NoneLabel) .ToolTip(LOCTEXT("NoMessagesTooltip", "No messages will be shown.")) + SSegmentedControl::Slot(ELogLevelFilter::Enabled) .Text(EnabledLabel) .ToolTip(LOCTEXT("EnabledMessagesTooltip", "Show messages from the enabled categories.")) + SSegmentedControl::Slot(ELogLevelFilter::All) .Text(AllLabel) .ToolTip(LOCTEXT("AllMessagesTooltip", "Show all messages, ignoring whether or not the category is enabled.")) ], FText::GetEmpty(), true ); MenuBuilder.AddWidget( SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .VAlign(VAlign_Center) .Padding(12, 0, 4, 0) [ SNew(STextBlock) .Text(LOCTEXT("Warnings", "Warnings")) .TextStyle(FAppStyle::Get(), "Menu.Label") .ColorAndOpacity(FLinearColor::White) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4, 0, 12, 2) [ SNew(SSegmentedControl) .Value(this, &SOutputLog::GetWarningsFilter) .OnValueChanged(this, &SOutputLog::OnWarningsFilterChanged) + SSegmentedControl::Slot(ELogLevelFilter::None) .Text(NoneLabel) .ToolTip(LOCTEXT("NoWarningsTooltip", "No warnings will be shown.")) + SSegmentedControl::Slot(ELogLevelFilter::Enabled) .Text(EnabledLabel) .ToolTip(LOCTEXT("EnabledWarningsTooltip", "Show warnings from the enabled categories.")) + SSegmentedControl::Slot(ELogLevelFilter::All) .Text(AllLabel) .ToolTip(LOCTEXT("AllWarningsTooltip", "Show all warnings, ignoring whether or not the category is enabled.")) ], FText::GetEmpty(), true ); MenuBuilder.AddWidget( SNew(SHorizontalBox) + SHorizontalBox::Slot() .FillWidth(1.0) .VAlign(VAlign_Center) .Padding(12, 0, 4, 0) [ SNew(STextBlock) .Text(LOCTEXT("Errors", "Errors")) .TextStyle(FAppStyle::Get(), "Menu.Label") .ColorAndOpacity(FLinearColor::White) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4, 0, 12, 2) [ SNew(SSegmentedControl) .Value(this, &SOutputLog::GetErrorsFilter) .OnValueChanged(this, &SOutputLog::OnErrorsFilterChanged) + SSegmentedControl::Slot(ELogLevelFilter::None) .Text(NoneLabel) .ToolTip(LOCTEXT("NoErrorsTooltip", "No errors will be shown.")) + SSegmentedControl::Slot(ELogLevelFilter::Enabled) .Text(EnabledLabel) .ToolTip(LOCTEXT("EnabledErrorsTooltip", "Show errors from the enabled categories.")) + SSegmentedControl::Slot(ELogLevelFilter::All) .Text(AllLabel) .ToolTip(LOCTEXT("AllErrorsTooltip", "Show all errors, ignoring whether or not the category is enabled.")) ], FText::GetEmpty(), true ); } MenuBuilder.EndSection(); MenuBuilder.BeginSection("OutputLogBetterFilter", LOCTEXT("OutputLogFilterCategories", "Categories")); { MenuBuilder.AddSubMenu( LOCTEXT("AllFilterCategories", "Category Filters"), LOCTEXT("AllFilterCategoriesTooltip", "Select the log categories that are displayed."), FNewMenuDelegate::CreateSP(this, &SOutputLog::MakeSelectCategoriesSubMenu), FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::CategoriesShowAll_Execute), FCanExecuteAction::CreateLambda([] { return true; }), FGetActionCheckState::CreateSP(this, &SOutputLog::CategoriesShowAll_IsChecked)), NAME_None, EUserInterfaceActionType::ToggleButton ); } MenuBuilder.EndSection(); return MenuBuilder.MakeWidget(); } void SOutputLog::MakeSelectCategoriesSubMenu(FMenuBuilder& MenuBuilder) { MenuBuilder.BeginSection("OutputLogCategoriesEntries"); { for (const FOutputLogCategorySettings& Category : Filter.GetCategoryFilters()) { FString NameString = Category.Name.ToString(); MenuBuilder.AddMenuEntry( FText::AsCultureInvariant(NameString), FText::Format(LOCTEXT("Category_Tooltip", "Filter the Output Log to show category: {0}"), FText::AsCultureInvariant(NameString)), FSlateIcon(), FUIAction(FExecuteAction::CreateSP(this, &SOutputLog::CategoriesSingle_Execute, Category.Name), FCanExecuteAction::CreateLambda([] { return true; }), FIsActionChecked::CreateSP(this, &SOutputLog::CategoriesSingle_IsChecked, Category.Name)), NAME_None, EUserInterfaceActionType::ToggleButton ); } } MenuBuilder.EndSection(); } ELogLevelFilter SOutputLog::GetMessagesFilter() const { return Filter.MessagesFilter; } void SOutputLog::OnMessagesFilterChanged(ELogLevelFilter NewFilter) { Filter.MessagesFilter = NewFilter; MessagesTextMarshaller->MarkMessagesCacheAsDirty(); Refresh(); } ELogLevelFilter SOutputLog::GetWarningsFilter() const { return Filter.WarningsFilter; } void SOutputLog::OnWarningsFilterChanged(ELogLevelFilter NewFilter) { Filter.WarningsFilter = NewFilter; MessagesTextMarshaller->MarkMessagesCacheAsDirty(); Refresh(); } ELogLevelFilter SOutputLog::GetErrorsFilter() const { return Filter.ErrorsFilter; } void SOutputLog::OnErrorsFilterChanged(ELogLevelFilter NewFilter) { Filter.ErrorsFilter = NewFilter; MessagesTextMarshaller->MarkMessagesCacheAsDirty(); Refresh(); } ECheckBoxState SOutputLog::CategoriesShowAll_IsChecked() const { return Filter.AreAllCategoriesSelected(); } bool SOutputLog::CategoriesSingle_IsChecked(FName InName) const { return Filter.IsLogCategoryEnabled(InName); } void SOutputLog::CategoriesShowAll_Execute() { const ECheckBoxState CurrentState = Filter.AreAllCategoriesSelected(); const bool NextState = CurrentState != ECheckBoxState::Checked; Filter.SetAllCategoriesEnabled(NextState); // 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(); } void SOutputLog::UpdateOutputLogFilter(const FOutputLogFilter& InFilter) { Filter = InFilter; MessagesTextMarshaller->MarkMessagesCacheAsDirty(); Refresh(); } void SOutputLog::UpdateOutputLogFilter(const FOutputLogFilterSettings& InSettings) { Filter.ApplySettings(InSettings); MessagesTextMarshaller->MarkMessagesCacheAsDirty(); Refresh(); } namespace { template TSharedPtr GetWidgetFromContext(const TContext& InContext) { UOutputLogMenuContext* Context = InContext.template FindContext(); if (!ensure(Context)) { return nullptr; } TSharedPtr Widget = Context->GetOutputLog(); ensure(Widget); return Widget; }; }; // static void SOutputLog::RegisterSettingsMenu() { // We declare the menu structure during module load, but instantiate the // widget much later. Because of this, predicates/actions need to "late // bind" to the instance, by pulling it back out of the FToolMenuContext // or FToolMenuSection. See GetWidgetFromContext() above. UToolMenus* ToolMenus = UToolMenus::Get(); if (!ensure(ToolMenus)) { return; } if (ensure(!ToolMenus->IsMenuRegistered(SettingsMenuName))) { UToolMenu* Menu = ToolMenus->RegisterMenu(SettingsMenuName); FToolMenuSection& Section = Menu->AddSection(NAME_None); RegisterSettingsMenu_WordWrap(Section); RegisterSettingsMenu_TimestampMode(Section); RegisterSettingsMenu_ClearOnPIE(Section); Section.AddSeparator(SettingsSeparatorName); RegisterSettingsMenu_BrowseLogs(Section); RegisterSettingsMenu_OpenLogExternal(Section); } } // static void SOutputLog::RegisterSettingsMenu_WordWrap(FToolMenuSection& InSection) { FToolUIAction WordWrapAction; WordWrapAction.ExecuteAction = FToolMenuExecuteAction::CreateLambda([](const FToolMenuContext& InContext) { if (TSharedPtr This = GetWidgetFromContext(InContext); ensure(This)) { This->SetWordWrapEnabled(This->IsWordWrapEnabled() ? ECheckBoxState::Unchecked : ECheckBoxState::Checked); } }); WordWrapAction.GetActionCheckState = FToolMenuGetActionCheckState::CreateLambda([](const FToolMenuContext& InContext) -> ECheckBoxState { if (TSharedPtr This = GetWidgetFromContext(InContext); ensure(This)) { return This->IsWordWrapEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } return ECheckBoxState::Unchecked; }); InSection.AddMenuEntry( SettingsWordWrapEntryName, LOCTEXT("WordWrapEnabledOption", "Enable Word Wrapping"), LOCTEXT("WordWrapEnabledOptionToolTip", "Enable word wrapping in the Output Log."), FSlateIcon(), WordWrapAction, EUserInterfaceActionType::ToggleButton ); } // static void SOutputLog::RegisterSettingsMenu_TimestampMode(FToolMenuSection& InSection) { InSection.AddDynamicEntry(SettingsTimestampsSubMenuName, FNewToolMenuSectionDelegate::CreateLambda([](FToolMenuSection& InSection) { FText TimestampModeTooltip; #if WITH_EDITORONLY_DATA TimestampModeTooltip = UOutputLogSettings::StaticClass()->FindPropertyByName( GET_MEMBER_NAME_CHECKED(UOutputLogSettings, LogTimestampMode))->GetToolTipText(); #endif // WITH_EDITORONLY_DATA if (TSharedPtr This = GetWidgetFromContext(InSection); ensure(This)) { InSection.AddSubMenu( SettingsTimestampsSubMenuName, TAttribute::CreateLambda([This]() { const UEnum* Enum = StaticEnum(); return FText::Format(LOCTEXT("TimestampsSubmenu", "Timestamp Mode: {0}"), Enum->GetDisplayNameTextByIndex(This->GetSelectedTimestampMode())); }), TimestampModeTooltip, FNewMenuDelegate::CreateSP(This.ToSharedRef(), &SOutputLog::AddTimestampMenuSection) ); } })); } // static void SOutputLog::RegisterSettingsMenu_ClearOnPIE(FToolMenuSection& InSection) { #if WITH_EDITOR FToolUIAction ClearOnPIEAction; ClearOnPIEAction.ExecuteAction = FToolMenuExecuteAction::CreateLambda([](const FToolMenuContext& InContext) { if (TSharedPtr This = GetWidgetFromContext(InContext); ensure(This)) { This->SetClearOnPIE(This->IsClearOnPIEEnabled() ? ECheckBoxState::Unchecked : ECheckBoxState::Checked); } }); ClearOnPIEAction.GetActionCheckState = FToolMenuGetActionCheckState::CreateLambda([](const FToolMenuContext& InContext) -> ECheckBoxState { if (TSharedPtr This = GetWidgetFromContext(InContext); ensure(This)) { return This->IsClearOnPIEEnabled() ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; } return ECheckBoxState::Unchecked; }); InSection.AddMenuEntry( SettingsClearOnPIEEntryName, LOCTEXT("ClearOnPIE", "Clear on PIE"), LOCTEXT("ClearOnPIEToolTip", "Enable clearing of the Output Log on PIE startup."), FSlateIcon(), ClearOnPIEAction, EUserInterfaceActionType::ToggleButton ); #endif } // static void SOutputLog::RegisterSettingsMenu_BrowseLogs(FToolMenuSection& InSection) { InSection.AddMenuEntry( SettingsBrowseLogDirectoryEntryName, LOCTEXT("FindSourceFile", "Open Source Location"), LOCTEXT("FindSourceFileTooltip", "Opens the folder containing the source of the Output Log."), FSlateIcon(FOutputLogStyle::Get().GetStyleSetName(), "OutputLog.OpenSourceLocation"), FToolMenuExecuteAction::CreateLambda([](const FToolMenuContext& InContext) { if (TSharedPtr This = GetWidgetFromContext(InContext); ensure(This)) { This->OpenLogFileInExplorer(); } }) ); } // static void SOutputLog::RegisterSettingsMenu_OpenLogExternal(FToolMenuSection& InSection) { InSection.AddMenuEntry( SettingsOpenLogExternalEntryName, LOCTEXT("OpenInExternalEditor", "Open In External Editor"), LOCTEXT("OpenInExternalEditorTooltip", "Opens the Output Log in the default external editor."), FSlateIcon(FOutputLogStyle::Get().GetStyleSetName(), "OutputLog.OpenInExternalEditor"), FToolMenuExecuteAction::CreateLambda([](const FToolMenuContext& InContext) { if (TSharedPtr This = GetWidgetFromContext(InContext); ensure(This)) { This->OpenLogFileInExternalEditor(); } }) ); } FName SOutputLog::GetSettingsMenuProfileForFlags(EOutputLogSettingsMenuFlags InFlags) { UToolMenus* ToolMenus = UToolMenus::Get(); if (!ensure(ToolMenus) || InFlags == EOutputLogSettingsMenuFlags::None) { return NAME_None; } const FName MenuProfileName = *FString::Printf(TEXT("OutputLogSettings_Flags%i"), static_cast(InFlags)); FToolMenuProfile* FlagsProfile = ToolMenus->FindRuntimeMenuProfile(SettingsMenuName, MenuProfileName); if (!FlagsProfile) { FlagsProfile = ToolMenus->AddRuntimeMenuProfile(SettingsMenuName, MenuProfileName); const bool bSupportWordWrapping = !EnumHasAnyFlags(InFlags, EOutputLogSettingsMenuFlags::SkipEnableWordWrapping); const bool bSupportClearOnPie = !EnumHasAnyFlags(InFlags, EOutputLogSettingsMenuFlags::SkipClearOnPie); const bool bSupportBrowseLocation = !EnumHasAnyFlags(InFlags, EOutputLogSettingsMenuFlags::SkipOpenSourceButton); const bool bSupportExternalEditor = !EnumHasAnyFlags(InFlags, EOutputLogSettingsMenuFlags::SkipOpenInExternalEditorButton); const bool bNeedsSeparator = (bSupportWordWrapping || bSupportClearOnPie) && (bSupportBrowseLocation || bSupportExternalEditor); if (!bSupportWordWrapping) { FlagsProfile->AddEntry(SettingsWordWrapEntryName)->Visibility = ECustomizedToolMenuVisibility::Hidden; } if (!bSupportClearOnPie) { FlagsProfile->AddEntry(SettingsClearOnPIEEntryName)->Visibility = ECustomizedToolMenuVisibility::Hidden; } if (!bNeedsSeparator) { FlagsProfile->AddEntry(SettingsSeparatorName)->Visibility = ECustomizedToolMenuVisibility::Hidden; } if (!bSupportBrowseLocation) { FlagsProfile->AddEntry(SettingsBrowseLogDirectoryEntryName)->Visibility = ECustomizedToolMenuVisibility::Hidden; } if (!bSupportExternalEditor) { FlagsProfile->AddEntry(SettingsOpenLogExternalEntryName)->Visibility = ECustomizedToolMenuVisibility::Hidden; } } return MenuProfileName; } TSharedRef SOutputLog::GetSettingsMenuContent(FName InMenuProfileName) { UToolMenus* ToolMenus = UToolMenus::Get(); if (!ensure(ToolMenus)) { return SNullWidget::NullWidget; } FToolMenuContext MenuContext; UOutputLogMenuContext* OutputLogContext = NewObject(); OutputLogContext->Init(SharedThis(this)); MenuContext.AddObject(OutputLogContext); if (InMenuProfileName != NAME_None) { UToolMenuProfileContext* ProfileContext = NewObject(); ProfileContext->ActiveProfiles.Add(InMenuProfileName); MenuContext.AddObject(ProfileContext); } return ToolMenus->GenerateWidget(SettingsMenuName, MenuContext); } TSharedRef SOutputLog::CreateDrawerDockButton() { if (bShouldCreateDrawerDockButton) { return SNew(SButton) .ButtonStyle(FOutputLogStyle::Get(), "SimpleButton") .ToolTipText(LOCTEXT("DockInLayout_Tooltip", "Docks this output log in the current layout.\nThe drawer will still be usable as a temporary log.")) .ContentPadding(FMargin(1, 0)) .Visibility_Lambda( []() { return FOutputLogModule::Get().GetOutputLogTab() == nullptr ? EVisibility::Visible : EVisibility::Hidden; }) .OnClicked(this, &SOutputLog::OnDockInLayoutClicked) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4.0, 0.0f) [ SNew(SImage) .ColorAndOpacity(FSlateColor::UseForeground()) .Image(FOutputLogStyle::Get().GetBrush("Icons.Layout")) ] + SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(4.0, 0.0f) [ SNew(STextBlock) .Text(LOCTEXT("DockInLayout", "Dock in Layout")) .ColorAndOpacity(FSlateColor::UseForeground()) ] ]; } return SNullWidget::NullWidget; } void SOutputLog::OpenLogFileInExplorer() { FString Path = FPaths::ConvertRelativePathToFull(FPaths::ProjectLogDir()); if (!Path.Len() || !IFileManager::Get().DirectoryExists(*Path)) { return; } FPlatformProcess::ExploreFolder(*FPaths::GetPath(Path)); } void SOutputLog::OpenLogFileInExternalEditor() { FString Path = FPaths::ConvertRelativePathToFull(FGenericPlatformOutputDevices::GetAbsoluteLogFilename()); if (!Path.Len() || IFileManager::Get().FileSize(*Path) == INDEX_NONE) { return; } FPlatformProcess::LaunchFileInDefaultExternalApplication(*Path, NULL, ELaunchVerb::Open); } FReply SOutputLog::OnDockInLayoutClicked() { TSharedPtr DockedTab; // Export our settings so that the docked tab starts from the current state. UOutputLogSettings* Settings = GetMutableDefault(); Filter.ExportSettings(Settings->OutputLogTabFilter); Settings->SaveConfig(); static const FName OutputLogTabName = FName("OutputLog"); if (TSharedPtr ActiveTab = FGlobalTabmanager::Get()->GetActiveTab()) { if (TSharedPtr TabManager = ActiveTab->GetTabManagerPtr()) { DockedTab = TabManager->TryInvokeTab(OutputLogTabName); } } if (!DockedTab) { FGlobalTabmanager::Get()->TryInvokeTab(OutputLogTabName); } return FReply::Handled(); } ECheckBoxState FOutputLogFilter::AreAllCategoriesSelected() const { int32 Count = 0; for (const FOutputLogCategorySettings& Item : Categories) { if (Item.bEnabled) { Count++; } } if (Count == Categories.Num()) { return ECheckBoxState::Checked; } if (Count == 0) { return ECheckBoxState::Unchecked; } return ECheckBoxState::Undetermined; } void FOutputLogFilter::ApplySettings(const FOutputLogFilterSettings& InSettings) { MessagesFilter = InSettings.MessagesFilter; WarningsFilter = InSettings.WarningsFilter; ErrorsFilter = InSettings.ErrorsFilter; bSelectNewCategories = InSettings.bSelectNewCategories; SetFilterText(InSettings.FilterText); if (InSettings.Categories.Num() == 0) { // This implies all _should_ be selected SetAllCategoriesEnabled(true); } else { // Clear and apply piecemeal so that configured logs that // haven't been hit yet are still added. SetAllCategoriesEnabled(false); for (const FOutputLogCategorySettings& Category : InSettings.Categories) { SetLogCategoryEnabled(Category.Name, Category.bEnabled); } } } void FOutputLogFilter::ExportSettings(FOutputLogFilterSettings& OutSettings) const { OutSettings.MessagesFilter = MessagesFilter; OutSettings.WarningsFilter = WarningsFilter; OutSettings.ErrorsFilter = ErrorsFilter; OutSettings.bSelectNewCategories = bSelectNewCategories; if (AreAllCategoriesSelected() == ECheckBoxState::Checked) { OutSettings.Categories.Empty(); } else { OutSettings.Categories = Categories; } } bool FOutputLogFilter::IsMessageAllowed(const TSharedPtr& Message) const { // Filter Verbosity const ELogLevelFilter LevelFilter = GetMessageLevelFilter(Message); if (LevelFilter == ELogLevelFilter::None) { return false; } // Filter by Category if (!IgnoreFilterVerbosities.Contains(Message->Verbosity) && LevelFilter == ELogLevelFilter::Enabled && !IsLogCategoryEnabled(Message->Category)) { return false; } // Filter search phrase if (!TextFilterExpressionEvaluator.TestTextFilter(FLogFilter_TextFilterExpressionContextOutputLog(*Message))) { return false; } return true; } ELogLevelFilter FOutputLogFilter::GetMessageLevelFilter(const TSharedPtr& Message) const { switch (Message->Verbosity) { case ELogVerbosity::Error: return ErrorsFilter; case ELogVerbosity::Warning: return WarningsFilter; default: return MessagesFilter; } } void FOutputLogFilter::AddAvailableLogCategory(const FName& LogCategory, TOptional InitiallySelected) { // Use an insert-sort to keep AvailableLogCategories alphabetically sorted int32 InsertIndex = 0; for (InsertIndex = Categories.Num()-1; InsertIndex >= 0; --InsertIndex) { FOutputLogCategorySettings& CheckCategory = Categories[InsertIndex]; // No duplicates if (CheckCategory.Name == LogCategory) { return; } else if (CheckCategory.Name.Compare(LogCategory) < 0) { break; } } Categories.Insert({ LogCategory, InitiallySelected.Get(bSelectNewCategories) }, InsertIndex+1); } bool FOutputLogFilter::IsLogCategoryAvailable(const FName& LogCategory) const { return FindCategoryFilter(LogCategory) != nullptr; } FOutputLogCategorySettings* FOutputLogFilter::FindCategoryFilter(const FName& LogCategory) { // TODO: Could in theory use a binary search as AddAvailableLogCategory() ensures `Categories` will be sorted. for (FOutputLogCategorySettings& Item : Categories) { if (Item.Name == LogCategory) { return &Item; } } return nullptr; } const FOutputLogCategorySettings* FOutputLogFilter::FindCategoryFilter(const FName& LogCategory) const { // TODO: Could in theory use a binary search as AddAvailableLogCategory() ensures `Categories` will be sorted. for (const FOutputLogCategorySettings& Item : Categories) { if (Item.Name == LogCategory) { return &Item; } } return nullptr; } void FOutputLogFilter::SetLogCategoryEnabled(const FName& LogCategory, bool bEnabled) { if (FOutputLogCategorySettings* CategoryFilter = FindCategoryFilter(LogCategory)) { CategoryFilter->bEnabled = bEnabled; } else { AddAvailableLogCategory(LogCategory, bEnabled); } } void FOutputLogFilter::SetAllCategoriesEnabled(bool bEnabled) { for (FOutputLogCategorySettings& Item : Categories) { Item.bEnabled = bEnabled; } } void FOutputLogFilter::ToggleLogCategory(const FName& LogCategory) { if (FOutputLogCategorySettings* CategoryFilter = FindCategoryFilter(LogCategory)) { CategoryFilter->bEnabled = !CategoryFilter->bEnabled; } else { AddAvailableLogCategory(LogCategory, true); } } bool FOutputLogFilter::IsLogCategoryEnabled(const FName& LogCategory) const { if (const FOutputLogCategorySettings* CategoryFilter = FindCategoryFilter(LogCategory)) { return CategoryFilter->bEnabled; } return false; } void FOutputLogFilter::ClearSelectedLogCategories() { SetAllCategoriesEnabled(false); } #undef LOCTEXT_NAMESPACE #pragma optimize("", on)