Files
UnrealEngine/Engine/Source/Developer/LegacyProjectLauncher/Private/Widgets/Progress/SProjectLauncherProgress.h
2025-05-18 13:04:45 +08:00

768 lines
20 KiB
C++

// 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<SScrollBar> HorizontalScrollBar = SNew(SScrollBar)
.Orientation(EOrientation::Orient_Horizontal)
.AlwaysShowScrollbar(true);
TSharedRef<SScrollBar> 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<ILauncherTaskPtr>)
.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<FProjectLauncherMessage> >)
.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<FProjectLauncherMessage> Message = MakeShareable(new FProjectLauncherMessage(FText::FromString(InMessage), Verbosity));
PendingMessages.Add(Message);
}
// Callback for getting the filled percentage of the progress bar.
TOptional<float> 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<ITableRow> HandleTaskListViewGenerateRow( ILauncherTaskPtr InItem, const TSharedRef<STableViewBase>& OwnerTable ) const
{
return SNew(SProjectLauncherTaskListRow)
.Task(InItem)
.OwnerTableView(OwnerTable);
}
// Callback for generating a row in the task list view.
TSharedRef<ITableRow> HandleMessageListViewGenerateRow( TSharedPtr<FProjectLauncherMessage> InItem, const TSharedRef<STableViewBase>& 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<TSharedPtr<FProjectLauncherMessage > > 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<FString> Filenames;
TSharedPtr<SWindow> 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<ILauncherWorker> LauncherWorker;
// Holds the output log.
TArray<TSharedPtr<FString>> OutputList;
// Holds the output list view.
TSharedPtr<SListView<TSharedPtr<FString>>> OutputListView;
// Holds the progress bar.
TSharedPtr<SProgressBar> ProgressBar;
// Holds the task list.
TArray<ILauncherTaskPtr> TaskList;
// Holds the message list.
TArray< TSharedPtr<FProjectLauncherMessage>> MessageList;
// Holds the filtered message list.
TArray< TSharedPtr<FProjectLauncherMessage>> FilterMessageList;
// Holds the pending message list.
TArray< TSharedPtr<FProjectLauncherMessage>> PendingMessages;
// Horizontal scroll box wrapping the message list view.
TSharedPtr<SScrollBox> HorizontalScrollBox;
// Holds the message list view.
TSharedPtr<SListView<TSharedPtr<FProjectLauncherMessage>>> 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<SListView<ILauncherTaskPtr>> TaskListView;
// Holds the box of task statuses.
TSharedPtr<SVerticalBox> 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<SButton> CopyButton;
// Holds the clear button.
TSharedPtr<SButton> ClearButton;
// Holds the save button.
TSharedPtr<SButton> 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