Files
UnrealEngineUWP/Engine/Source/Editor/IntroTutorials/Private/TutorialText.cpp
Thomas Sarkanen 6f1d963577 Adding image support to tutorial rich text
Improved text layout support when inserting runs/text or splitting lines on runs that were non-text (images or widgets). The text layout now inserts an extra text run when splitting a non-text run, which avoids issues where text was either being inserted into a non-text run (and vanishing), or an image run was being cloned (and appearing twice).

This also fixes the cursor movement in the multiline editable text when selecting over images or widgets (the cursor would jump to the start of the document as GetTextIndexAt hadn't been implemented. Additionally, it also fixes an issue where Measure was always trying to place the cursor at the end of an image run (ignoring the values of BeginIndex and EndIndex) which made the cursor offset draw in the wrong place.

These changes required the text layout to be able to create a default text run, which involved refactoring the text marshallers as the Slate text run now knows about the default text style, taking that responsibility away from the marshallers

Added tutorial-specific image decorator that accepts a content-relative or engine relative image path.

Added button to tutorial rich text editor to add new images.

All previously-imported images should still 'work'.

reviewed by Jamie.Dale,Nick.Atamas,Justin.Sargent

[CL 2302278 by Thomas Sarkanen in Main branch]
2014-09-18 08:09:29 -04:00

325 lines
10 KiB
C++

// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved.
#include "IntroTutorialsPrivatePCH.h"
#include "TutorialText.h"
#include "IDocumentation.h"
#include "ISourceCodeAccessModule.h"
#include "ContentBrowserModule.h"
#include "DesktopPlatformModule.h"
#include "Editor/MainFrame/Public/MainFrame.h"
#include "EditorTutorial.h"
#include "SourceCodeNavigation.h"
#include "TutorialImageDecorator.h"
#define LOCTEXT_NAMESPACE "TutorialText"
TArray<TSharedPtr<FHyperlinkTypeDesc>> FTutorialText::HyperlinkDescs;
/**
* This is a custom decorator used to allow arbitrary styling of text within a rich-text editor
* This is required since normal text styling can only work with known styles from a given Slate style-set
*/
class FTextStyleDecorator : public ITextDecorator
{
public:
static TSharedRef<FTextStyleDecorator> Create()
{
return MakeShareable(new FTextStyleDecorator());
}
virtual ~FTextStyleDecorator()
{
}
virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override
{
return (RunParseResult.Name == TEXT("TextStyle"));
}
virtual TSharedRef<ISlateRun> Create(const TSharedRef<class FTextLayout>& TextLayout, const FTextRunParseResults& RunParseResult, const FString& OriginalText, const TSharedRef< FString >& InOutModelText, const ISlateStyle* Style) override
{
FRunInfo RunInfo(RunParseResult.Name);
for(const TPair<FString, FTextRange>& Pair : RunParseResult.MetaData)
{
RunInfo.MetaData.Add(Pair.Key, OriginalText.Mid(Pair.Value.BeginIndex, Pair.Value.EndIndex - Pair.Value.BeginIndex));
}
FTextRange ModelRange;
ModelRange.BeginIndex = InOutModelText->Len();
*InOutModelText += OriginalText.Mid(RunParseResult.ContentRange.BeginIndex, RunParseResult.ContentRange.EndIndex - RunParseResult.ContentRange.BeginIndex);
ModelRange.EndIndex = InOutModelText->Len();
return FSlateTextRun::Create(RunInfo, InOutModelText, FTextStyleAndName::CreateTextBlockStyle(RunInfo), ModelRange);
}
};
static void OnBrowserLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url)
{
FPlatformProcess::LaunchURL(**Url, nullptr, nullptr);
}
}
static void OnDocLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url)
{
IDocumentation::Get()->Open(*Url);
}
}
static void ParseTutorialLink(const FString &InternalLink)
{
UBlueprint* Blueprint = LoadObject<UBlueprint>(nullptr, *InternalLink);
if (Blueprint && Blueprint->GeneratedClass)
{
FIntroTutorials& IntroTutorials = FModuleManager::GetModuleChecked<FIntroTutorials>(TEXT("IntroTutorials"));
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
const bool bRestart = true;
IntroTutorials.LaunchTutorial(Blueprint->GeneratedClass->GetDefaultObject<UEditorTutorial>(), bRestart, MainFrameModule.GetParentWindow());
}
}
static void OnTutorialLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url)
{
ParseTutorialLink(*Url);
}
}
static void ParseCodeLink(const FString &InternalLink)
{
// Tokens used by the code parsing. Details in the parse section
static const FString ProjectSpecifier(TEXT("[PROJECTPATH]"));
static const FString ProjectPathSpecifier(TEXT("[PROJECT]"));
static const FString EnginePathSpecifier(TEXT("[ENGINEPATH]"));
FString Path;
int32 Line = 0;
int32 Col = 0;
TArray<FString> Tokens;
InternalLink.ParseIntoArray(&Tokens, TEXT(","), 0);
int32 TokenStringsCount = Tokens.Num();
if (TokenStringsCount > 0)
{
Path = Tokens[0];
}
if (TokenStringsCount > 1)
{
TTypeFromString<int32>::FromString(Line, *Tokens[1]);
}
if (TokenStringsCount > 2)
{
TTypeFromString<int32>::FromString(Col, *Tokens[2]);
}
ISourceCodeAccessModule& SourceCodeAccessModule = FModuleManager::LoadModuleChecked<ISourceCodeAccessModule>("SourceCodeAccess");
ISourceCodeAccessor& SourceCodeAccessor = SourceCodeAccessModule.GetAccessor();
if (Path.Contains(EnginePathSpecifier) == true)
{
// replace engine path specifier with path to engine
Path.ReplaceInline(*EnginePathSpecifier, *FPaths::EngineDir());
}
if (Path.Contains(ProjectSpecifier) == true)
{
// replace project specifier with path to project
Path.ReplaceInline(*ProjectSpecifier, FApp::GetGameName());
}
if (Path.Contains(ProjectPathSpecifier) == true)
{
// replace project specifier with path to project
Path.ReplaceInline(*ProjectPathSpecifier, *FPaths::GetProjectFilePath());
}
Path = FPaths::ConvertRelativePathToFull(Path);
SourceCodeAccessor.OpenFileAtLine(Path, Line, Col);
}
static void OnCodeLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url)
{
ParseCodeLink(*Url);
}
}
static void ParseAssetLink(const FString& InternalLink, const FString* Action)
{
UObject* RequiredObject = LoadObject<UObject>(nullptr, *InternalLink);
if (RequiredObject != nullptr)
{
if(Action && *Action == TEXT("select"))
{
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
TArray<UObject*> AssetToBrowse;
AssetToBrowse.Add(RequiredObject);
ContentBrowserModule.Get().SyncBrowserToAssets(AssetToBrowse);
}
else
{
FAssetEditorManager::Get().OpenEditorForAsset(RequiredObject);
}
}
}
static void OnAssetLinkClicked(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
const FString* Action = Metadata.Find(TEXT("action"));
if(Url)
{
ParseAssetLink(*Url, Action);
}
}
static TSharedRef<IToolTip> OnGenerateDocTooltip(const FSlateHyperlinkRun::FMetadata& Metadata)
{
FText DisplayText;
const FString* Url = Metadata.Find(TEXT("href"));
if(Url != nullptr)
{
DisplayText = FText::Format(LOCTEXT("DocLinkPattern", "View Documentation: {0}"), FText::FromString(*Url));
}
else
{
DisplayText = LOCTEXT("UnknownLink", "Empty Hyperlink");
}
FString UrlString;
if(Url != nullptr)
{
UrlString = *Url;
}
// urls for rich tooltips must start with Shared/
if(UrlString.Len() > 0 && !UrlString.StartsWith(TEXT("Shared")))
{
UrlString = FString(TEXT("Shared")) / UrlString;
}
FString ExcerptString;
const FString* Excerpt = Metadata.Find(TEXT("excerpt"));
if(Excerpt != nullptr)
{
ExcerptString = *Excerpt;
}
return IDocumentation::Get()->CreateToolTip(DisplayText, nullptr, UrlString, ExcerptString);
}
static FText OnGetAssetTooltipText(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url != nullptr)
{
const FString* Action = Metadata.Find(TEXT("action"));
return FText::Format(LOCTEXT("AssetLinkPattern", "{0} asset: {1}"), (Action == nullptr || *Action == TEXT("select")) ? LOCTEXT("AssetOpenDesc", "Open") : LOCTEXT("AssetFindDesc", "Find"), FText::FromString(*Url));
}
return LOCTEXT("InvalidAssetLink", "Invalid Asset Link");
}
static FText OnGetCodeTooltipText(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url != nullptr)
{
const bool bUseShortIDEName = true;
return FText::Format(LOCTEXT("CodeLinkPattern", "Open code in {0}: {1}"), FSourceCodeNavigation::GetSuggestedSourceCodeIDE(), FText::FromString(*Url));
}
return LOCTEXT("InvalidCodeLink", "Invalid Code Link");
}
static FText OnGetTutorialTooltipText(const FSlateHyperlinkRun::FMetadata& Metadata)
{
const FString* Url = Metadata.Find(TEXT("href"));
if(Url != nullptr)
{
return FText::Format(LOCTEXT("TutorialLinkPattern", "Open tutorial: {0}"), FText::FromString(*Url));
}
return LOCTEXT("InvalidTutorialLink", "Invalid Tutorial Link");
}
void FTutorialText::GetRichTextDecorators(TArray< TSharedRef< class ITextDecorator > >& OutDecorators)
{
Initialize();
for(const auto& HyperlinkDesc : HyperlinkDescs)
{
OutDecorators.Add(FHyperlinkDecorator::Create(HyperlinkDesc->Id, HyperlinkDesc->OnClickedDelegate, HyperlinkDesc->TooltipTextDelegate, HyperlinkDesc->TooltipDelegate));
}
OutDecorators.Add(FTextStyleDecorator::Create());
OutDecorators.Add(FTutorialImageDecorator::Create());
}
void FTutorialText::Initialize()
{
if(HyperlinkDescs.Num() == 0)
{
HyperlinkDescs.Add(MakeShareable(new FHyperlinkTypeDesc(
EHyperlinkType::Browser,
LOCTEXT("BrowserLinkTypeLabel", "URL"),
LOCTEXT("BrowserLinkTypeTooltip", "A link that opens a browser window (e.g. http://www.unrealengine.com)"),
TEXT("browser"),
FSlateHyperlinkRun::FOnClick::CreateStatic(&OnBrowserLinkClicked))));
HyperlinkDescs.Add(MakeShareable(new FHyperlinkTypeDesc(
EHyperlinkType::UDN,
LOCTEXT("UDNLinkTypeLabel", "UDN"),
LOCTEXT("UDNLinkTypeTooltip", "A link that opens some UDN documentation (e.g. /Engine/Blueprints/UserGuide/Types/ClassBlueprint)"),
TEXT("udn"),
FSlateHyperlinkRun::FOnClick::CreateStatic(&OnDocLinkClicked),
FSlateHyperlinkRun::FOnGetTooltipText(),
FSlateHyperlinkRun::FOnGenerateTooltip::CreateStatic(&OnGenerateDocTooltip))));
HyperlinkDescs.Add(MakeShareable(new FHyperlinkTypeDesc(
EHyperlinkType::Asset,
LOCTEXT("AssetLinkTypeLabel", "Asset"),
LOCTEXT("AssetLinkTypeTooltip", "A link that opens an asset (e.g. /Game/StaticMeshes/SphereMesh.SphereMesh)"),
TEXT("asset"),
FSlateHyperlinkRun::FOnClick::CreateStatic(&OnAssetLinkClicked),
FSlateHyperlinkRun::FOnGetTooltipText::CreateStatic(&OnGetAssetTooltipText))));
HyperlinkDescs.Add(MakeShareable(new FHyperlinkTypeDesc(
EHyperlinkType::Code,
LOCTEXT("CodeLinkTypeLabel", "Code"),
LOCTEXT("CodeLinkTypeTooltip", "A link that opens code in your selected IDE.\nFor example: [PROJECTPATH]/Private/SourceFile.cpp,1,1.\nThe numbers correspond to line number and column number.\nYou can use [PROJECT], [PROJECTPATH] and [ENGINEPATH] tags to make paths."),
TEXT("code"),
FSlateHyperlinkRun::FOnClick::CreateStatic(&OnCodeLinkClicked),
FSlateHyperlinkRun::FOnGetTooltipText::CreateStatic(&OnGetCodeTooltipText))));
HyperlinkDescs.Add(MakeShareable(new FHyperlinkTypeDesc(
EHyperlinkType::Tutorial,
LOCTEXT("TutorialLinkTypeLabel", "Tutorial"),
LOCTEXT("TutorialLinkTypeTooltip", "A type of asset link that opens another tutorial, e.g. /Game/Tutorials/StaticMeshTutorial.StaticMeshTutorial"),
TEXT("tutorial"),
FSlateHyperlinkRun::FOnClick::CreateStatic(&OnTutorialLinkClicked),
FSlateHyperlinkRun::FOnGetTooltipText::CreateStatic(&OnGetTutorialTooltipText))));
}
}
const TArray<TSharedPtr<FHyperlinkTypeDesc>>& FTutorialText::GetHyperLinkDescs()
{
Initialize();
return HyperlinkDescs;
}
#undef LOCTEXT_NAMESPACE