// Copyright Epic Games, Inc. All Rights Reserved. #include "OutputLogModule.h" #include "Modules/ModuleManager.h" #include "Framework/Application/SlateApplication.h" #include "Textures/SlateIcon.h" #include "Framework/Docking/TabManager.h" #include "Styling/AppStyle.h" #include "OutputLogCreationParams.h" #include "SDebugConsole.h" #include "SOutputLog.h" #include "SDeviceOutputLog.h" #include "Widgets/Docking/SDockTab.h" #include "Misc/ConfigCacheIni.h" #if WITH_EDITOR #include "Editor.h" #include "ISettingsModule.h" #endif #if WITH_EDITOR || (IS_PROGRAM && WITH_UNREAL_DEVELOPER_TOOLS) #include "WorkspaceMenuStructure.h" #include "WorkspaceMenuStructureModule.h" #include "OutputLogSettings.h" #endif #include "Internationalization/Internationalization.h" #include "OutputLogStyle.h" #if WITH_EDITOR #include "StatusBarSubsystem.h" #endif IMPLEMENT_MODULE(FOutputLogModule, OutputLog); namespace OutputLogModule { static const FName OutputLogTabName = FName(TEXT("OutputLog")); static const FName DeviceOutputLogTabName = FName(TEXT("DeviceOutputLog")); bool bHideConsole = false; FAutoConsoleVariableRef CVarHideConsoleCommand( TEXT("OutputLogModule.HideConsole"), bHideConsole, TEXT("Whether debug console widgets should be hidden (false by default)"), FConsoleVariableDelegate::CreateLambda([](IConsoleVariable* /*CVar*/) { if (bHideConsole) { FOutputLogModule::Get().CloseDebugConsole(); } }), ECVF_ReadOnly); } /** This class is to capture all log output even if the log window is closed */ class FOutputLogHistory : public FOutputDevice { public: FOutputLogHistory() { GLog->AddOutputDevice(this); } ~FOutputLogHistory() { // At shutdown, GLog may already be null if (GLog != NULL) { GLog->RemoveOutputDevice(this); } } /** Gets all captured messages */ const TArray< TSharedPtr >& GetMessages() const { return Messages; } void Suspend() { bSuspended = true; } void Resume() { bSuspended = false; } protected: virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const class FName& Category) override { if (!bSuspended) { // Capture all incoming messages and store them in history SOutputLog::CreateLogMessages(V, Verbosity, Category, Messages); } } private: /** All log messsges since this module has been started */ TArray< TSharedPtr > Messages; bool bSuspended = false; }; TSharedRef FOutputLogModule::SpawnOutputLogTab(const FSpawnTabArgs& Args) { TSharedRef NewLog = SNew(SOutputLog, false).Messages(OutputLogHistory->GetMessages()); NewLog->UpdateOutputLogFilter(GetDefault()->OutputLogTabFilter); OutputLog = NewLog; TSharedRef NewTab = SNew(SDockTab) .TabRole(ETabRole::NomadTab) .Label(NSLOCTEXT("OutputLog", "TabTitle", "Output Log")) .OnPersistVisualState_Raw(this, &FOutputLogModule::SaveDockedTabSettings) [ NewLog ]; OutputLogTab = NewTab; return NewTab; } void FOutputLogModule::SaveDockedTabSettings() { if (TSharedPtr SharedOutputLog = OutputLog.Pin()) { // Cache the closing LogFilterTab so that we can restore the same filter when it's opened again UOutputLogSettings* Settings = GetMutableDefault(); SharedOutputLog->GetOutputLogFilter().ExportSettings(Settings->OutputLogTabFilter); Settings->SaveConfig(); } } TSharedRef FOutputLogModule::SpawnDeviceOutputLogTab(const FSpawnTabArgs& Args) { return SNew(SDockTab) .TabRole(ETabRole::NomadTab) .Label(NSLOCTEXT("OutputLog", "DeviceTabTitle", "Device Output Log")) [ SNew(SDeviceOutputLog) ]; } void FOutputLogModule::StartupModule() { FOutputLogStyle::Get(); #if WITH_EDITOR ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings"); if (SettingsModule) { SettingsModule->RegisterSettings("Editor", "General", "Output Log", NSLOCTEXT("OutputLog", "OutputLogSettingsName", "Output Log"), NSLOCTEXT("OutputLog", "OutputLogSettingsDescription", "Set up preferences for the Output Log appearance and workflow."), GetMutableDefault() ); } FEditorDelegates::BeginPIE.AddRaw(this, &FOutputLogModule::ClearOnPIE); #endif #if WITH_EDITOR || (IS_PROGRAM && WITH_UNREAL_DEVELOPER_TOOLS) FGlobalTabmanager::Get()->RegisterNomadTabSpawner(OutputLogModule::OutputLogTabName, FOnSpawnTab::CreateRaw(this, &FOutputLogModule::SpawnOutputLogTab)) .SetDisplayName(NSLOCTEXT("OutputLog", "OutputLogTab", "Output Log")) .SetTooltipText(NSLOCTEXT("OutputLog", "OutputLogTooltipText", "Open the Output Log tab.")) .SetGroup(WorkspaceMenu::GetMenuStructure().GetDeveloperToolsLogCategory()) .SetIcon(FSlateIcon(FOutputLogStyle::Get().GetStyleSetName(), "Log.TabIcon")); FGlobalTabmanager::Get()->RegisterNomadTabSpawner(OutputLogModule::DeviceOutputLogTabName, FOnSpawnTab::CreateRaw(this, &FOutputLogModule::SpawnDeviceOutputLogTab)) .SetDisplayName(NSLOCTEXT("OutputLog", "DeviceOutputLogTab", "Device Output Log")) .SetTooltipText(NSLOCTEXT("OutputLog", "DeviceOutputLogTooltipText", "Open the Device Output Log tab.")) .SetGroup(WorkspaceMenu::GetMenuStructure().GetDeveloperToolsLogCategory()) .SetIcon(FSlateIcon(FOutputLogStyle::Get().GetStyleSetName(), "Log.TabIcon")); #endif OutputLogHistory = MakeShareable(new FOutputLogHistory); OutputLogFilterCache = MakeUnique(); SOutputLog::RegisterSettingsMenu(); } void FOutputLogModule::ShutdownModule() { #if WITH_EDITOR || (IS_PROGRAM && WITH_UNREAL_DEVELOPER_TOOLS) if (FSlateApplication::IsInitialized()) { FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(OutputLogModule::OutputLogTabName); FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(OutputLogModule::DeviceOutputLogTabName); } #endif #if WITH_EDITOR FEditorDelegates::BeginPIE.RemoveAll(this); #endif FOutputLogStyle::Shutdown(); OutputLogHistory.Reset(); } const FName FOutputLogModule::ModuleName = UE_MODULE_NAME; FOutputLogModule& FOutputLogModule::Get() { return FModuleManager::Get().LoadModuleChecked(ModuleName); } FOutputLogModule* FOutputLogModule::TryGet() { return FModuleManager::Get().GetModulePtr(ModuleName); } bool FOutputLogModule::ShouldHideConsole() const { return OutputLogModule::bHideConsole; } TSharedRef FOutputLogModule::MakeConsoleInputBox(TSharedPtr& OutExposedEditableTextBox, const FSimpleDelegate& OnCloseConsole, const FSimpleDelegate& OnConsoleCommandExecuted) const { TSharedRef NewConsoleInputBox = SNew(SConsoleInputBox) .Visibility(MakeAttributeLambda([](){ return FOutputLogModule::Get().ShouldHideConsole() ? EVisibility::Collapsed : EVisibility::Visible; })) .OnCloseConsole(OnCloseConsole) .OnConsoleCommandExecuted(OnConsoleCommandExecuted); OutExposedEditableTextBox = NewConsoleInputBox->GetEditableTextBox(); return NewConsoleInputBox; } TSharedRef FOutputLogModule::MakeOutputLogDrawerWidget(const FSimpleDelegate& OnCloseConsole) { TSharedPtr OutputLogDrawerPinned = OutputLogDrawer.Pin(); if (!OutputLogDrawerPinned.IsValid()) { OutputLogDrawerPinned = SNew(SOutputLog, true) .OnCloseConsole(OnCloseConsole) .OnClearLog_Lambda([] { if (FOutputLogModule* OutputLogModule = FOutputLogModule::TryGet()) { OutputLogModule->OnOutputLogDrawerCleared.Broadcast(); } }) .Messages(OutputLogHistory->GetMessages()); OutputLogDrawerPinned->UpdateOutputLogFilter(*OutputLogFilterCache); OutputLogDrawer = OutputLogDrawerPinned; } return OutputLogDrawerPinned.ToSharedRef(); } TSharedRef FOutputLogModule::MakeOutputLogWidget(const FOutputLogCreationParams& Params) { return SNew(SOutputLog, Params.bCreateDockInLayoutButton) .OnCloseConsole(Params.OnCloseConsole) .Messages(OutputLogHistory->GetMessages()) .SettingsMenuFlags(Params.SettingsMenuCreationFlags) .DefaultCategorySelection(Params.DefaultCategorySelection) .AllowInitialLogCategory(Params.AllowAsInitialLogCategory) .EnableLoggingLimitMenu(Params.LoggingLimit.IsSet()) .LoggingLineLimit(Params.LoggingLimit); } void FOutputLogModule::SuspendGlobalLog() { check(OutputLogHistory); OutputLogHistory->Suspend(); } void FOutputLogModule::ResumeGlobalLog() { check(OutputLogHistory); OutputLogHistory->Resume(); } void FOutputLogModule::ToggleDebugConsoleForWindow(const TSharedRef& Window, const EDebugConsoleStyle::Type InStyle, const FDebugConsoleDelegates& DebugConsoleDelegates) { if (ShouldHideConsole()) { return; } bool bShouldOpen = true; // Close an existing console box, if there is one TSharedPtr< SWidget > PinnedDebugConsole(DebugConsole.Pin()); if (PinnedDebugConsole.IsValid()) { // If the console is already open close it unless it is in a different window. In that case reopen it on that window bShouldOpen = false; TSharedPtr< SWindow > WindowForExistingConsole = FSlateApplication::Get().FindWidgetWindow(PinnedDebugConsole.ToSharedRef()); if (WindowForExistingConsole.IsValid()) { if (PreviousKeyboardFocusedWidget.IsValid()) { FSlateApplication::Get().SetKeyboardFocus(PreviousKeyboardFocusedWidget.Pin()); PreviousKeyboardFocusedWidget.Reset(); } WindowForExistingConsole->RemoveOverlaySlot(PinnedDebugConsole.ToSharedRef()); DebugConsole.Reset(); } if (WindowForExistingConsole != Window) { // Console is being opened on another window bShouldOpen = true; } } TSharedPtr ActiveTab = FGlobalTabmanager::Get()->GetActiveTab(); if (ActiveTab.IsValid() && ActiveTab == OutputLogTab) { FGlobalTabmanager::Get()->DrawAttention(ActiveTab.ToSharedRef()); bShouldOpen = false; } if (bShouldOpen) { const EDebugConsoleStyle::Type DebugConsoleStyle = InStyle; TSharedRef< SDebugConsole > DebugConsoleRef = SNew(SDebugConsole, DebugConsoleStyle, this, &DebugConsoleDelegates); DebugConsole = DebugConsoleRef; const int32 MaximumZOrder = MAX_int32; Window->AddOverlaySlot(MaximumZOrder) .VAlign(VAlign_Bottom) .HAlign(HAlign_Center) .Padding(10.0f) [ DebugConsoleRef ]; PreviousKeyboardFocusedWidget = FSlateApplication::Get().GetKeyboardFocusedWidget(); // Force keyboard focus DebugConsoleRef->SetFocusToEditableText(); } } void FOutputLogModule::CloseDebugConsole() { TSharedPtr< SWidget > PinnedDebugConsole(DebugConsole.Pin()); if (PinnedDebugConsole.IsValid()) { TSharedPtr< SWindow > WindowForExistingConsole = FSlateApplication::Get().FindWidgetWindow(PinnedDebugConsole.ToSharedRef()); if (WindowForExistingConsole.IsValid()) { WindowForExistingConsole->RemoveOverlaySlot(PinnedDebugConsole.ToSharedRef()); DebugConsole.Reset(); if (TSharedPtr PreviousKeyboardFocusedWidgetPinned = PreviousKeyboardFocusedWidget.Pin()) { FSlateApplication::Get().SetKeyboardFocus(PreviousKeyboardFocusedWidgetPinned); PreviousKeyboardFocusedWidget.Reset(); } } } } void FOutputLogModule::ClearOnPIE(const bool bIsSimulating) { bool bClearOnPIEEnabled = false; GConfig->GetBool(TEXT("/Script/OutputLog.OutputLogSettings"), TEXT("bEnableOutputLogClearOnPIE"), bClearOnPIEEnabled, GEditorPerProjectIni); if (bClearOnPIEEnabled) { if (TSharedPtr OutputLogPinned = OutputLog.Pin()) { if (OutputLogPinned->CanClearLog()) { OutputLogPinned->OnClearLog(); } } if (TSharedPtr OutputLogPinned = OutputLogDrawer.Pin()) { if (OutputLogPinned->CanClearLog()) { OutputLogPinned->OnClearLog(); } } } } void FOutputLogModule::FocusOutputLogConsoleBox(const TSharedRef OutputLogToFocus) { if (OutputLog == OutputLogToFocus) { OutputLog.Pin()->FocusConsoleCommandBox(); } else if (OutputLogDrawer == OutputLogToFocus) { OutputLogDrawer.Pin()->FocusConsoleCommandBox(); } } const TSharedPtr FOutputLogModule::GetOutputLog() const { return OutputLog.Pin(); } void FOutputLogModule::FocusOutputLog() { // 1. Output log tab is open but not active. if (OutputLog.IsValid()) { OutputLogTab.Pin()->DrawAttention(); } #if WITH_EDITOR // 2. Output log tab isn't open and the window directly behind the notification window has a status bar, then open Output Log Drawer. else if (GEditor->GetEditorSubsystem()->ActiveWindowBehindNotificationHasStatusBar()) { TSharedRef ParentWindow = FSlateApplication::Get().GetActiveTopLevelRegularWindow().ToSharedRef(); // try toggle the console to open the Output Log Drawer if (GEditor->GetEditorSubsystem()->ToggleDebugConsole(ParentWindow, true)) { OutputLogDrawer.Pin()->FocusConsoleCommandBox(); } // if unable to open the drawer, invoke a new Output Log tab. else { OpenOutputLog(); OutputLogTab.Pin()->DrawAttention(); } } #endif // 3. The parent window has no status bar, then invoke a new Output Log tab. else { OpenOutputLog(); OutputLogTab.Pin()->DrawAttention(); } } void FOutputLogModule::FocusOutputLogAndScrollToEnd() { FocusOutputLog(); if (TSharedPtr Log = OutputLog.Pin()) { Log->OnConsoleCommandExecuted(); } if (TSharedPtr Log = OutputLogDrawer.Pin()) { Log->OnConsoleCommandExecuted(); } } void FOutputLogModule::UpdateOutputLogFilter(const TArray& CategoriesToShow, TOptional bShowErrors, TOptional bShowWarnings, TOptional bShowLogs) { FOutputFilterParams Params; Params.bShowErrors = bShowErrors; Params.bShowWarnings = bShowWarnings; Params.bShowLogs = bShowLogs; UpdateOutputLogFilter(CategoriesToShow, Params); } void FOutputLogModule::UpdateOutputLogFilter(const TArray& CategoriesToShow, const FOutputFilterParams& InParams) { if (InParams.bShowErrors.IsSet()) { OutputLogFilterCache->ErrorsFilter = InParams.bShowErrors.GetValue() ? ELogLevelFilter::Enabled : ELogLevelFilter::None; } // Update the filter cache to these new settings // This will be useful for the case where the OutputLog Drawer or Tab get created after this call if (InParams.bShowWarnings.IsSet()) { OutputLogFilterCache->WarningsFilter = InParams.bShowWarnings.GetValue() ? ELogLevelFilter::Enabled : ELogLevelFilter::None; } if (InParams.bShowLogs.IsSet()) { OutputLogFilterCache->MessagesFilter = InParams.bShowLogs.GetValue() ? ELogLevelFilter::Enabled : ELogLevelFilter::None; } if (InParams.IgnoreFilterVerbosities.IsSet()) { // Redirect Log/Warning/Error versions to the enum variants. // Only using the set for compatability OutputLogFilterCache->IgnoreFilterVerbosities.Reset(); for (ELogVerbosity::Type Verbosity : InParams.IgnoreFilterVerbosities.GetValue()) { switch (Verbosity) { case ELogVerbosity::Log: OutputLogFilterCache->MessagesFilter = ELogLevelFilter::All; break; case ELogVerbosity::Warning: OutputLogFilterCache->WarningsFilter = ELogLevelFilter::All; break; case ELogVerbosity::Error: OutputLogFilterCache->ErrorsFilter = ELogLevelFilter::All; break; default: OutputLogFilterCache->IgnoreFilterVerbosities.Add(Verbosity); } } } if (CategoriesToShow.IsEmpty()) { OutputLogFilterCache->SetAllCategoriesEnabled(true); } else { OutputLogFilterCache->SetAllCategoriesEnabled(false); for (const FName& CategoryToShow : CategoriesToShow) { OutputLogFilterCache->SetLogCategoryEnabled(CategoryToShow, true); } } if (TSharedPtr SharedOutputLog = OutputLog.Pin()) { SharedOutputLog->UpdateOutputLogFilter(*OutputLogFilterCache); } if (TSharedPtr SharedOutputLogDrawer = OutputLogDrawer.Pin()) { SharedOutputLogDrawer->UpdateOutputLogFilter(*OutputLogFilterCache); } } void FOutputLogModule::OpenOutputLog() const { FGlobalTabmanager::Get()->TryInvokeTab(OutputLogModule::OutputLogTabName); } bool FOutputLogModule::ShouldCycleToOutputLogDrawer() const { #if WITH_EDITOR || (IS_PROGRAM && WITH_UNREAL_DEVELOPER_TOOLS) return GetDefault()->bCycleToOutputLogDrawer; #else return true; #endif }