// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. #include "IntroTutorialsPrivatePCH.h" #include "SIntroTutorials.h" #include "IDocumentation.h" #include "SoundDefinitions.h" #define LOCTEXT_NAMESPACE "IntroTutorials" const FString SIntroTutorials::HomePath = TEXT("Shared/Tutorials"); /** * A widget used to control the display of the dynamic titles in a tutorial. */ class STutorialStageTicker : public SCompoundWidget { public: SLATE_BEGIN_ARGS( STutorialStageTicker ) {} /** The current excerpt to display */ SLATE_ATTRIBUTE( FText, CurrentExcerpt ) /** The progress string for the current excerpt (e.g. 4/16) */ SLATE_ATTRIBUTE( FText, CurrentProgressText ) SLATE_END_ARGS() void Construct( const FArguments& InArgs ) { ChildSlot [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() [ SNew(SImage) .Image(FEditorStyle::GetBrush("Tutorials.CurrentExcerpt")) ] +SHorizontalBox::Slot() .FillWidth(1.0f) .AutoWidth() [ SNew(STextBlock) .Text(InArgs._CurrentExcerpt) .TextStyle(FEditorStyle::Get(), "Tutorials.CurrentExcerpt") ] ]; } }; /** * A widget used for consistency of appearance of tutorial navigation buttons */ class STutorialNavigationButton : public SButton { public: SLATE_BEGIN_ARGS( STutorialNavigationButton ) {} /** The image to display */ SLATE_ARGUMENT( FName, ImageName ) /** The text to display */ SLATE_ATTRIBUTE( FText, Text ) /** Called when the button is clicked */ SLATE_EVENT( FOnClicked, OnClicked ) /** Whether the image should be displayed on the left or right */ SLATE_ARGUMENT( bool, ImageOnLeft ) /** Whether the image should be displayed on the left or right */ SLATE_ARGUMENT(bool, ImageOnly) SLATE_END_ARGS() void Construct( const FArguments& InArgs ) { SButton::Construct(SButton::FArguments() .ButtonStyle( FEditorStyle::Get(), "HoverHintOnly" ) .HAlign(HAlign_Center) .VAlign(VAlign_Center) .ToolTipText(InArgs._ToolTipText) .OnClicked(InArgs._OnClicked) [ MakeContent(InArgs._ImageOnLeft, InArgs._ImageOnly, InArgs._Text, InArgs._ImageName) ]); } private: /** Make the button content */ TSharedRef MakeContent(bool bImageOnLeft, bool bImageOnly, const TAttribute& InText, const FName& InImageName) { if (bImageOnly) { return MakeImage(InImageName); } else if(bImageOnLeft) { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) .Padding(FMargin(4.f, 0.f)) [ MakeImage(InImageName) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) [ MakeText(InText) ]; } else { return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .Padding( FMargin(4.f,0.f) ) .VAlign(VAlign_Center) [ MakeText(InText) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .HAlign(HAlign_Center) [ MakeImage(InImageName) ]; } } /** Make the image content */ TSharedRef MakeImage( const FName& InImageName ) { return SNew(SImage) .Image(FEditorStyle::GetBrush(InImageName)) .ColorAndOpacity(this, &STutorialNavigationButton::GetColor); } /** Make the text content */ TSharedRef MakeText(const TAttribute& InText) { return SNew(STextBlock) .Text(InText) .TextStyle(FEditorStyle::Get(), "Tutorials.NavigationButtons") .ColorAndOpacity(this, &STutorialNavigationButton::GetColor); } /** Get the color according to state */ FSlateColor GetColor() const { if(IsEnabled()) { return IsHovered() ? FEditorStyle::GetSlateColor("Tutorials.ButtonHighlightColor") : FEditorStyle::GetColor("Tutorials.ButtonColor"); } // The default disabled effect tries to de-saturate the color, but the color // is already fairly desaturated, so we need to supply a different color when the // button is disabled for it to show up. return FEditorStyle::GetSlateColor("Tutorials.ButtonDisabledColor"); } }; void SIntroTutorials::Construct( const FArguments& Args ) { ParentWindowPtr = Args._ParentWindow; HomeButtonVisibility = Args._HomeButtonVisibility; OnGotoNextTutorial = Args._OnGotoNextTutorial; DialogueAudioComponent = NULL; bCurrentSectionIsInteractive = false; CurrentExcerptIndex = 0; CurrentPageStartTime = 0.0; ParserConfiguration = FParserConfiguration::Create(); ParserConfiguration->OnNavigate = FOnNavigate::CreateRaw(this, &SIntroTutorials::ChangePage); ChildSlot [ SNew(SBorder) .Padding(5) .BorderImage(this, &SIntroTutorials::GetContentAreaBackground) [ SNew(SVerticalBox) +SVerticalBox::Slot() [ SNew(SVerticalBox) .Visibility(this, &SIntroTutorials::GetContentVisibility) +SVerticalBox::Slot() .FillHeight(1.0f) [ SNew(SVerticalBox) +SVerticalBox::Slot() .AutoHeight() [ SNew(STutorialStageTicker) .CurrentExcerpt(this, &SIntroTutorials::GetCurrentExcerptTitle) .CurrentProgressText(this, &SIntroTutorials::GetProgressText) ] +SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.0f, 8.0f)) [ SNew(SProgressBar) .Style( FEditorStyle::Get(), "Tutorials.ProgressBar") .Percent(this, &SIntroTutorials::GetProgress) .FillColorAndOpacity( FLinearColor(0.19f, 0.33f, 0.72f) ) ] +SVerticalBox::Slot() .FillHeight(1.0f) [ SAssignNew(ContentArea, SBorder) .Padding(6.f) .BorderImage(FEditorStyle::GetBrush("NoBorder")) ] +SVerticalBox::Slot() .AutoHeight() .Padding(FMargin(0.f, 6.f, 0.f, 0.f)) [ SNew(SSeparator) .SeparatorImage(FEditorStyle::GetBrush("Tutorials.Separator")) ] +SVerticalBox::Slot() .Padding(FMargin(0.0f, 8.0f, 0.0f, 0.0f)) .AutoHeight() [ SNew(SHorizontalBox) .Visibility(this, &SIntroTutorials::GetNavigationVisibility) +SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(STutorialNavigationButton) .ToolTipText(LOCTEXT("PreviousButtonTooltip", "Go back to the previous tutorial page.")) .OnClicked(this, &SIntroTutorials::OnPreviousClicked) .IsEnabled(this, &SIntroTutorials::OnPreviousIsEnabled) .ImageName("Tutorials.Back") .Text(this, &SIntroTutorials::GetBackButtonText) .ImageOnLeft(true) .ImageOnly(false) .Visibility(this, &SIntroTutorials::GetBackButtonVisibility) ] +SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(STutorialNavigationButton) .ToolTipText(LOCTEXT("HomeButtonTooltip", "Go back to the tutorial index.")) .OnClicked(this, &SIntroTutorials::OnHomeClicked) .ImageName("Tutorials.Home") .ImageOnLeft(true) .ImageOnly(true) .Visibility(HomeButtonVisibility) ] +SHorizontalBox::Slot() .FillWidth(1.0f) [ SNew(STutorialNavigationButton) .ToolTipText(LOCTEXT("NextButtonTooltip", "Go to the next tutorial page.")) .OnClicked(this, &SIntroTutorials::OnNextClicked) .IsEnabled(this, &SIntroTutorials::OnNextIsEnabled) .ImageName("Tutorials.Next") .Text(this, &SIntroTutorials::GetNextButtonText) .ImageOnLeft(false) .ImageOnly(false) .Visibility(this, &SIntroTutorials::GetNextButtonVisibility) ] ] ] ] +SVerticalBox::Slot() [ SAssignNew(HomeContentArea, SBorder) .Visibility(this, &SIntroTutorials::GetHomeContentVisibility) .Padding(FMargin(0,0,0,5)) .BorderImage(FEditorStyle::GetBrush("NoBorder")) ] ] ]; // Set the documentation style to what we want to use in the tutorials */ DocumentationStyle .ContentStyle(TEXT("Tutorials.Content")) .BoldContentStyle(TEXT("Tutorials.BoldContent")) .NumberedContentStyle(TEXT("Tutorials.NumberedContent")) .Header1Style(TEXT("Tutorials.Header1")) .Header2Style(TEXT("Tutorials.Header2")) .HyperlinkButtonStyle(TEXT("Tutorials.Hyperlink.Button")) .HyperlinkTextStyle(TEXT("Tutorials.Hyperlink.Text")) .SeparatorStyle(TEXT("Tutorials.Separator")); InteractiveTutorials = MakeShareable(new FInteractiveTutorials(FSimpleDelegate::CreateSP(this, &SIntroTutorials::TriggerCompleted))); InteractiveTutorials->SetupEditorHooks(); ChangePage(HomePath); } bool SIntroTutorials::IsHomeStyle() const { return InteractiveTutorials->GetCurrentTutorialStyle() == ETutorialStyle::Home; } void SIntroTutorials::ChangePage(const FString& Path) { bool const bPageExists = IDocumentation::Get()->PageExists(*Path); if (bPageExists) { TSharedPtr NewPage = IDocumentation::Get()->GetPage(Path, ParserConfiguration, DocumentationStyle); if (NewPage.IsValid() && (NewPage->GetNumExcerpts() > 0)) { InteractiveTutorials->SetCurrentTutorial(Path); CurrentPage = NewPage; CurrentPage->GetExcerpts(Excerpts); SetCurrentExcerpt(0); CurrentPagePath = Path; CurrentPageStartTime = FPlatformTime::Seconds(); // Set window title to current tutorial name if (ParentWindowPtr.IsValid()) { ParentWindowPtr.Pin()->SetTitle(GetCurrentTutorialName()); } } else { FFormatNamedArguments Args; Args.Add(TEXT("PathToDocumentation"), FText::FromString(Path)); const FText Message = FText::Format(LOCTEXT("PageOpenFailMessage", "Unable to access tutorial page \"{PathToDocumentation}\"."), Args); FNotificationInfo Info(Message); Info.ExpireDuration = 3.0f; Info.bUseLargeFont = false; TSharedPtr Notification = FSlateNotificationManager::Get().AddNotification(Info); if (Notification.IsValid()) { Notification->SetCompletionState(SNotificationItem::CS_Fail); } } } } void SIntroTutorials::SetContentArea() { HomeContentArea->ClearContent(); ContentArea->ClearContent(); if (Excerpts.IsValidIndex(CurrentExcerptIndex)) { auto& Excerpt = Excerpts[CurrentExcerptIndex]; if ( !Excerpt.Content.IsValid() ) { CurrentPage->GetExcerptContent( Excerpt ); } TSharedPtr ContentAreaToUse; if(IsHomeStyle()) { ContentAreaToUse = HomeContentArea; } else { ContentAreaToUse = ContentArea; } ContentAreaToUse->SetContent( SNew(SScrollBox) +SScrollBox::Slot() [ Excerpt.Content.ToSharedRef() ]); } if (ParentWindowPtr.IsValid()) { ParentWindowPtr.Pin()->BringToFront(); } } void SIntroTutorials::PlayDialogue(UDialogueWave* InDialogueWave) { if(InDialogueWave == NULL) { return; } // Set up the tutorial dialogue context if(TutorialDialogueContext.Speaker == NULL) { FString ZakVoicePath(TEXT("/Engine/Tutorial/Audio/Zak.Zak")); TutorialDialogueContext.Speaker = LoadObject(NULL, *ZakVoicePath); if(TutorialDialogueContext.Speaker != NULL) { TutorialDialogueContext.Speaker->AddToRoot(); } TutorialDialogueContext.Targets.Reset(); FString AudienceVoicePath(TEXT("/Engine/Tutorial/Audio/Audience.Audience")); UDialogueVoice* AudienceVoice = LoadObject(NULL, *AudienceVoicePath); if(AudienceVoice != NULL) { AudienceVoice->AddToRoot(); TutorialDialogueContext.Targets.Add(AudienceVoice); } } USoundBase* Sound = InDialogueWave->GetWaveFromContext(TutorialDialogueContext); if(Sound != NULL) { // Create audio component if(DialogueAudioComponent == NULL) { DialogueAudioComponent = FAudioDevice::CreateComponent( Sound, NULL, NULL, false ); DialogueAudioComponent->AddToRoot(); } else { DialogueAudioComponent->Stop(); } DialogueAudioComponent->Sound = Sound; DialogueAudioComponent->bAutoDestroy = false; DialogueAudioComponent->bIsUISound = true; DialogueAudioComponent->bAllowSpatialization = false; DialogueAudioComponent->bReverb = false; DialogueAudioComponent->Play(); } } void SIntroTutorials::GotoPreviousPage() { SetCurrentExcerpt(CurrentExcerptIndex - 1); } void SIntroTutorials::GotoNextPage() { SetCurrentExcerpt(CurrentExcerptIndex + 1); } void SIntroTutorials::SetCurrentExcerpt(int32 NewExcerptIdx) { if ( Excerpts.IsValidIndex(NewExcerptIdx) ) { CurrentExcerptIndex = NewExcerptIdx; // see if we should skip forward from here while ( !IsLastPage() && InteractiveTutorials->ShouldSkipExcerpt(Excerpts[CurrentExcerptIndex].Name) ) { CurrentExcerptIndex++; } SetContentArea(); auto& Excerpt = Excerpts[CurrentExcerptIndex]; UDialogueWave* const DialogueWave = InteractiveTutorials->GetDialogueForExcerpt(Excerpt.Name); if(DialogueWave != NULL) { PlayDialogue(DialogueWave); } InteractiveTutorials->SetCurrentExcerpt(Excerpt.Name); bCurrentSectionIsInteractive = InteractiveTutorials->IsExcerptInteractive(Excerpt.Name); } } void SIntroTutorials::TriggerCompleted() { InteractiveTutorials->OnExcerptCompleted( Excerpts[CurrentExcerptIndex].Name ); GotoNextPage(); } EVisibility SIntroTutorials::GetBackButtonVisibility() const { // hide back button on first page return IsFirstPage() ? EVisibility::Hidden : EVisibility::Visible; } FText SIntroTutorials::GetBackButtonText() const { // "home" on first page, "back" otherwise return LOCTEXT("BackLabel", "BACK"); } EVisibility SIntroTutorials::GetNextButtonVisibility() const { // hide next button on last page, unless we have a chain if(IsLastPage()) { FString NextTutorial; if(OnGotoNextTutorial.IsBound()) { NextTutorial = OnGotoNextTutorial.Execute(CurrentPagePath); } return NextTutorial.Len() > 0 ? EVisibility::Visible : EVisibility::Hidden; } else { return EVisibility::Visible; } } FText SIntroTutorials::GetNextButtonText() const { // "home" on first page, "back" otherwise return LOCTEXT("NextLabel", "NEXT"); } EVisibility SIntroTutorials::GetNavigationVisibility() const { return (IsHomeStyle() && (Excerpts.Num() == 1)) ? EVisibility::Collapsed : EVisibility::Visible; } FText SIntroTutorials::GetCurrentTutorialName() const { return CurrentPage->GetTitle(); } FText SIntroTutorials::GetTimeRemaining() const { if (Excerpts.Num() > 0) { FString VariableName = FString::Printf(TEXT("TimeRemaining%d"), CurrentExcerptIndex + 1); const FString* VariableValue = Excerpts[CurrentExcerptIndex].Variables.Find(VariableName); if(VariableValue != NULL) { return FText::FromString(*VariableValue); } } return FText::GetEmpty(); } EVisibility SIntroTutorials::GetContentVisibility() const { return !IsHomeStyle() ? EVisibility::Visible : EVisibility::Collapsed; } EVisibility SIntroTutorials::GetHomeContentVisibility() const { return IsHomeStyle() ? EVisibility::Visible : EVisibility::Collapsed; } FReply SIntroTutorials::OnHomeClicked() { OnGoHome.ExecuteIfBound(); ChangePage(HomePath); return FReply::Handled(); } FReply SIntroTutorials::OnPreviousClicked() { if (IsFirstPage()) { return OnHomeClicked(); } GotoPreviousPage(); return FReply::Handled(); } bool SIntroTutorials::OnPreviousIsEnabled() const { return Excerpts.IsValidIndex(CurrentExcerptIndex) && InteractiveTutorials->CanManuallyReverseExcerpt( Excerpts[CurrentExcerptIndex].Name ); } FReply SIntroTutorials::OnNextClicked() { if (IsLastPage()) { if(OnGotoNextTutorial.IsBound()) { FString NextPage = OnGotoNextTutorial.Execute(CurrentPagePath); if(NextPage.Len() > 0) { // Note: this currently limits us to chaining only two tutorials together ChangePage(NextPage); return FReply::Handled(); } } return OnHomeClicked(); } GotoNextPage(); return FReply::Handled(); } bool SIntroTutorials::OnNextIsEnabled() const { return Excerpts.IsValidIndex(CurrentExcerptIndex) && InteractiveTutorials->CanManuallyAdvanceExcerpt(Excerpts[CurrentExcerptIndex].Name); } FString SIntroTutorials::GetCurrentExcerptIdentifierName() const { if (Excerpts.IsValidIndex(CurrentExcerptIndex)) { return Excerpts[CurrentExcerptIndex].Name; } return FString(); } FText SIntroTutorials::GetCurrentExcerptTitle() const { if (Excerpts.IsValidIndex(CurrentExcerptIndex)) { // First try for unadorned 'StageTitle' FString VariableName = FString::Printf(TEXT("StageTitle")); const FString* VariableValue = Excerpts[CurrentExcerptIndex].Variables.Find(VariableName); if(VariableValue != NULL) { return FText::FromString(*VariableValue); } // Then try 'StageTitle' VariableName = FString::Printf(TEXT("StageTitle%d"), CurrentExcerptIndex + 1); VariableValue = Excerpts[CurrentExcerptIndex].Variables.Find(VariableName); if(VariableValue != NULL) { return FText::FromString(*VariableValue); } } FFormatNamedArguments Args; Args.Add(TEXT("CurrentPageNumber"), CurrentExcerptIndex + 1); return FText::Format(LOCTEXT("GenericStageTitle", "Step {CurrentPageNumber}"), Args); } FText SIntroTutorials::GetProgressText() const { FFormatNamedArguments Args; Args.Add(TEXT("CurrentPageNumber"), CurrentExcerptIndex + 1); Args.Add(TEXT("TotalPages"), Excerpts.Num()); return FText::Format(LOCTEXT("TutorialProgress", "{CurrentPageNumber}/{TotalPages}"), Args); } TOptional SIntroTutorials::GetProgress() const { if (Excerpts.Num() > 0) { return (float)(CurrentExcerptIndex) / (float)(Excerpts.Num() - 1); } return 0.0f; } const FSlateBrush* SIntroTutorials::GetContentAreaBackground() const { return FEditorStyle::GetBrush("Tutorials.ContentAreaBackground"); } int32 SIntroTutorials::GetCurrentExcerptIndex() const { return CurrentExcerptIndex; } FString SIntroTutorials::GetCurrentPagePath() const { return CurrentPagePath; } float SIntroTutorials::GetCurrentPageElapsedTime() const { return (float)(FPlatformTime::Seconds() - CurrentPageStartTime); } void SIntroTutorials::SetOnGoHome(const FOnGoHome& InDelegate) { OnGoHome = InDelegate; } bool SIntroTutorials::IsLastPage() const { return (CurrentExcerptIndex == (Excerpts.Num() - 1)); } bool SIntroTutorials::IsFirstPage() const { return CurrentExcerptIndex == 0; } #undef LOCTEXT_NAMESPACE