// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "IntroTutorialsPrivatePCH.h" #include "STutorialContent.h" #include "STutorialEditableText.h" #include "IntroTutorials.h" #define LOCTEXT_NAMESPACE "STutorialContent" namespace TutorialConstants { const float BorderPulseAnimationLength = 1.0f; const float BorderIntroAnimationLength = 0.5f; const float ContentIntroAnimationLength = 0.5f; const float MinBorderOpacity = 0.1f; const float ShadowScale = 8.0f; const float MaxBorderOffset = 8.0f; const float BorderSize = 24.0f; } const float ContentOffset = 10.0f; void STutorialContent::Construct(const FArguments& InArgs, const FTutorialContent& InContent) { bIsVisible = Anchor.Type == ETutorialAnchorIdentifier::None; VerticalAlignment = InArgs._VAlign; HorizontalAlignment = InArgs._HAlign; WidgetOffset = InArgs._Offset; bIsStandalone = InArgs._IsStandalone; OnClosed = InArgs._OnClosed; Anchor = InArgs._Anchor; BorderIntroAnimation.AddCurve(0.0f, TutorialConstants::BorderIntroAnimationLength, ECurveEaseFunction::CubicOut); BorderPulseAnimation.AddCurve(0.0f, TutorialConstants::BorderPulseAnimationLength, ECurveEaseFunction::Linear); BorderIntroAnimation.Play(); BorderPulseAnimation.Play(); ContentIntroAnimation.AddCurve(0.0f, TutorialConstants::ContentIntroAnimationLength, ECurveEaseFunction::Linear); ContentIntroAnimation.Play(); if (InContent.Text.IsEmpty() == true) { ChildSlot [ SAssignNew(ContentWidget, SBorder) .Visibility(EVisibility::SelfHitTestInvisible) ]; return; } ChildSlot [ SNew(SFxWidget) .RenderScale(this, &STutorialContent::GetAnimatedZoom) .RenderScaleOrigin(FVector2D(0.5f, 0.5f)) [ SAssignNew(ContentWidget, SBorder) // Add more padding if the content is to be displayed centrally (i.e. not on a widget) .Padding(TutorialConstants::BorderSize) .BorderImage(FEditorStyle::GetBrush("Tutorials.Border")) .Visibility(this, &STutorialContent::GetVisibility) .BorderBackgroundColor(this, &STutorialContent::GetBackgroundColor) .ForegroundColor(FCoreStyle::Get().GetSlateColor("InvertedForeground")) [ SNew(SFxWidget) .RenderScale(this, &STutorialContent::GetInverseAnimatedZoom) .RenderScaleOrigin(FVector2D(0.5f, 0.5f)) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .MaxWidth(600.0f) .VAlign(VAlign_Center) [ GenerateContentWidget(InContent, InArgs._WrapTextAt, DocumentationPage) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Top) .Padding(5.0f, 0.0f, 0.0f, 0.0f) [ SNew(SButton) .OnClicked(this, &STutorialContent::OnCloseButtonClicked) .Visibility(this, &STutorialContent::GetCloseButtonVisibility) .ButtonStyle(&FEditorStyle::Get().GetWidgetStyle("Tutorials.Content.Button")) .ContentPadding(0.0f) [ SNew(SImage) .Image(FEditorStyle::GetBrush("Symbols.X")) .ColorAndOpacity(FLinearColor(0.0f, 0.0f, 0.0f, 1.0f)) ] ] ] ] ] ]; } static void GetAnimationValues(bool bIsIntro, float InAnimationProgress, float& OutAlphaFactor, float& OutPulseFactor, FLinearColor& OutShadowTint, FLinearColor& OutBorderTint) { if(bIsIntro) { OutAlphaFactor = InAnimationProgress; OutPulseFactor = (1.0f - OutAlphaFactor) * 50.0f; OutShadowTint = FLinearColor(1.0f, 1.0f, 0.0f, OutAlphaFactor); OutBorderTint = FLinearColor(1.0f, 1.0f, 0.0f, OutAlphaFactor * OutAlphaFactor); } else { OutAlphaFactor = 1.0f - (0.5f + (FMath::Cos(2.0f * PI * InAnimationProgress) * 0.5f)); OutPulseFactor = 0.5f + (FMath::Cos(2.0f * PI * InAnimationProgress) * 0.5f); OutShadowTint = FLinearColor(1.0f, 1.0f, 0.0f, 1.0f); OutBorderTint = FLinearColor(1.0f, 1.0f, 0.0f, TutorialConstants::MinBorderOpacity + ((1.0f - TutorialConstants::MinBorderOpacity) * OutAlphaFactor)); } } int32 STutorialContent::OnPaint( const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyClippingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled ) const { CachedContentGeometry = AllottedGeometry; CachedContentGeometry.AppendTransform(FSlateLayoutTransform(OutDrawElements.GetWindow()->GetPositionInScreen())); if(bIsVisible && Anchor.Type != ETutorialAnchorIdentifier::None && Anchor.bDrawHighlight) { float AlphaFactor; float PulseFactor; FLinearColor ShadowTint; FLinearColor BorderTint; GetAnimationValues(BorderIntroAnimation.IsPlaying(), BorderIntroAnimation.IsPlaying() ? BorderIntroAnimation.GetLerp() : BorderPulseAnimation.GetLerpLooping(), AlphaFactor, PulseFactor, ShadowTint, BorderTint); const FSlateBrush* ShadowBrush = FCoreStyle::Get().GetBrush(TEXT("Tutorials.Shadow")); const FSlateBrush* BorderBrush = FCoreStyle::Get().GetBrush(TEXT("Tutorials.Border")); const FGeometry& WidgetGeometry = CachedGeometry; const FVector2D WindowSize = OutDrawElements.GetWindow()->GetSizeInScreen(); // We should be clipped by the window size, not our containing widget, as we want to draw outside the widget FSlateRect WindowClippingRect(0.0f, 0.0f, WindowSize.X, WindowSize.Y); FPaintGeometry ShadowGeometry((WidgetGeometry.AbsolutePosition - FVector2D(ShadowBrush->Margin.Left, ShadowBrush->Margin.Top) * ShadowBrush->ImageSize * WidgetGeometry.Scale * TutorialConstants::ShadowScale), ((WidgetGeometry.Size * WidgetGeometry.Scale) + (FVector2D(ShadowBrush->Margin.Right * 2.0f, ShadowBrush->Margin.Bottom * 2.0f) * ShadowBrush->ImageSize * WidgetGeometry.Scale * TutorialConstants::ShadowScale)), WidgetGeometry.Scale * TutorialConstants::ShadowScale); // draw highlight shadow FSlateDrawElement::MakeBox(OutDrawElements, LayerId++, ShadowGeometry, ShadowBrush, WindowClippingRect, ESlateDrawEffect::None, ShadowTint); FVector2D PulseOffset = FVector2D(PulseFactor * TutorialConstants::MaxBorderOffset, PulseFactor * TutorialConstants::MaxBorderOffset); FVector2D BorderPosition = (WidgetGeometry.AbsolutePosition - ((FVector2D(BorderBrush->Margin.Left, BorderBrush->Margin.Top) * BorderBrush->ImageSize * WidgetGeometry.Scale) + PulseOffset)); FVector2D BorderSize = ((WidgetGeometry.Size * WidgetGeometry.Scale) + (PulseOffset * 2.0f) + (FVector2D(BorderBrush->Margin.Right * 2.0f, BorderBrush->Margin.Bottom * 2.0f) * BorderBrush->ImageSize * WidgetGeometry.Scale)); FPaintGeometry BorderGeometry(BorderPosition, BorderSize, WidgetGeometry.Scale); // draw highlight border FSlateDrawElement::MakeBox(OutDrawElements, LayerId++, BorderGeometry, BorderBrush, WindowClippingRect, ESlateDrawEffect::None, BorderTint); } return SCompoundWidget::OnPaint(Args, AllottedGeometry, MyClippingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); } /** Helper function to generate title widget, if any */ static TSharedRef GetStageTitle(const FExcerpt& InExcerpt, int32 InCurrentExcerptIndex) { // First try for unadorned 'StageTitle' FString VariableName(TEXT("StageTitle")); const FString* VariableValue = InExcerpt.Variables.Find(VariableName); if(VariableValue != NULL) { return SNew(STextBlock) .Text(FText::FromString(*VariableValue)) .TextStyle(FEditorStyle::Get(), "Tutorials.CurrentExcerpt"); } // Then try 'StageTitle' VariableName = FString::Printf(TEXT("StageTitle%d"), InCurrentExcerptIndex + 1); VariableValue = InExcerpt.Variables.Find(VariableName); if(VariableValue != NULL) { return SNew(STextBlock) .Text(FText::FromString(*VariableValue)) .TextStyle(FEditorStyle::Get(), "Tutorials.CurrentExcerpt"); } return SNullWidget::NullWidget; } TSharedRef STutorialContent::GenerateContentWidget(const FTutorialContent& InContent, float WrapTextAt, TSharedPtr& OutDocumentationPage) { // Style for the documentation static FDocumentationStyle DocumentationStyle; DocumentationStyle .ContentStyle(TEXT("Tutorials.Content.Text")) .BoldContentStyle(TEXT("Tutorials.Content.TextBold")) .NumberedContentStyle(TEXT("Tutorials.Content.Text")) .Header1Style(TEXT("Tutorials.Content.HeaderText1")) .Header2Style(TEXT("Tutorials.Content.HeaderText2")) .HyperlinkStyle(TEXT("Tutorials.Content.Hyperlink")) .HyperlinkTextStyle(TEXT("Tutorials.Content.HyperlinkText")) .SeparatorStyle(TEXT("Tutorials.Separator")); OutDocumentationPage = nullptr; switch(InContent.Type) { case ETutorialContent::Text: { return SNew(STextBlock) .Visibility(EVisibility::SelfHitTestInvisible) .WrapTextAt(WrapTextAt) .Text(InContent.Text) .TextStyle(FEditorStyle::Get(), "Tutorials.Content"); } case ETutorialContent::UDNExcerpt: if (IDocumentation::Get()->PageExists(InContent.Content)) { OutDocumentationPage = IDocumentation::Get()->GetPage(InContent.Content, TSharedPtr(), DocumentationStyle); FExcerpt Excerpt; if(OutDocumentationPage->GetExcerpt(InContent.ExcerptName, Excerpt) && OutDocumentationPage->GetExcerptContent(Excerpt)) { return SNew(SVerticalBox) +SVerticalBox::Slot() .HAlign(HAlign_Left) .AutoHeight() .Padding(0.0f, 0.0f, 0.0f, 5.0f) [ GetStageTitle(Excerpt, 0) ] +SVerticalBox::Slot() .HAlign(HAlign_Fill) .AutoHeight() [ Excerpt.Content.ToSharedRef() ]; } } break; case ETutorialContent::RichText: { TArray< TSharedRef< class ITextDecorator > > Decorators; Decorators.Add(FHyperlinkDecorator::Create(TEXT("browser"), FSlateHyperlinkRun::FOnClick::CreateStatic(&TutorialTextHelpers::OnBrowserLinkClicked))); Decorators.Add(FHyperlinkDecorator::Create(TEXT("udn"), FSlateHyperlinkRun::FOnClick::CreateStatic(&TutorialTextHelpers::OnDocLinkClicked))); Decorators.Add(FHyperlinkDecorator::Create(TEXT("tutorial"), FSlateHyperlinkRun::FOnClick::CreateStatic(&TutorialTextHelpers::OnTutorialLinkClicked))); Decorators.Add(FHyperlinkDecorator::Create(TEXT("code"), FSlateHyperlinkRun::FOnClick::CreateStatic(&TutorialTextHelpers::OnCodeLinkClicked))); Decorators.Add(FHyperlinkDecorator::Create(TEXT("asset"), FSlateHyperlinkRun::FOnClick::CreateStatic(&TutorialTextHelpers::OnAssetLinkClicked))); Decorators.Add(FTextStyleDecorator::Create()); return SNew(SRichTextBlock) .Visibility(EVisibility::SelfHitTestInvisible) .TextStyle(FEditorStyle::Get(), "Tutorials.Content.Text") .DecoratorStyleSet(&FEditorStyle::Get()) .Decorators(Decorators) .Text(InContent.Text) .WrapTextAt(WrapTextAt) .Margin(4) .LineHeightPercentage(1.1f); } break; } return SNullWidget::NullWidget; } FVector2D STutorialContent::GetPosition() const { float XOffset = 0.0f; switch(HorizontalAlignment.Get()) { case HAlign_Left: XOffset = -(ContentWidget->GetDesiredSize().X - ContentOffset); break; default: case HAlign_Fill: case HAlign_Center: XOffset = (CachedGeometry.Size.X * 0.5f) - (ContentWidget->GetDesiredSize().X * 0.5f); break; case HAlign_Right: XOffset = CachedGeometry.Size.X - ContentOffset; break; } XOffset += WidgetOffset.Get().X; float YOffset = 0.0f; switch(VerticalAlignment.Get()) { case VAlign_Top: YOffset = -(ContentWidget->GetDesiredSize().Y - ContentOffset); break; default: case VAlign_Fill: case VAlign_Center: YOffset = (CachedGeometry.Size.Y * 0.5f) - (ContentWidget->GetDesiredSize().Y * 0.5f); break; case VAlign_Bottom: YOffset = (CachedGeometry.Size.Y - ContentOffset); break; } YOffset += WidgetOffset.Get().Y; // now build & clamp to area FVector2D BaseOffset = FVector2D(CachedGeometry.AbsolutePosition.X + XOffset, CachedGeometry.AbsolutePosition.Y + YOffset); BaseOffset.X = FMath::Clamp(BaseOffset.X, 0.0f, CachedWindowSize.X - ContentWidget->GetDesiredSize().X); BaseOffset.Y = FMath::Clamp(BaseOffset.Y, 0.0f, CachedWindowSize.Y - ContentWidget->GetDesiredSize().Y); return BaseOffset; } FVector2D STutorialContent::GetSize() const { return ContentWidget->GetDesiredSize(); } FReply STutorialContent::OnCloseButtonClicked() { OnClosed.ExecuteIfBound(); return FReply::Handled(); } EVisibility STutorialContent::GetCloseButtonVisibility() const { return bIsStandalone ? EVisibility::Visible : EVisibility::Collapsed; } void STutorialContent::HandlePaintNamedWidget(TSharedRef InWidget, const FGeometry& InGeometry) { switch(Anchor.Type) { case ETutorialAnchorIdentifier::NamedWidget: { TSharedPtr MetaData = InWidget->GetMetaData(); if( Anchor.WrapperIdentifier == InWidget->GetTag() || (MetaData.IsValid() && MetaData->Tag == Anchor.WrapperIdentifier)) { bIsVisible = true; CachedGeometry = InGeometry; } } break; } } void STutorialContent::HandleResetNamedWidget() { bIsVisible = false; } void STutorialContent::HandleCacheWindowSize(const FVector2D& InWindowSize) { CachedWindowSize = InWindowSize; } EVisibility STutorialContent::GetVisibility() const { return bIsVisible || Anchor.Type == ETutorialAnchorIdentifier::None ? EVisibility::SelfHitTestInvisible : EVisibility::Collapsed; } FSlateColor STutorialContent::GetBackgroundColor() const { // note cant use IsHovered() here because our widget is SelfHitTestInvisible const FVector2D CursorPos = FSlateApplication::Get().GetCursorPos(); return CachedContentGeometry.IsUnderLocation(CursorPos) ? FLinearColor(1.0f, 1.0f, 1.0f, 1.0f) : FLinearColor(1.0f, 1.0f, 1.0f, 0.8f); } float STutorialContent::GetAnimatedZoom() const { if(ContentIntroAnimation.IsPlaying()) { FIntroTutorials& IntroTutorials = FModuleManager::GetModuleChecked(TEXT("IntroTutorials")); return 0.75f + (0.25f * IntroTutorials.GetIntroCurveValue(ContentIntroAnimation.GetLerp())); } else { return 1.0f; } } float STutorialContent::GetInverseAnimatedZoom() const { return 1.0f / GetAnimatedZoom(); } #undef LOCTEXT_NAMESPACE