// Copyright Epic Games, Inc. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Layout/Visibility.h" #include "Widgets/DeclarativeSyntaxSupport.h" #include "ILauncherWorker.h" #include "Input/Reply.h" #include "Layout/Margin.h" #include "Widgets/SCompoundWidget.h" #include "Misc/Paths.h" #include "HAL/FileManager.h" #include "HAL/PlatformApplicationMisc.h" #include "Types/SlateStructs.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Layout/SBorder.h" #include "Widgets/Text/STextBlock.h" #include "Widgets/Input/SButton.h" #include "Widgets/Layout/SSplitter.h" #include "Styling/AppStyle.h" #include "Widgets/Views/STableViewBase.h" #include "Widgets/Views/SHeaderRow.h" #include "Widgets/Layout/SBox.h" #include "Widgets/Views/STableRow.h" #include "Misc/MessageDialog.h" #include "Misc/ScopeLock.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Notifications/SProgressBar.h" #include "DesktopPlatformModule.h" #include "Widgets/Layout/SScrollBar.h" #include "Framework/Layout/Overscroll.h" #include "Widgets/Views/SListView.h" #include "Widgets/Layout/SGridPanel.h" #include "Widgets/Layout/SScrollBox.h" #include "Widgets/Progress/SProjectLauncherTaskListRow.h" #include "Widgets/Progress/SProjectLauncherMessageListRow.h" #define LOCTEXT_NAMESPACE "SProjectLauncherProgress" /** * Implements the launcher's progress page. */ class SProjectLauncherProgress : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SProjectLauncherProgress) { } SLATE_EVENT(FOnClicked, OnCloseClicked) SLATE_EVENT(FOnClicked, OnRerunClicked) SLATE_END_ARGS() public: /** * Destructor. */ ~SProjectLauncherProgress( ) { } public: /** * Constructs the widget. * * @param InArgs The Slate argument list. */ void Construct( const FArguments& InArgs ) { OnCloseClicked = InArgs._OnCloseClicked; OnRerunClicked = InArgs._OnRerunClicked; TSharedRef HorizontalScrollBar = SNew(SScrollBar) .Orientation(EOrientation::Orient_Horizontal) .AlwaysShowScrollbar(true); TSharedRef VerticalScrollBar = SNew(SScrollBar) .Orientation(EOrientation::Orient_Vertical) .AlwaysShowScrollbar(true); MaxMessageListRowWidth = 0.0f; ChildSlot [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() .Padding(8.0, 16.0, 16.0, 0.0) [ SNew(SVerticalBox) + SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .Font(FAppStyle::Get().GetFontStyle("NormalFontBold")) .Text(this, &SProjectLauncherProgress::GetSelectedProfileNameText) ] + SVerticalBox::Slot() .AutoHeight() [ SNew(STextBlock) .Text(this, &SProjectLauncherProgress::HandleProgressTextBlockText) ] + SVerticalBox::Slot() .AutoHeight() .Padding(0.0, 4.0, 0.0, 0.0) [ SAssignNew(ProgressBar, SProgressBar) .Percent(this, &SProjectLauncherProgress::HandleProgressBarPercent) ] ] + SVerticalBox::Slot() .Padding(0.0, 8.0, 0.0, 0.0) [ SNew(SSplitter) .Orientation(Orient_Vertical) + SSplitter::Slot() .Value(0.33f) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(0.0f) [ SAssignNew(TaskListView, SListView) .HeaderRow ( SNew(SHeaderRow) + SHeaderRow::Column("Icon") .DefaultLabel(LOCTEXT("TaskListIconColumnHeader", " ")) .FixedWidth(20.0) + SHeaderRow::Column("Task") .DefaultLabel(LOCTEXT("TaskListTaskColumnHeader", "Task")) .FillWidth(1.0) + SHeaderRow::Column("Warnings") .DefaultLabel(LOCTEXT("TaskListWarningsColumnHeader", "Warnings")) .FixedWidth(64.0) + SHeaderRow::Column("Errors") .DefaultLabel(LOCTEXT("TaskListErrorsColumnHeader", "Errors")) .FixedWidth(64.0) + SHeaderRow::Column("Duration") .DefaultLabel(LOCTEXT("TaskListDurationColumnHeader", "Duration")) .FixedWidth(64.0) + SHeaderRow::Column("Status") .DefaultLabel(LOCTEXT("TaskListStatusColumnHeader", "Status")) .FixedWidth(80.0) ) .ListItemsSource(&TaskList) .OnGenerateRow(this, &SProjectLauncherProgress::HandleTaskListViewGenerateRow) .SelectionMode(ESelectionMode::Single) ] ] //content area for the log + SSplitter::Slot() .Value(0.66f) [ SNew(SBorder) .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) .Padding(0.0f) [ SNew(SGridPanel) .FillColumn(0, 1.f) .FillRow(1, 1.f) + SGridPanel::Slot(0, 0) [ SNew(SHeaderRow) + SHeaderRow::Column("Status") .DefaultLabel(LOCTEXT("TaskListOutputLogColumnHeader", "Output Log")) .FillWidth(1.0) ] + SGridPanel::Slot(1, 0) [ SNew(SHeaderRow) ] + SGridPanel::Slot(0, 1) [ SAssignNew(HorizontalScrollBox, SScrollBox) .Orientation(EOrientation::Orient_Horizontal) .ExternalScrollbar(HorizontalScrollBar) + SScrollBox::Slot() [ SNew(SBox) .Padding(0.0f) .MinDesiredWidth_Lambda([this]() { // Cache the max desired width seen so far of rows in the message list. // We use this to prevent the horizontal scroll bar from constantly shrinking/expanding as rows are virtualized. // This will get reset whenever the message list is cleared. MaxMessageListRowWidth = FMath::Max(MaxMessageListRowWidth, (float)MessageListView->GetDesiredSize().X); // Make the message list at least as wide as the scroll box so that there's not an empty space to its right; // scroll boxes do not deal with alignment in the scroll direction (horizontal in this case). const float CurrentScrollWidth = (float)HorizontalScrollBox->GetTickSpaceGeometry().GetLocalSize().X; return FMath::Max(MaxMessageListRowWidth, CurrentScrollWidth); }) [ SAssignNew(MessageListView, SListView< TSharedPtr >) .HeaderRow ( SNew(SHeaderRow) .Visibility(EVisibility::Collapsed) + SHeaderRow::Column("Status") .DefaultLabel(LOCTEXT("TaskListOutputLogColumnHeader", "Output Log")) ) .ListItemsSource(&MessageList) .OnGenerateRow(this, &SProjectLauncherProgress::HandleMessageListViewGenerateRow) .SelectionMode(ESelectionMode::Multi) .ExternalScrollbar(VerticalScrollBar) .AllowOverscroll(EAllowOverscroll::No) .ConsumeMouseWheel(EConsumeMouseWheel::Always) ] ] ] + SGridPanel::Slot(1, 1) [ SNew(SBox) .WidthOverride(FOptionalSize(16)) [ VerticalScrollBar ] ] + SGridPanel::Slot(0, 2) [ SNew(SBox) .HeightOverride(FOptionalSize(16)) [ HorizontalScrollBar ] ] ] ] ] + SVerticalBox::Slot() .AutoHeight() .Padding(0.0f, 5.0f, 0.0f, 0.0f) [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .AutoWidth() [ // copy button SAssignNew(CopyButton, SButton) .ContentPadding(FMargin(6.0f, 2.0f)) .IsEnabled(false) .Text(LOCTEXT("CopyButtonText", "Copy")) .ToolTipText(LOCTEXT("CopyButtonTooltip", "Copy the selected log messages to the clipboard")) .OnClicked(this, &SProjectLauncherProgress::HandleCopyButtonClicked) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ // clear button SAssignNew(ClearButton, SButton) .ContentPadding(FMargin(6.0f, 2.0f)) .IsEnabled(false) .Text(LOCTEXT("ClearButtonText", "Clear Log")) .ToolTipText(LOCTEXT("ClearButtonTooltip", "Clear the log window")) .OnClicked(this, &SProjectLauncherProgress::HandleClearButtonClicked) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ // save button SAssignNew(SaveButton, SButton) .ContentPadding(FMargin(6.0f, 2.0f)) .IsEnabled(false) .Text(LOCTEXT("ExportButtonText", "Save Log...")) .ToolTipText(LOCTEXT("SaveButtonTooltip", "Save the entire log to a file")) .Visibility((FDesktopPlatformModule::Get() != NULL) ? EVisibility::Visible : EVisibility::Collapsed) .OnClicked(this, &SProjectLauncherProgress::HandleSaveButtonClicked) ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ // Re-Run button SNew(SButton) .ContentPadding(FMargin(6.0f, 2.0f)) .IsEnabled(this, &SProjectLauncherProgress::IsRerunButtonEnabled) .OnClicked(this, &SProjectLauncherProgress::HandleRerunButtonClicked) .ToolTipText(this, &SProjectLauncherProgress::GetRerunButtonToolTip) [ SNew(STextBlock) .Text(this, &SProjectLauncherProgress::GetRerunButtonText) ] ] + SHorizontalBox::Slot() .AutoWidth() .Padding(4.0f, 0.0f, 0.0f, 0.0f) [ // Cancel / Done button SNew(SButton) .ContentPadding(FMargin(6.0f, 2.0f)) .IsEnabled(this, &SProjectLauncherProgress::IsDoneButtonEnabled) .OnClicked(this, &SProjectLauncherProgress::HandleDoneButtonClicked) .ToolTipText(this, &SProjectLauncherProgress::GetDoneButtonToolTip) [ SNew(STextBlock) .Text(this, &SProjectLauncherProgress::GetDoneButtonText) ] ] ] ]; } /** * Sets the launcher worker to track the progress for. * * @param Worker The launcher worker. */ void SetLauncherWorker( const ILauncherWorkerRef& Worker ) { LauncherWorker = Worker; Worker->GetTasks(TaskList); TaskListView->RequestListRefresh(); MessageList.Reset(); MaxMessageListRowWidth = 0.0; Worker->OnOutputReceived().AddRaw(this, &SProjectLauncherProgress::HandleOutputReceived); MessageListView->RequestListRefresh(); } void Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime ) override { if (PendingMessages.Num() > 0) { FScopeLock ScopedLock(&CriticalSection); for (int32 Index = 0; Index < PendingMessages.Num(); ++Index) { MessageList.Add(PendingMessages[Index]); } PendingMessages.Reset(); MessageListView->RequestListRefresh(); // only scroll when at the end of the listview if (FMath::IsNearlyEqual(MessageListView->GetScrollDistanceRemaining().Y, 0.0f, 1.e-7f)) { MessageListView->RequestScrollIntoView(MessageList.Last()); } } SaveButton->SetEnabled(MessageList.Num() > 0); ClearButton->SetEnabled(MessageList.Num() > 0); CopyButton->SetEnabled(MessageListView->GetNumItemsSelected() > 0); } private: void HandleOutputReceived(const FString& InMessage) { FScopeLock ScopedLock(&CriticalSection); ELogVerbosity::Type Verbosity = ELogVerbosity::Log; if (InMessage.Contains(TEXT("Automation.ParseCommandLine:"), ESearchCase::CaseSensitive)) { Verbosity = ELogVerbosity::Display; } else if ( InMessage.Contains(TEXT("Error:"), ESearchCase::IgnoreCase) ) { Verbosity = ELogVerbosity::Error; } else if ( InMessage.Contains(TEXT("Warning:"), ESearchCase::IgnoreCase) ) { Verbosity = ELogVerbosity::Warning; } TSharedPtr Message = MakeShareable(new FProjectLauncherMessage(FText::FromString(InMessage), Verbosity)); PendingMessages.Add(Message); } // Callback for getting the filled percentage of the progress bar. TOptional HandleProgressBarPercent( ) const { if (TaskList.Num() > 0) { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid()) { int32 NumFinished = 0; for (int32 TaskIndex = 0; TaskIndex < TaskList.Num(); ++TaskIndex) { if (TaskList[TaskIndex]->IsFinished()) { ++NumFinished; } } return ((float)NumFinished / TaskList.Num()); } } return 0.0f; } // Callback for getting the text of the progress text box. FText HandleProgressTextBlockText( ) const { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid()) { if ((LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Busy) || (LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Canceling)) { return LOCTEXT("OperationInProgressText", "Operation in progress..."); } int32 NumCanceled = 0; int32 NumCompleted = 0; int32 NumFailed = 0; for (int32 TaskIndex = 0; TaskIndex < TaskList.Num(); ++TaskIndex) { ELauncherTaskStatus::Type TaskStatus = TaskList[TaskIndex]->GetStatus(); if (TaskStatus == ELauncherTaskStatus::Canceled) { ++NumCanceled; } else if (TaskStatus == ELauncherTaskStatus::Completed) { ++NumCompleted; } else if (TaskStatus == ELauncherTaskStatus::Failed) { ++NumFailed; } } FFormatNamedArguments Args; Args.Add(TEXT("NumCompleted"), NumCompleted); Args.Add(TEXT("NumFailed"), NumFailed); Args.Add(TEXT("NumCanceled"), NumCanceled); return FText::Format(LOCTEXT("TasksFinishedFormatText", "Operation finished. Completed: {NumCompleted}, Failed: {NumFailed}, Canceled: {NumCanceled}"), Args); } return FText::GetEmpty(); } // Callback for generating a row in the task list view. TSharedRef HandleTaskListViewGenerateRow( ILauncherTaskPtr InItem, const TSharedRef& OwnerTable ) const { return SNew(SProjectLauncherTaskListRow) .Task(InItem) .OwnerTableView(OwnerTable); } // Callback for generating a row in the task list view. TSharedRef HandleMessageListViewGenerateRow( TSharedPtr InItem, const TSharedRef& OwnerTable ) const { return SNew(SProjectLauncherMessageListRow, OwnerTable) .Message(InItem) .ToolTipText(InItem->Message); } FReply HandleClearButtonClicked( ) { ClearLog(); return FReply::Handled(); } FReply HandleCopyButtonClicked( ) { CopyLog(); return FReply::Handled(); } FReply HandleSaveButtonClicked( ) { SaveLog(); return FReply::Handled(); } bool IsRerunButtonEnabled() const { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid() && (LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Canceled || LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Completed)) { return true; } return false; } FReply HandleRerunButtonClicked() { if (OnRerunClicked.IsBound()) { return OnRerunClicked.Execute(); } return FReply::Handled(); } FText GetRerunButtonToolTip() const { return LOCTEXT("RerunButtonTooltip", "Run this launch profile."); } FText GetRerunButtonText() const { return LOCTEXT("RerunButtonLabel", "Run"); } bool IsDoneButtonEnabled() const { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid() && LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Canceling) { return false; } return true; } FReply HandleDoneButtonClicked() { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid() && LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Busy) { LauncherWorkerPtr->Cancel(); } else if (LauncherWorkerPtr.IsValid() && LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Canceling) { // do nothing } else if (OnCloseClicked.IsBound()) { return OnCloseClicked.Execute(); } return FReply::Handled(); } FText GetDoneButtonToolTip() const { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid()) { if (LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Busy) { return LOCTEXT("DoneButtonCancelTooltip", "Cancel the run of this profile."); } else if (LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Canceling) { return LOCTEXT("DoneButtonCancellingTooltip", "Currently canceling."); } } return LOCTEXT("DoneButtonCloseTooltip", "Close this page."); } FText GetDoneButtonText() const { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid()) { if (LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Busy) { return LOCTEXT("DoneButtonCancelLabel", "Cancel"); } else if (LauncherWorkerPtr->GetStatus() == ELauncherWorkerStatus::Canceling) { return LOCTEXT("DoneButtonCancellingLabel", "Cancelling"); } } return LOCTEXT("DoneButtonDoneLabel", "Done"); } void ClearLog() { MessageList.Reset(); MaxMessageListRowWidth = 0.0; MessageListView->RequestListRefresh(); } void CopyLog() { TArray > SelectedItems = MessageListView->GetSelectedItems(); if (SelectedItems.Num() > 0) { FString SelectedText; for( int32 Index = 0; Index < SelectedItems.Num(); ++Index ) { SelectedText += SelectedItems[Index]->Message.ToString(); SelectedText += LINE_TERMINATOR; } FPlatformApplicationMisc::ClipboardCopy( *SelectedText ); } } void SaveLog() { IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); if (DesktopPlatform != NULL) { TArray Filenames; TSharedPtr ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared()); void* ParentWindowHandle = (ParentWindow.IsValid() && ParentWindow->GetNativeWindow().IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr; if (DesktopPlatform->SaveFileDialog( ParentWindowHandle, LOCTEXT("SaveLogDialogTitle", "Save Log As...").ToString(), LastLogFileSaveDirectory, TEXT("ProjectLauncher.log"), TEXT("Log Files (*.log)|*.log"), EFileDialogFlags::None, Filenames)) { if (Filenames.Num() > 0) { FString Filename = Filenames[0]; // keep path as default for next time LastLogFileSaveDirectory = FPaths::GetPath(Filename); // add a file extension if none was provided if (FPaths::GetExtension(Filename).IsEmpty()) { Filename += Filename + TEXT(".log"); } // save file FArchive* LogFile = IFileManager::Get().CreateFileWriter(*Filename); if (LogFile != NULL) { for( int32 Index = 0; Index < MessageList.Num(); ++Index ) { FString LogEntry = MessageList[Index]->Message.ToString() + LINE_TERMINATOR; LogFile->Serialize(TCHAR_TO_ANSI(*LogEntry), LogEntry.Len()); } LogFile->Close(); delete LogFile; } else { FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("SaveLogDialogFileError", "Failed to open the specified file for saving!")); } } } } else { FMessageDialog::Open(EAppMsgType::Ok, LOCTEXT("SaveLogDialogUnsupportedError", "Saving is not supported on this platform!")); } } FText GetSelectedProfileNameText() const { ILauncherWorkerPtr LauncherWorkerPtr = LauncherWorker.Pin(); if (LauncherWorkerPtr.IsValid()) { const ILauncherProfilePtr& ProfilePtr = LauncherWorkerPtr->GetLauncherProfile(); if (ProfilePtr.IsValid()) { return FText::FromString(ProfilePtr->GetName()); } } return FText::GetEmpty(); } private: // Holds the launcher worker. TWeakPtr LauncherWorker; // Holds the output log. TArray> OutputList; // Holds the output list view. TSharedPtr>> OutputListView; // Holds the progress bar. TSharedPtr ProgressBar; // Holds the task list. TArray TaskList; // Holds the message list. TArray< TSharedPtr> MessageList; // Holds the filtered message list. TArray< TSharedPtr> FilterMessageList; // Holds the pending message list. TArray< TSharedPtr> PendingMessages; // Horizontal scroll box wrapping the message list view. TSharedPtr HorizontalScrollBox; // Holds the message list view. TSharedPtr>> MessageListView; // Caches the size of the longest row seen in the message list. Should be cleared when message list is cleared. float MaxMessageListRowWidth; // Holds the task list view. TSharedPtr> TaskListView; // Holds the box of task statuses. TSharedPtr TaskStatusBox; // Critical section for updating the messages FCriticalSection CriticalSection; // Holds the directory where the log file was last saved to. FString LastLogFileSaveDirectory; // Holds the copy log button. TSharedPtr CopyButton; // Holds the clear button. TSharedPtr ClearButton; // Holds the save button. TSharedPtr SaveButton; // Holds a delegate to be invoked when this panel is closed. FOnClicked OnCloseClicked; // Holds a delegate to be invoked when we want the launch profile rerun. FOnClicked OnRerunClicked; }; #undef LOCTEXT_NAMESPACE