Files
UnrealEngineUWP/Engine/Source/Editor/IntroTutorials/Private/IntroTutorials.cpp
Thomas Sarkanen 8dc16308f3 Added tutorials button in top bar of editor & sub-editors
Icon is only visible if content is available for the editor in question.
Split editor settings into two groups - one is persistent settings and one is progress/state.
Tutorials record their dismissed state, so users can permenantly disable the 'nag' for a particular tutorial.
Tutorial content now solidifies when the mouse is hovered over it, so it can be made easier to read.
Fixed crash on startup if an intro tutorial was displaying rich text.
Also fixed crash for TTP# 345094, where a zero-length tutorial was being accessed.

[CL 2275934 by Thomas Sarkanen in Main branch]
2014-08-28 06:22:40 -04:00

797 lines
33 KiB
C++

// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "IntroTutorialsPrivatePCH.h"
#include "SIntroTutorials.h"
#include "Editor/WorkspaceMenuStructure/Public/WorkspaceMenuStructureModule.h"
#include "Editor/MainFrame/Public/MainFrame.h"
#include "Editor/LevelEditor/Public/LevelEditor.h"
#include "Editor/Kismet/Public/BlueprintEditorModule.h"
#include "../Plugins/Editor/EpicSurvey/Source/EpicSurvey/Public/IEpicSurveyModule.h"
#include "Editor/GameProjectGeneration/Public/GameProjectGenerationModule.h"
#include "Editor/UnrealEd/Public/EditorModes.h"
#include "Editor/UnrealEd/Public/SourceCodeNavigation.h"
#include "Editor/Matinee/Public/MatineeModule.h"
#include "EngineAnalytics.h"
#include "Runtime/Analytics/Analytics/Public/Interfaces/IAnalyticsProvider.h"
#include "Particles/ParticleSystem.h"
#include "EditorTutorialSettings.h"
#include "TutorialStateSettings.h"
#include "TutorialSettings.h"
#include "Settings.h"
#include "EditorTutorial.h"
#include "SEditorTutorials.h"
#include "STutorialsBrowser.h"
#include "STutorialNavigation.h"
#include "STutorialOverlay.h"
#include "TutorialStructCustomization.h"
#include "EditorTutorialDetailsCustomization.h"
#include "STutorialRoot.h"
#include "STutorialButton.h"
#define LOCTEXT_NAMESPACE "IntroTutorials"
IMPLEMENT_MODULE(FIntroTutorials, IntroTutorials)
const FName FIntroTutorials::IntroTutorialTabName(TEXT("IntroTutorial"));
const FString FIntroTutorials::IntroTutorialConfigSection(TEXT("IntroTutorials"));
const FString FIntroTutorials::DisableTutorialsSettingName(TEXT("DisableAllTutorials"));
const FString FIntroTutorials::InEditorTutorialPath(TEXT("Shared/Tutorials/inEditorTutorial"));
const FString FIntroTutorials::WelcomeTutorialPath(TEXT("Shared/Tutorials/UE4Welcome"));
const FString FIntroTutorials::InEditorGamifiedTutorialPath(TEXT("Shared/Tutorials/inEditorGamifiedTutorial"));
const FString FIntroTutorials::HomePath(TEXT("Shared/Tutorials"));
const FString FIntroTutorials::BlueprintHomePath(TEXT("Shared/Tutorials/TemplateTutorials/TemplateOverview"));
const FString FIntroTutorials::TemplateOverviewPath(TEXT("Shared/Tutorials/UE4Welcome"));
const FWelcomeTutorialProperties FIntroTutorials::UE4WelcomeTutorial(TEXT("Shared/Tutorials/UE4Welcome"), TEXT("SeenUE4Welcome"));
const FWelcomeTutorialProperties FIntroTutorials::BlueprintHomeTutorial(TEXT("Shared/Tutorials/InBlueprintEditorTutorial"), TEXT("SeenBlueprintWelcome"));
const FWelcomeTutorialProperties FIntroTutorials::ClassBlueprintWelcomeTutorial(TEXT("Shared/Tutorials/BlueprintInterfaceTutorial"), TEXT("SeenBlueprintWelcome_Class"), FString("5DCD4D58-9C9F-407F-8388-75D89CBBA7E8"));
const FWelcomeTutorialProperties FIntroTutorials::MacroLibraryBlueprintWelcomeTutorial(TEXT("Shared/Tutorials/BlueprintMacroLibInterfaceTutorial"), TEXT("SeenBlueprintWelcome_MacroLib"), FString("0D5081E0-F29A-4C35-B4DC-1E5E2825AB54"));
const FWelcomeTutorialProperties FIntroTutorials::InterfaceBlueprintWelcomeTutorial(TEXT("Shared/Tutorials/BlueprintInterfacesInterfaceTutorial"), TEXT("SeenBlueprintWelcome_Interface"), FString("E86E8C9A-4804-4680-B10F-BDC8E95C7AFD"));
const FWelcomeTutorialProperties FIntroTutorials::LevelScriptBlueprintWelcomeTutorial(TEXT("Shared/Tutorials/LevelBlueprintInterfaceTutorial"), TEXT("SeenBlueprintWelcome_LevelScript"), FString("B061E309-517D-4916-BFCB-E8104C8F4C35"));
const FWelcomeTutorialProperties FIntroTutorials::AddCodeToProjectWelcomeTutorial(TEXT("Shared/Tutorials/AddCodeToProjectTutorial"), TEXT("SeenAddCodeToProjectWelcome"), FString("D8D9A7E7-68B3-4CBE-80FE-4B88C47B7524"));
const FWelcomeTutorialProperties FIntroTutorials::MatineeEditorWelcomeTutorial(TEXT("Shared/Tutorials/InMatineeEditorTutorial"), TEXT("SeenMatineeEditorWelcome"), FString("6439C991-A77B-4B52-953D-3F29B1DD1860"));
const FWelcomeTutorialProperties FIntroTutorials::TemplateOverview(TemplateOverviewPath, TEXT("SeenTemplateOverview"));
FIntroTutorials::FIntroTutorials()
: CurrentObjectClass(nullptr)
{
bDisableTutorials = false;
bEnablePostTutorialSurveys = false;
bDesireResettingTutorialSeenFlagOnLoad = FParse::Param(FCommandLine::Get(), TEXT("tutorials"));
}
/** Generates an analytics name for the given tutorial path string */
FString FIntroTutorials::AnalyticsEventNameFromTutorialPath(const FString& TutorialPath) const
{
// strip off everything but the last folder, supporting both types of slashes
FString RightStr;
bool bSplit = TutorialPath.Split( TEXT("/"), NULL, &RightStr, ESearchCase::IgnoreCase, ESearchDir::FromEnd );
if (!bSplit)
{
TutorialPath.Split( TEXT("\\"), NULL, &RightStr, ESearchCase::IgnoreCase, ESearchDir::FromEnd );
}
// then append that to the header
// e.g. Rocket.Tutorials.ClosedInEditorTutorial
FString OutStr(TEXT("Rocket.Tutorials.Closed"));
OutStr += RightStr;
return OutStr;
}
TSharedRef<FExtender> FIntroTutorials::AddSummonBlueprintTutorialsMenuExtender(const TSharedRef<FUICommandList> CommandList, const TArray<UObject*> EditingObjects) const
{
UObject* PrimaryObject = nullptr;
if(EditingObjects.Num() > 0)
{
PrimaryObject = EditingObjects[0];
}
TSharedRef<FExtender> Extender(new FExtender());
Extender->AddMenuExtension(
"HelpBrowse",
EExtensionHook::After,
CommandList,
FMenuExtensionDelegate::CreateRaw(this, &FIntroTutorials::AddSummonBlueprintTutorialsMenuExtension, PrimaryObject));
return Extender;
}
void FIntroTutorials::StartupModule()
{
GConfig->GetBool(*IntroTutorialConfigSection, *DisableTutorialsSettingName, bDisableTutorials, GEditorGameAgnosticIni);
// This code can run with content commandlets. Slate is not initialized with commandlets and the below code will fail.
if (!bDisableTutorials && !IsRunningCommandlet())
{
// Add tutorial for main frame opening
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.OnMainFrameCreationFinished().AddRaw(this, &FIntroTutorials::MainFrameLoad);
MainFrameModule.OnMainFrameSDKNotInstalled().AddRaw(this, &FIntroTutorials::HandleSDKNotInstalled);
// Add menu option for level editor tutorial
MainMenuExtender = MakeShareable(new FExtender);
MainMenuExtender->AddMenuExtension("HelpBrowse", EExtensionHook::After, NULL, FMenuExtensionDelegate::CreateRaw(this, &FIntroTutorials::AddSummonTutorialsMenuExtension));
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>( "LevelEditor" );
LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MainMenuExtender);
// Add menu option to blueprint editor as well
FBlueprintEditorModule& BPEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>( "Kismet" );
BPEditorModule.GetMenuExtensibilityManager()->GetExtenderDelegates().Add(FAssetEditorExtender::CreateRaw(this, &FIntroTutorials::AddSummonBlueprintTutorialsMenuExtender));
// Add hook for when specific asset editor is opened
FAssetEditorManager::Get().OnAssetEditorOpened().AddRaw(this, &FIntroTutorials::OnAssetEditorOpened);
IMatineeModule::Get().OnMatineeEditorOpened().AddRaw(this, &FIntroTutorials::OnMatineeEditorOpened);
// Add hook for when AddToCodeProject dialog window is opened
FGameProjectGenerationModule::Get().OnAddCodeToProjectDialogOpened().AddRaw(this, &FIntroTutorials::OnAddCodeToProjectDialogOpened);
// Add hook for New Project dialog window is opened
//FGameProjectGenerationModule::Get().OnNewProjectProjectDialogOpened().AddRaw(this, &FIntroTutorials::OnNewProjectDialogOpened);
// Add hook for when editor changes modes (e.g. Place/Paint/Landscape/Foliage)
GLevelEditorModeTools().OnEditorModeChanged().AddRaw(this, &FIntroTutorials::OnEditorModeChanged);
FSourceCodeNavigation::AccessOnCompilerNotFound().AddRaw( this, &FIntroTutorials::HandleCompilerNotFound );
// set up some tutorial data
// NOTE: we match classes based on an "is-a" relationship, so place more derived classes in the map before base classes so they are encountered first
AssetEditorTutorialPropertyMap.Add(UTexture::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InTextureEditorTutorial"), TEXT("SeenTextureEditorWelcome"), FString("6CC90D96-03D6-4847-B1DF-EB3018C36C4E")));
AssetEditorTutorialPropertyMap.Add(UMaterial::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InMaterialEditorTutorial"), TEXT("SeenMaterialEditorWelcome"), FString("2290389E-A428-46C1-B9BD-A7F110CDF83E")));
AssetEditorTutorialPropertyMap.Add(UAnimBlueprint::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InPersonaAnimBlueprintEditorTutorial"), TEXT("SeenPersonaAnimBlueprintEditorWelcome"), FString("FE5CD131-E4AF-4FE6-9269-12B573B66CA8")));
AssetEditorTutorialPropertyMap.Add(UBlueprint::StaticClass(), FWelcomeTutorialProperties(FWelcomeTutorialProperties::FWelcomeTutorialPropertiesChooserDelegate::CreateRaw(this, &FIntroTutorials::ChooseBlueprintWelcomeTutorial)));
AssetEditorTutorialPropertyMap.Add(UStaticMesh::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InStaticMeshEditorTutorial"), TEXT("SeenStaticMeshEditorWelcome"), FString("762FCAD0-9384-4A6B-8D57-92DA771AD890")));
AssetEditorTutorialPropertyMap.Add(UDestructibleMesh::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InDestructibleMeshEditorTutorial"), TEXT("SeenDestructibleMeshEditorWelcome"), FString("415C385F-C7D3-4CE8-BCC2-F19BB512AF06")));
AssetEditorTutorialPropertyMap.Add(USkeletalMesh::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InSkeletalMeshEditorTutorial"), TEXT("SeenSkeletalMeshEditorWelcome"), FString("F8BF69A0-391F-4612-B289-B2B2B3F7F428")));
AssetEditorTutorialPropertyMap.Add(UParticleSystem::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InParticleSystemEditorTutorial"), TEXT("SeenParticleSystemEditorWelcome"), FString("B5ACB9D0-229B-4DA2-A3D6-001A368A48B1")));
AssetEditorTutorialPropertyMap.Add(USoundCue::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InSoundCueEditorTutorial"), TEXT("SeenSoundCueEditorWelcome"), FString("3C12B4B3-36F8-48D2-A8B0-874CCD5D3891")));
AssetEditorTutorialPropertyMap.Add(UMaterialInstance::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InMaterialInstanceEditorTutorial"), TEXT("SeenMaterialInstanceEditorWelcome"), FString("A1AAC488-8389-4B71-925D-7A2CFB65F8D4")));
AssetEditorTutorialPropertyMap.Add(UPhysicsAsset::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InPhATEditorTutorial"), TEXT("SeenPhATEditorWelcome"), FString("B576D8AB-9248-4FE4-80F7-96EDA87BBAD3")));
// note these 3 go to the same editor
AssetEditorTutorialPropertyMap.Add(UAnimMontage::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InPersonaAnimEditorTutorial"), TEXT("SeenPersonaAnimEditorWelcome"), FString("8C7C3772-9135-426C-93A7-5937C5CBEA7B")));
AssetEditorTutorialPropertyMap.Add(UAnimSequence::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InPersonaAnimEditorTutorial"), TEXT("SeenPersonaAnimEditorWelcome"), FString("8C7C3772-9135-426C-93A7-5937C5CBEA7B")));
AssetEditorTutorialPropertyMap.Add(UBlendSpace::StaticClass(), FWelcomeTutorialProperties(TEXT("Shared/Tutorials/InPersonaAnimEditorTutorial"), TEXT("SeenPersonaAnimEditorWelcome"), FString("8C7C3772-9135-426C-93A7-5937C5CBEA7B")));
// map editor modes to tutorial properties
//@todo Placement mode appears to be firing erroneously when switching to other modes, want to understand why before enabling this mode
//EditorModeTutorialPropertyMap.Add(FBuiltinEditorModes::EM_Placement, FWelcomeTutorialProperties(TEXT("Shared/Tutorials/EditorPlacementModeTutorial"), TEXT("SeenEditorPlacementModeWelcome")));
EditorModeTutorialPropertyMap.Add(FBuiltinEditorModes::EM_Landscape, FWelcomeTutorialProperties(TEXT("Shared/Tutorials/EditorLandscapeModeTutorial"), TEXT("SeenEditorLandscapeModeWelcome"), FString("8B33592A-CCEE-4129-AE3F-77A5B68955CB")));
EditorModeTutorialPropertyMap.Add(FBuiltinEditorModes::EM_MeshPaint, FWelcomeTutorialProperties(TEXT("Shared/Tutorials/EditorMeshPaintModeTutorial"), TEXT("SeenEditorMeshPaintModeWelcome"), FString("8E36B45D-D249-42E1-ABEE-2ABCBCE8ED26")));
EditorModeTutorialPropertyMap.Add(FBuiltinEditorModes::EM_Foliage, FWelcomeTutorialProperties(TEXT("Shared/Tutorials/EditorFoliageModeTutorial"), TEXT("SeenEditorFoliageModeWelcome"), FString("9DD00A25-16D5-40D5-BDC4-98DF70CEB0F7")));
// init survey map
{
for (auto It = AssetEditorTutorialPropertyMap.CreateConstIterator(); It; ++It)
{
const FWelcomeTutorialProperties& Props = It.Value();
TutorialSurveyMap.Add(Props.TutorialPath, Props.SurveyGuid);
}
for (auto It = EditorModeTutorialPropertyMap.CreateConstIterator(); It; ++It)
{
const FWelcomeTutorialProperties& Props = It.Value();
TutorialSurveyMap.Add(Props.TutorialPath, Props.SurveyGuid);
}
TutorialSurveyMap.Add(ClassBlueprintWelcomeTutorial.TutorialPath, ClassBlueprintWelcomeTutorial.SurveyGuid);
TutorialSurveyMap.Add(MacroLibraryBlueprintWelcomeTutorial.TutorialPath, MacroLibraryBlueprintWelcomeTutorial.SurveyGuid);
TutorialSurveyMap.Add(InterfaceBlueprintWelcomeTutorial.TutorialPath, InterfaceBlueprintWelcomeTutorial.SurveyGuid);
TutorialSurveyMap.Add(LevelScriptBlueprintWelcomeTutorial.TutorialPath, LevelScriptBlueprintWelcomeTutorial.SurveyGuid);
TutorialSurveyMap.Add(AddCodeToProjectWelcomeTutorial.TutorialPath, AddCodeToProjectWelcomeTutorial.SurveyGuid);
TutorialSurveyMap.Add(MatineeEditorWelcomeTutorial.TutorialPath, MatineeEditorWelcomeTutorial.SurveyGuid);
FGuid SurveyGuid;
// set up some paths for surveys and analytics
SurveyGuid.ParseExact(FString("B6C19629-3172-4924-91FE-AC557F66703C"), EGuidFormats::DigitsWithHyphens, SurveyGuid);
TutorialSurveyMap.Add(InEditorTutorialPath, SurveyGuid);
SurveyGuid.ParseExact(FString("3ADE38A2-9FCA-4BA1-B0A3-F64DDFAB2A0E"), EGuidFormats::DigitsWithHyphens, SurveyGuid);
TutorialSurveyMap.Add(InEditorGamifiedTutorialPath, SurveyGuid);
}
// maybe reset all the "have I seen this once" flags
if (bDesireResettingTutorialSeenFlagOnLoad)
{
ResetWelcomeTutorials();
}
}
// Register to display our settings
ISettingsModule* SettingsModule = ISettingsModule::Get();
if (SettingsModule != nullptr)
{
SettingsModule->RegisterSettings("Editor", "General", "Tutorials",
LOCTEXT("EditorTutorialSettingsName", "Tutorials"),
LOCTEXT("EditorTutorialSettingsDescription", "Control what tutorials are available in the Editor."),
GetMutableDefault<UEditorTutorialSettings>()
);
SettingsModule->RegisterSettings("Project", "Engine", "Tutorials",
LOCTEXT("TutorialSettingsName", "Tutorials"),
LOCTEXT("TutorialSettingsDescription", "Control what tutorials are available in this project."),
GetMutableDefault<UTutorialSettings>()
);
}
// register details customizations
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.RegisterCustomPropertyTypeLayout("TutorialContent", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FTutorialContentCustomization::MakeInstance));
PropertyEditorModule.RegisterCustomPropertyTypeLayout("TutorialContentAnchor", FOnGetPropertyTypeCustomizationInstance::CreateStatic(&FTutorialContentAnchorCustomization::MakeInstance));
PropertyEditorModule.RegisterCustomClassLayout("EditorTutorial", FOnGetDetailCustomizationInstance::CreateStatic(&FEditorTutorialDetailsCustomization::MakeInstance));
}
void FIntroTutorials::ShutdownModule()
{
if (!bDisableTutorials && !IsRunningCommandlet())
{
FSourceCodeNavigation::AccessOnCompilerNotFound().RemoveAll( this );
GLevelEditorModeTools().OnEditorModeChanged().RemoveAll(this);
}
if (BlueprintEditorExtender.IsValid() && FModuleManager::Get().IsModuleLoaded("Kismet"))
{
FBlueprintEditorModule& BPEditorModule = FModuleManager::LoadModuleChecked<FBlueprintEditorModule>("Kismet");
BPEditorModule.GetMenuExtensibilityManager()->RemoveExtender(BlueprintEditorExtender);
}
if (MainMenuExtender.IsValid() && FModuleManager::Get().IsModuleLoaded("LevelEditor"))
{
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>( "LevelEditor" );
LevelEditorModule.GetMenuExtensibilityManager()->RemoveExtender(MainMenuExtender);
}
if (FModuleManager::Get().IsModuleLoaded("MainFrame"))
{
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.OnMainFrameCreationFinished().RemoveAll(this);
}
ISettingsModule* SettingsModule = ISettingsModule::Get();
if (SettingsModule != nullptr)
{
SettingsModule->UnregisterSettings("Editor", "General", "Tutorials");
SettingsModule->UnregisterSettings("Project", "Engine", "Tutorials");
}
if(FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyEditorModule.UnregisterCustomPropertyTypeLayout("TutorialContent");
PropertyEditorModule.UnregisterCustomPropertyTypeLayout("TutorialWidgetReference");
PropertyEditorModule.UnregisterCustomClassLayout("EditorTutorial");
}
}
void FIntroTutorials::AddSummonTutorialsMenuExtension(FMenuBuilder& MenuBuilder)
{
MenuBuilder.BeginSection("Tutorials", LOCTEXT("TutorialsLabel", "Tutorials"));
MenuBuilder.AddMenuEntry(
LOCTEXT("TutorialsMenuEntryTitle", "Tutorials"),
LOCTEXT("TutorialsMenuEntryToolTip", "Opens up introductory tutorials covering the basics of using the Unreal Engine 4 Editor."),
FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tutorials"),
FUIAction(FExecuteAction::CreateRaw(this, &FIntroTutorials::SummonTutorialHome)));
MenuBuilder.EndSection();
}
void FIntroTutorials::AddSummonBlueprintTutorialsMenuExtension(FMenuBuilder& MenuBuilder, UObject* PrimaryObject)
{
MenuBuilder.BeginSection("Tutorials", LOCTEXT("TutorialsLabel", "Tutorials"));
MenuBuilder.AddMenuEntry(
LOCTEXT("BlueprintMenuEntryTitle", "Blueprint Overview"),
LOCTEXT("BlueprintMenuEntryToolTip", "Opens up an introductory overview of Blueprints."),
FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tutorials"),
FUIAction(FExecuteAction::CreateRaw(this, &FIntroTutorials::SummonBlueprintTutorialHome, PrimaryObject, true)));
if(PrimaryObject != nullptr)
{
UBlueprint* BP = Cast<UBlueprint>(PrimaryObject);
if(BP != nullptr)
{
UEnum* Enum = FindObject<UEnum>(ANY_PACKAGE, TEXT("EBlueprintType"), true);
check(Enum);
MenuBuilder.AddMenuEntry(
FText::Format(LOCTEXT("BlueprintTutorialsMenuEntryTitle", "{0} Tutorial"), Enum->GetEnumText(BP->BlueprintType)),
LOCTEXT("BlueprintTutorialsMenuEntryToolTip", "Opens up an introductory tutorial covering this particular part of the Blueprint editor."),
FSlateIcon(FEditorStyle::GetStyleSetName(), "LevelEditor.Tutorials"),
FUIAction(FExecuteAction::CreateRaw(this, &FIntroTutorials::SummonBlueprintTutorialHome, PrimaryObject, false)));
}
}
MenuBuilder.EndSection();
}
void FIntroTutorials::MainFrameLoad(TSharedPtr<SWindow> InRootWindow, bool bIsNewProjectWindow)
{
if (!bIsNewProjectWindow)
{
// Save off pointer to root window so we can parent the tutorial window to it when summoned
if (InRootWindow.IsValid())
{
RootWindow = InRootWindow;
}
// install a root widget for the tutorial overlays to hang off
if(InRootWindow.IsValid() && !TutorialRoot.IsValid())
{
InRootWindow->AddOverlaySlot()
[
SAssignNew(TutorialRoot, STutorialRoot)
];
}
// See if we should show 'welcome' screen
MaybeOpenWelcomeTutorial(UE4WelcomeTutorial.TutorialPath, UE4WelcomeTutorial.SeenOnceSettingName);
}
}
void FIntroTutorials::SummonTutorialHome()
{
if(FParse::Param(FCommandLine::Get(), TEXT("NewTutorials")))
{
FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked<FLevelEditorModule>( "LevelEditor" );
SummonTutorialBrowser(LevelEditorModule.GetLevelEditorTab()->GetParentWindow().ToSharedRef());
}
else
{
CurrentObjectClass = nullptr;
SummonTutorialWindowForPage(HomePath);
}
}
void FIntroTutorials::SummonBlueprintTutorialHome(UObject* Asset, bool bForceWelcome)
{
if(Asset != nullptr)
{
CurrentObjectClass = Asset->GetClass();
}
FWelcomeTutorialProperties const* Tutorial = ChooseBlueprintWelcomeTutorial(Asset, bForceWelcome);
check(Tutorial);
SummonTutorialWindowForPage(Tutorial->TutorialPath);
// make sure we've seen this tutorial
GConfig->SetBool(*IntroTutorialConfigSection, *Tutorial->SeenOnceSettingName, true, GEditorGameAgnosticIni);
}
void FIntroTutorials::SummonTutorialWindowForPage(const FString& Path)
{
TSharedPtr<SWindow> Window;
// If window already exists, bring it to the front
if (TutorialWindow.IsValid())
{
Window = TutorialWindow.Pin();
Window->BringToFront();
}
// If not, create a new window
else
{
// Window
Window = SNew(SWindow)
.Title(LOCTEXT( "WindowTitle", "Tutorials" ))
.ClientSize(FVector2D(660.f, 637.f))
.SupportsMaximize(false)
.SupportsMinimize(false);
TutorialWindow = Window;
if(RootWindow.IsValid() && !PLATFORM_MAC)
{
FSlateApplication::Get().AddWindowAsNativeChild(Window.ToSharedRef(), RootWindow.Pin().ToSharedRef());
}
else
{
FSlateApplication::Get().AddWindow(Window.ToSharedRef());
}
// tutorial Widget
TSharedRef<SIntroTutorials> TutorialWidgetRef =
SNew(SIntroTutorials)
.ParentWindow(Window)
.HomeButtonVisibility(TAttribute<EVisibility>::Create(TAttribute<EVisibility>::FGetter::CreateRaw(this, &FIntroTutorials::GetHomeButtonVisibility)))
.OnGotoNextTutorial(FOnGotoNextTutorial::CreateRaw(this, &FIntroTutorials::HandleGotoNextTutorial));
Window->SetContent(TutorialWidgetRef);
Window->SetOnWindowClosed( FOnWindowClosed::CreateRaw(this, &FIntroTutorials::OnTutorialWindowClosed) );
TutorialWidgetRef->SetOnGoHome( FOnGoHome::CreateRaw(this, &FIntroTutorials::OnTutorialDismissed) );
TutorialWidget = TutorialWidgetRef;
Window->BringToFront();
}
// Change page to desired path
if(TutorialWidget.IsValid())
{
TutorialWidget.Pin()->ChangePage(Path);
}
}
void FIntroTutorials::OnTutorialWindowClosed(const TSharedRef<SWindow>& Window)
{
OnTutorialDismissed();
}
void FIntroTutorials::OnTutorialDismissed() const
{
TSharedPtr<SIntroTutorials> WidgetPtr = TutorialWidget.Pin();
if (WidgetPtr.IsValid())
{
FString const CurrentPagePath = WidgetPtr->GetCurrentPagePath();
// submit analytics data
if( FEngineAnalytics::IsAvailable() )
{
// prepare and send analytics data
bool const bQuitOnWelcomeScreen = (CurrentPagePath == WelcomeTutorialPath);
FString const CurrentExcerptTitle = bQuitOnWelcomeScreen ? TEXT("Welcome Screen") : WidgetPtr->GetCurrentExcerptIdentifierName();
int32 const CurrentExcerptIndex = bQuitOnWelcomeScreen ? -1 : WidgetPtr->GetCurrentExcerptIndex();
float const CurrentPageElapsedTime = bQuitOnWelcomeScreen ? 0.f : WidgetPtr->GetCurrentPageElapsedTime();
TArray<FAnalyticsEventAttribute> EventAttributes;
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("LastExcerptIndex"), CurrentExcerptIndex));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("LastExcerptTitle"), CurrentExcerptTitle));
EventAttributes.Add(FAnalyticsEventAttribute(TEXT("TimeSpentInTutorial"), CurrentPageElapsedTime));
FString AnalyticsEventName = AnalyticsEventNameFromTutorialPath(CurrentPagePath);
FEngineAnalytics::GetProvider().RecordEvent( AnalyticsEventName, EventAttributes );
}
// kick off survey, but not if it was dismissed immediately
if ( bEnablePostTutorialSurveys && IEpicSurveyModule::IsAvailable() && (WidgetPtr->GetCurrentExcerptIndex() > 0) )
{
FGuid const* const SurveyID = TutorialSurveyMap.Find(CurrentPagePath);
if (SurveyID && !SurveyID->IsValid())
{
// launch end-of-tutorial survey if one is desired
IEpicSurveyModule::Get().PromptSurvey(*SurveyID);
}
}
}
}
void FIntroTutorials::OnAssetEditorOpened(UObject* Asset)
{
if(Asset != nullptr)
{
FWelcomeTutorialProperties const* const FoundProps = FindAssetEditorTutorialProperties(Asset->GetClass());
if (FoundProps)
{
// run delegate if it exists
FWelcomeTutorialProperties const* const PropsToUse = FoundProps->ChooserDelegate.IsBound()
? FoundProps->ChooserDelegate.Execute(Asset)
: FoundProps;
if (PropsToUse)
{
if(MaybeOpenWelcomeTutorial(PropsToUse->TutorialPath, PropsToUse->SeenOnceSettingName))
{
CurrentObjectClass = Asset->GetClass();
}
}
}
}
}
FWelcomeTutorialProperties const* FIntroTutorials::ChooseBlueprintWelcomeTutorial(UObject* BlueprintObject)
{
return ChooseBlueprintWelcomeTutorial(BlueprintObject, true);
}
FWelcomeTutorialProperties const* FIntroTutorials::ChooseBlueprintWelcomeTutorial(UObject* BlueprintObject, bool bForceWelcome)
{
TutorialChainMap.Empty();
UBlueprint* BP = Cast<UBlueprint>(BlueprintObject);
if (BP)
{
bool bSeenWelcome = false;
GConfig->GetBool(*IntroTutorialConfigSection, *BlueprintHomeTutorial.SeenOnceSettingName, bSeenWelcome, GEditorGameAgnosticIni);
if(!bSeenWelcome || bForceWelcome)
{
switch (BP->BlueprintType)
{
case BPTYPE_Normal:
TutorialChainMap.Add(BlueprintHomePath, ClassBlueprintWelcomeTutorial.TutorialPath);
break;
case BPTYPE_MacroLibrary:
TutorialChainMap.Add(BlueprintHomePath, MacroLibraryBlueprintWelcomeTutorial.TutorialPath);
break;
case BPTYPE_Interface:
TutorialChainMap.Add(BlueprintHomePath, InterfaceBlueprintWelcomeTutorial.TutorialPath);
break;
case BPTYPE_LevelScript:
TutorialChainMap.Add(BlueprintHomePath, LevelScriptBlueprintWelcomeTutorial.TutorialPath);
break;
}
}
else
{
switch (BP->BlueprintType)
{
case BPTYPE_Normal:
return &ClassBlueprintWelcomeTutorial;
case BPTYPE_MacroLibrary:
return &MacroLibraryBlueprintWelcomeTutorial;
case BPTYPE_Interface:
return &InterfaceBlueprintWelcomeTutorial;
case BPTYPE_LevelScript:
return &LevelScriptBlueprintWelcomeTutorial;
}
}
}
return &BlueprintHomeTutorial;
}
FWelcomeTutorialProperties const* FIntroTutorials::FindAssetEditorTutorialProperties(UClass const* Class) const
{
for (auto It = AssetEditorTutorialPropertyMap.CreateConstIterator(); It; ++It)
{
UClass* const KeyClass = It.Key();
if (Class->IsChildOf(KeyClass))
{
return &It.Value();
}
}
return NULL;
}
bool FIntroTutorials::MaybeOpenWelcomeTutorial(const FString& TutorialPath, const FString& ConfigSettingName)
{
if(FParse::Param(FCommandLine::Get(), TEXT("NewTutorials")))
{
// try editor startup tutorial
TSubclassOf<UEditorTutorial> EditorStartupTutorialClass = LoadClass<UEditorTutorial>(NULL, *GetDefault<UEditorTutorialSettings>()->StartupTutorial.AssetLongPathname, NULL, LOAD_None, NULL);
if(EditorStartupTutorialClass != nullptr)
{
UEditorTutorial* Tutorial = EditorStartupTutorialClass->GetDefaultObject<UEditorTutorial>();
if (!GetDefault<UTutorialStateSettings>()->HaveSeenTutorial(Tutorial))
{
LaunchTutorial(Tutorial);
return true;
}
}
// Try project startup tutorial
TSubclassOf<UEditorTutorial> ProjectStartupTutorialClass = LoadClass<UEditorTutorial>(NULL, *GetDefault<UTutorialSettings>()->StartupTutorial.AssetLongPathname, NULL, LOAD_None, NULL);
if(ProjectStartupTutorialClass != nullptr)
{
UEditorTutorial* Tutorial = ProjectStartupTutorialClass->GetDefaultObject<UEditorTutorial>();
if (!GetDefault<UTutorialStateSettings>()->HaveSeenTutorial(Tutorial))
{
LaunchTutorial(Tutorial);
return true;
}
}
return false;
}
// don't open if viewing any tutorial other than the index
if (TutorialWidget.IsValid() && (TutorialWidget.Pin()->GetCurrentPagePath() != HomePath))
{
return false;
}
bool bSeenWelcome = false;
GConfig->GetBool(*IntroTutorialConfigSection, *ConfigSettingName, bSeenWelcome, GEditorGameAgnosticIni);
if (!bSeenWelcome)
{
bool const bPageExists = IDocumentation::Get()->PageExists(*TutorialPath);
if (bPageExists)
{
SummonTutorialWindowForPage(*TutorialPath);
// Tell ini file that we've seen this now
GConfig->SetBool(*IntroTutorialConfigSection, *ConfigSettingName, true, GEditorGameAgnosticIni);
return true;
}
}
return false;
}
bool FIntroTutorials::MaybeOpenWelcomeTutorial(const FWelcomeTutorialProperties& TutorialProperties)
{
return MaybeOpenWelcomeTutorial(TutorialProperties.TutorialPath, TutorialProperties.SeenOnceSettingName);
}
template< typename KeyType >
void FIntroTutorials::ResetTutorialPropertyMap(TMap<KeyType, FWelcomeTutorialProperties> Map) const
{
for (auto It = Map.CreateConstIterator(); It; ++It)
{
ResetTutorial(It.Value());
}
}
void FIntroTutorials::ResetWelcomeTutorials() const
{
ResetTutorialPropertyMap(AssetEditorTutorialPropertyMap);
ResetTutorialPropertyMap(EditorModeTutorialPropertyMap);
ResetTutorial(UE4WelcomeTutorial);
ResetTutorial(BlueprintHomeTutorial);
ResetTutorial(ClassBlueprintWelcomeTutorial);
ResetTutorial(MacroLibraryBlueprintWelcomeTutorial);
ResetTutorial(InterfaceBlueprintWelcomeTutorial);
ResetTutorial(LevelScriptBlueprintWelcomeTutorial);
ResetTutorial(AddCodeToProjectWelcomeTutorial);
ResetTutorial(MatineeEditorWelcomeTutorial);
ResetTutorial(TemplateOverview);
}
void FIntroTutorials::OnAddCodeToProjectDialogOpened()
{
MaybeOpenWelcomeTutorial(AddCodeToProjectWelcomeTutorial);
}
void FIntroTutorials::OnNewProjectDialogOpened()
{
MaybeOpenWelcomeTutorial(TemplateOverview);
}
void FIntroTutorials::OnMatineeEditorOpened()
{
MaybeOpenWelcomeTutorial(MatineeEditorWelcomeTutorial);
}
void FIntroTutorials::OnEditorModeChanged(FEdMode* Mode, bool bEnteringMode)
{
// editor-mode changed events can fire before the MainFrameLoad event, so wait until *after* the welcome tutorial to start showing editor-mode tutorials
// this avoids erroneously suppressing a mode tutorial because it opened but was then immediately replaced by the UE4 welcome tutorial
if (!HasSeenTutorial(UE4WelcomeTutorial))
return;
// only show tutorials when entering an editor mode, not when leaving it
if (bEnteringMode)
{
FWelcomeTutorialProperties const* const FoundProp = EditorModeTutorialPropertyMap.Find(Mode->GetID());
if (FoundProp)
{
MaybeOpenWelcomeTutorial(FoundProp->TutorialPath, FoundProp->SeenOnceSettingName);
}
}
}
void FIntroTutorials::HandleCompilerNotFound()
{
#if PLATFORM_WINDOWS
SummonTutorialWindowForPage( TEXT( "Shared/Tutorials/InstallingVisualStudioTutorial" ) );
#elif PLATFORM_MAC
SummonTutorialWindowForPage( TEXT( "Shared/Tutorials/InstallingXCodeTutorial" ) );
#else
STUBBED("FIntroTutorials::HandleCompilerNotFound");
#endif
}
void FIntroTutorials::HandleSDKNotInstalled(const FString& PlatformName, const FString& DocLink)
{
SummonTutorialWindowForPage(DocLink);
}
bool FIntroTutorials::HasSeenTutorial( const FWelcomeTutorialProperties& TutProps ) const
{
bool bSeenTutorial = false;
GConfig->GetBool(*IntroTutorialConfigSection, *TutProps.SeenOnceSettingName, bSeenTutorial, GEditorGameAgnosticIni);
return bSeenTutorial;
}
void FIntroTutorials::ResetTutorial(const FWelcomeTutorialProperties& TutProps) const
{
FString SettingName = TutProps.SeenOnceSettingName;
if (!SettingName.IsEmpty())
{
GConfig->SetBool(*IntroTutorialConfigSection, *SettingName, false, GEditorGameAgnosticIni);
}
}
EVisibility FIntroTutorials::GetHomeButtonVisibility() const
{
if(CurrentObjectClass != nullptr)
{
return CurrentObjectClass->IsChildOf(UBlueprint::StaticClass()) ? EVisibility::Hidden : EVisibility::Visible;
}
return EVisibility::Visible;
}
FString FIntroTutorials::HandleGotoNextTutorial(const FString& InCurrentPage) const
{
const FString* NextTutorial = TutorialChainMap.Find(InCurrentPage);
if(NextTutorial != nullptr)
{
return *NextTutorial;
}
return FString();
}
void FIntroTutorials::RegisterTutorialForAssetEditor(UClass* AssetClass, const FString& TutorialDocPath, const FString& TutorialHasBeenSeenSettingName, const FString& SurveyGUIDString)
{
FWelcomeTutorialProperties TutorialData(TutorialDocPath, TutorialHasBeenSeenSettingName, SurveyGUIDString);
if (bDesireResettingTutorialSeenFlagOnLoad)
{
ResetTutorial(TutorialData);
}
TutorialSurveyMap.Add(TutorialDocPath, TutorialData.SurveyGuid);
AssetEditorTutorialPropertyMap.Add(AssetClass, TutorialData);
}
void FIntroTutorials::UnregisterTutorialForAssetEditor(UClass* AssetClass)
{
const FWelcomeTutorialProperties& TutorialData = AssetEditorTutorialPropertyMap.FindChecked(AssetClass);
TutorialSurveyMap.Remove(TutorialData.TutorialPath);
AssetEditorTutorialPropertyMap.Remove(AssetClass);
}
FOnIsPicking& FIntroTutorials::OnIsPicking()
{
return OnIsPickingDelegate;
}
void FIntroTutorials::SummonTutorialBrowser(TSharedRef<SWindow> InWindow, const FString& InFilter)
{
if(TutorialRoot.IsValid())
{
TutorialRoot->SummonTutorialBrowser(InWindow, InFilter);
}
}
void FIntroTutorials::LaunchTutorial(UEditorTutorial* InTutorial, bool bInRestart, TWeakPtr<SWindow> InNavigationWindow, FSimpleDelegate OnTutorialClosed, FSimpleDelegate OnTutorialExited)
{
if(TutorialRoot.IsValid())
{
TutorialRoot->LaunchTutorial(InTutorial, bInRestart, InNavigationWindow, OnTutorialClosed, OnTutorialExited);
}
}
void FIntroTutorials::CloseAllTutorialContent()
{
if (TutorialRoot.IsValid())
{
TutorialRoot->CloseAllTutorialContent();
}
}
void FIntroTutorials::GoToPreviousStage()
{
if (TutorialRoot.IsValid())
{
TutorialRoot->GoToPreviousStage();
}
}
void FIntroTutorials::GoToNextStage(TWeakPtr<SWindow> InNavigationWindow)
{
if (TutorialRoot.IsValid())
{
TutorialRoot->GoToNextStage(InNavigationWindow);
}
}
TSharedRef<SWidget> FIntroTutorials::CreateTutorialsWidget(FName InContext, TWeakPtr<SWindow> InContextWindow) const
{
return SNew(STutorialButton)
.Context(InContext)
.ContextWindow(InContextWindow);
}
#undef LOCTEXT_NAMESPACE