// 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(); } 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()); const AlignmentArrangeResult XAlignmentResult = AlignChild(SlotSize.X, CurChild, SlotPadding); const AlignmentArrangeResult YAlignmentResult = AlignChild(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& InWidget) { TSharedPtr 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(); } } for (int32 Index = MaxNumVisible; Index < Children.Num();) { if (StaticCastSharedRef(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, 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("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& 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 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 WidgetStack; TSharedPtr PauseButton, AbortButton, ProgressBar, TopRow; TWeakPtr 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& Widget) { if (NotificationContent.IsValid()) { NotificationContent->Add(Widget); } } TSharedRef 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