EM_Task/UnrealEd/Private/AutoReimport/ReimportFeedbackContext.cpp
Boshuang Zhao 5144a49c9b add
2026-02-13 16:18:33 +08:00

539 lines
19 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AutoReimport/ReimportFeedbackContext.h"
#include "Animation/CurveSequence.h"
#include "Widgets/SBoxPanel.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Modules/ModuleManager.h"
#include "Layout/LayoutUtils.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Notifications/SProgressBar.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Input/SButton.h"
#include "EditorStyleSet.h"
#include "FileCacheUtilities.h"
#include "MessageLogModule.h"
#include "Widgets/Input/SHyperlink.h"
#define LOCTEXT_NAMESPACE "ReimportContext"
class SWidgetStack: public SVerticalBox
{
SLATE_BEGIN_ARGS(SWidgetStack) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, int32 InMaxNumVisible)
{
MaxNumVisible = InMaxNumVisible;
SlideCurve = FCurveSequence(0.f, .5f, ECurveEaseFunction::QuadOut);
SizeCurve = FCurveSequence(0.f, .5f, ECurveEaseFunction::QuadOut);
StartSlideOffset = 0;
StartSizeOffset = FVector2D(ForceInitToZero);
}
FVector2D ComputeTotalSize() const
{
FVector2D Size(ForceInitToZero);
for (int32 Index = 0; Index < FMath::Min(NumSlots(), MaxNumVisible); ++Index)
{
const FVector2D& ChildSize = Children[Index].GetWidget()->GetDesiredSize();
if (ChildSize.X > Size.X)
{
Size.X = ChildSize.X;
}
Size.Y += ChildSize.Y + Children[Index].SlotPadding.Get().GetTotalSpaceAlong<Orient_Vertical>();
}
return Size;
}
virtual FVector2D ComputeDesiredSize(float) const override
{
const float Lerp = SizeCurve.GetLerp();
return ComputeTotalSize() * Lerp + StartSizeOffset * (1.f - Lerp);
}
virtual void OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const override
{
if (Children.Num() == 0)
{
return;
}
const float Alpha = 1.f - SlideCurve.GetLerp();
float PositionSoFar = AllottedGeometry.GetLocalSize().Y + StartSlideOffset * Alpha;
for (int32 Index = 0; Index < NumSlots(); ++Index)
{
const SBoxPanel::FSlot& CurChild = Children[Index];
const EVisibility ChildVisibility = CurChild.GetWidget()->GetVisibility();
if (ChildVisibility != EVisibility::Collapsed)
{
const FVector2D ChildDesiredSize = CurChild.GetWidget()->GetDesiredSize();
const FMargin SlotPadding(CurChild.SlotPadding.Get());
const FVector2D SlotSize(AllottedGeometry.Size.X, ChildDesiredSize.Y + SlotPadding.GetTotalSpaceAlong<Orient_Vertical>());
const AlignmentArrangeResult XAlignmentResult = AlignChild<Orient_Horizontal>(SlotSize.X, CurChild, SlotPadding);
const AlignmentArrangeResult YAlignmentResult = AlignChild<Orient_Vertical>(SlotSize.Y, CurChild, SlotPadding);
ArrangedChildren.AddWidget(ChildVisibility, AllottedGeometry.MakeChild(
CurChild.GetWidget(),
FVector2D(XAlignmentResult.Offset, PositionSoFar - SlotSize.Y + YAlignmentResult.Offset),
FVector2D(XAlignmentResult.Size, YAlignmentResult.Size)));
PositionSoFar -= SlotSize.Y;
}
}
}
void Add(const TSharedRef<SWidget>& InWidget)
{
TSharedPtr<SWidgetStackItem> NewItem;
InsertSlot(0)
.AutoHeight()
[SAssignNew(NewItem, SWidgetStackItem)
[InWidget]];
{
auto Widget = Children[0].GetWidget();
Widget->SlatePrepass();
const float WidgetHeight = Widget->GetDesiredSize().Y;
StartSlideOffset += WidgetHeight;
// Fade in time is 1 second x the proportion of the slide amount that this widget takes up
NewItem->FadeIn(WidgetHeight / StartSlideOffset);
if (!SlideCurve.IsPlaying())
{
SlideCurve.Play(AsShared());
}
}
const FVector2D NewSize = ComputeTotalSize();
if (NewSize != StartSizeOffset)
{
StartSizeOffset = NewSize;
if (!SizeCurve.IsPlaying())
{
SizeCurve.Play(AsShared());
}
}
}
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override
{
if (!SlideCurve.IsPlaying())
{
StartSlideOffset = 0;
}
// Delete any widgets that are now offscreen
if (Children.Num() != 0)
{
const float Alpha = 1.f - SlideCurve.GetLerp();
float PositionSoFar = AllottedGeometry.GetLocalSize().Y + Alpha * StartSlideOffset;
for (int32 Index = 0; PositionSoFar > 0 && Index < NumSlots(); ++Index)
{
const SBoxPanel::FSlot& CurChild = Children[Index];
const EVisibility ChildVisibility = CurChild.GetWidget()->GetVisibility();
if (ChildVisibility != EVisibility::Collapsed)
{
const FVector2D ChildDesiredSize = CurChild.GetWidget()->GetDesiredSize();
PositionSoFar -= ChildDesiredSize.Y + CurChild.SlotPadding.Get().GetTotalSpaceAlong<Orient_Vertical>();
}
}
for (int32 Index = MaxNumVisible; Index < Children.Num();)
{
if (StaticCastSharedRef<SWidgetStackItem>(Children[Index].GetWidget())->bIsFinished)
{
Children.RemoveAt(Index);
}
else
{
++Index;
}
}
}
}
class SWidgetStackItem: public SCompoundWidget
{
SLATE_BEGIN_ARGS(SWidgetStackItem) {}
SLATE_DEFAULT_SLOT(FArguments, Content)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
bIsFinished = false;
ChildSlot
[SNew(SBorder)
.BorderImage(FEditorStyle::GetBrush("NoBorder"))
.ColorAndOpacity(this, &SWidgetStackItem::GetColorAndOpacity)
.Padding(0)
[InArgs._Content.Widget]];
}
void FadeIn(float Time)
{
OpacityCurve = FCurveSequence(0.f, Time, ECurveEaseFunction::QuadOut);
OpacityCurve.Play(AsShared());
}
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
if (!bIsFinished && OpacityCurve.IsAtStart() && OpacityCurve.IsInReverse())
{
bIsFinished = true;
}
}
FLinearColor GetColorAndOpacity() const
{
return FLinearColor(1.f, 1.f, 1.f, OpacityCurve.GetLerp());
}
bool bIsFinished;
FCurveSequence OpacityCurve;
};
FCurveSequence SlideCurve, SizeCurve;
float StartSlideOffset;
FVector2D StartSizeOffset;
int32 MaxNumVisible;
};
class SWidgetStack;
/** Feedback context that overrides GWarn for import operations to prevent popup spam */
class SReimportFeedback: public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SReimportFeedback): _ExpireDuration(3.f) {}
SLATE_ARGUMENT(TWeakPtr<FReimportFeedbackContext>, FeedbackContext)
SLATE_ARGUMENT(float, ExpireDuration)
SLATE_EVENT(FSimpleDelegate, OnExpired)
SLATE_EVENT(FSimpleDelegate, OnPauseClicked)
SLATE_EVENT(FSimpleDelegate, OnAbortClicked)
SLATE_END_ARGS()
virtual FVector2D ComputeDesiredSize(float LayoutScale) const override
{
auto Size = SCompoundWidget::ComputeDesiredSize(LayoutScale);
// The width is determined by the top row, plus some padding
Size.X = TopRow->GetDesiredSize().X + 100;
return Size;
}
/** Construct this widget */
void Construct(const FArguments& InArgs)
{
ExpireDuration = InArgs._ExpireDuration;
OnExpired = InArgs._OnExpired;
FeedbackContext = InArgs._FeedbackContext;
bPaused = false;
bExpired = false;
auto OpenMessageLog = []
{
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
MessageLogModule.OpenMessageLog("AssetReimport");
};
ChildSlot
[SNew(SBorder)
.Padding(FMargin(10))
.BorderImage(FCoreStyle::Get().GetBrush("NotificationList.ItemBackground"))
[SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[SAssignNew(TopRow, SHorizontalBox)
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
[SNew(STextBlock)
.Text(LOCTEXT("ProcessingChanges", "Processing source file changes..."))
.Font(FCoreStyle::Get().GetFontStyle(TEXT("NotificationList.FontLight")))]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(FMargin(4, 0, 4, 0))
[SAssignNew(PauseButton, SButton)
.ButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
.ToolTipText(LOCTEXT("PauseTooltip", "Temporarily pause processing of these source content files"))
.OnClicked(this, &SReimportFeedback::OnPauseClicked, InArgs._OnPauseClicked)
[SNew(SImage)
.ColorAndOpacity(FLinearColor(.8f, .8f, .8f, 1.f))
.Image(this, &SReimportFeedback::GetPlayPauseBrush)]]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[SAssignNew(AbortButton, SButton)
.ButtonStyle(FEditorStyle::Get(), "HoverHintOnly")
.ToolTipText(LOCTEXT("AbortTooltip", "Permanently abort processing of these source content files"))
.OnClicked(this, &SReimportFeedback::OnAbortClicked, InArgs._OnAbortClicked)
[SNew(SImage)
.ColorAndOpacity(FLinearColor(0.8f, 0.8f, 0.8f, 1.f))
.Image(FEditorStyle::GetBrush("GenericStop"))]]]
+ SVerticalBox::Slot()
.Padding(FMargin(0, 1))
.AutoHeight()
[SNew(SBox)
.HeightOverride(2)
[SAssignNew(ProgressBar, SProgressBar)
.BorderPadding(FVector2D::ZeroVector)
.Percent(this, &SReimportFeedback::GetProgressFraction)
.BackgroundImage(FEditorStyle::GetBrush("ProgressBar.ThinBackground"))
.FillImage(FEditorStyle::GetBrush("ProgressBar.ThinFill"))]]
+ SVerticalBox::Slot()
.Padding(FMargin(0, 5, 0, 0))
.AutoHeight()
[SAssignNew(WidgetStack, SWidgetStack, 3)]
+ SVerticalBox::Slot()
.Padding(FMargin(0, 5, 0, 0))
.AutoHeight()
.HAlign(HAlign_Right)
[SNew(SHyperlink)
.Visibility(this, &SReimportFeedback::GetHyperlinkVisibility)
.Text(LOCTEXT("OpenMessageLog", "Open message log"))
.TextStyle(FCoreStyle::Get(), "SmallText")
.OnNavigate_Lambda(OpenMessageLog)]]];
}
/** Add a widget to this feedback's widget stack */
void Add(const TSharedRef<SWidget>& Widget)
{
WidgetStack->Add(Widget);
}
/** Disable input to this widget's dynamic content (except the message log hyperlink) */
void Disable()
{
ExpireTimeout = DirectoryWatcher::FTimeLimit(ExpireDuration);
WidgetStack->SetVisibility(EVisibility::HitTestInvisible);
PauseButton->SetVisibility(EVisibility::Collapsed);
AbortButton->SetVisibility(EVisibility::Collapsed);
ProgressBar->SetVisibility(EVisibility::Collapsed);
}
/** Enable, if previously disabled */
void Enable()
{
ExpireTimeout = DirectoryWatcher::FTimeLimit();
bPaused = false;
WidgetStack->SetVisibility(EVisibility::Visible);
PauseButton->SetVisibility(EVisibility::Visible);
AbortButton->SetVisibility(EVisibility::Visible);
ProgressBar->SetVisibility(EVisibility::Visible);
}
private:
TOptional<float> GetProgressFraction() const
{
auto PinnedContext = FeedbackContext.Pin();
if (PinnedContext.IsValid() && PinnedContext->GetScopeStack().Num() >= 0)
{
return PinnedContext->GetScopeStack().GetProgressFraction(0);
}
return 1.f;
}
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override
{
if (bExpired)
{
return;
}
else if (ExpireTimeout.IsValid())
{
if (ExpireTimeout.Exceeded())
{
OnExpired.ExecuteIfBound();
bExpired = true;
}
}
}
/** Get the play/pause image */
const FSlateBrush* GetPlayPauseBrush() const
{
return bPaused ? FEditorStyle::GetBrush("GenericPlay") : FEditorStyle::GetBrush("GenericPause");
}
/** Called when pause is clicked */
FReply OnPauseClicked(FSimpleDelegate UserOnClicked)
{
bPaused = !bPaused;
UserOnClicked.ExecuteIfBound();
return FReply::Handled();
}
/** Called when abort is clicked */
FReply OnAbortClicked(FSimpleDelegate UserOnClicked)
{
// Destroy();
UserOnClicked.ExecuteIfBound();
return FReply::Handled();
}
/** Get the visibility of the hyperlink to open the message log */
EVisibility GetHyperlinkVisibility() const
{
return WidgetStack->NumSlots() != 0 ? EVisibility::Visible : EVisibility::Collapsed;
}
private:
/** The expire timeout used to fire OnExpired. Invalid when no timeout is set. */
DirectoryWatcher::FTimeLimit ExpireTimeout;
/** Amount of time to wait after this widget has been disabled before calling OnExpired */
float ExpireDuration;
/** Event that is called when this widget has been inactive and open for too long, and will fade out */
FSimpleDelegate OnExpired;
/** Whether we are paused and/or expired */
bool bPaused, bExpired;
/** The widget stack, displaying contextural information about the current state of the process */
TSharedPtr<SWidgetStack> WidgetStack;
TSharedPtr<SWidget> PauseButton, AbortButton, ProgressBar, TopRow;
TWeakPtr<FReimportFeedbackContext> FeedbackContext;
};
FReimportFeedbackContext::FReimportFeedbackContext(const FSimpleDelegate& InOnPauseClicked, const FSimpleDelegate& InOnAbortClicked)
: bSuppressSlowTaskMessages(false), OnPauseClickedEvent(InOnPauseClicked), OnAbortClickedEvent(InOnAbortClicked), MessageLog("AssetReimport")
{
}
void FReimportFeedbackContext::Show(int32 TotalWork)
{
// Important - we first destroy the old main task, then create a new one to ensure that they are (removed from/added to) the scope stack in the correct order
MainTask.Reset();
MainTask.Reset(new FScopedSlowTask(TotalWork, FText(), true, *this));
if (NotificationContent.IsValid())
{
NotificationContent->Enable();
}
else
{
NotificationContent = SNew(SReimportFeedback)
.FeedbackContext(AsShared())
.OnExpired(this, &FReimportFeedbackContext::OnNotificationExpired)
.OnPauseClicked(OnPauseClickedEvent)
.OnAbortClicked(OnAbortClickedEvent);
FNotificationInfo Info(AsShared());
Info.bFireAndForget = false;
Notification = FSlateNotificationManager::Get().AddNotification(Info);
MessageLog.NewPage(FText::Format(LOCTEXT("MessageLogPageLabel", "Outstanding source content changes {0}"), FText::AsTime(FDateTime::Now())));
}
}
void FReimportFeedbackContext::Hide()
{
MainTask.Reset();
if (Notification.IsValid())
{
NotificationContent->Disable();
Notification->SetCompletionState(SNotificationItem::CS_Success);
}
}
void FReimportFeedbackContext::OnNotificationExpired()
{
if (Notification.IsValid())
{
MessageLog.Notify(FText(), EMessageSeverity::Error);
Notification->Fadeout();
NotificationContent = nullptr;
Notification = nullptr;
}
}
void FReimportFeedbackContext::AddMessage(EMessageSeverity::Type Severity, const FText& Message)
{
MessageLog.Message(Severity, Message);
AddWidget(SNew(STextBlock).Text(Message));
}
void FReimportFeedbackContext::AddWidget(const TSharedRef<SWidget>& Widget)
{
if (NotificationContent.IsValid())
{
NotificationContent->Add(Widget);
}
}
TSharedRef<SWidget> FReimportFeedbackContext::AsWidget()
{
return NotificationContent.ToSharedRef();
}
void FReimportFeedbackContext::StartSlowTask(const FText& Task, bool bShowCancelButton)
{
FFeedbackContext::StartSlowTask(Task, bShowCancelButton);
if (NotificationContent.IsValid() && !bSuppressSlowTaskMessages && !Task.IsEmpty())
{
if (SlowTaskText.IsValid())
{
SlowTaskText->SetText(FText::Format(LOCTEXT("SlowTaskPattern_Default", "{0} (0%)"), Task));
}
else
{
NotificationContent->Add(SAssignNew(SlowTaskText, STextBlock).Text(Task));
}
}
}
void FReimportFeedbackContext::ProgressReported(const float TotalProgressInterp, FText DisplayMessage)
{
if (SlowTaskText.IsValid())
{
SlowTaskText->SetText(FText::Format(LOCTEXT("SlowTaskPattern", "{0} ({1}%)"), DisplayMessage, FText::AsNumber(int(TotalProgressInterp * 100))));
}
}
void FReimportFeedbackContext::FinalizeSlowTask()
{
if (SlowTaskText.IsValid())
{
SlowTaskText->SetVisibility(EVisibility::Collapsed);
SlowTaskText = nullptr;
}
FFeedbackContext::FinalizeSlowTask();
}
#undef LOCTEXT_NAMESPACE