Files
2025-05-18 13:04:45 +08:00

432 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Documentation.h"
#include "CoreTypes.h"
#include "Delegates/Delegate.h"
#include "Dialogs/Dialogs.h"
#include "DocumentationLink.h"
#include "DocumentationPage.h"
#include "DocumentationSettings.h"
#include "EngineAnalytics.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "IAnalyticsProviderET.h"
#include "Interfaces/IMainFrameModule.h"
#include "Interfaces/IPluginManager.h"
#include "Internationalization/Internationalization.h"
#include "Internationalization/Text.h"
#include "Layout/Margin.h"
#include "Misc/Attribute.h"
#include "Misc/CommandLine.h"
#include "Misc/PackageName.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "Misc/ReverseIterate.h"
#include "Modules/ModuleManager.h"
#include "SDocumentationAnchor.h"
#include "SDocumentationToolTip.h"
#include "Styling/CoreStyle.h"
#include "Styling/ISlateStyle.h"
#include "UDNParser.h"
#include "UnrealEdMisc.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SToolTip.h"
class IDocumentationPage;
class SVerticalBox;
class SWidget;
#define LOCTEXT_NAMESPACE "DocumentationActor"
DEFINE_LOG_CATEGORY(LogDocumentation);
TSharedRef< IDocumentation > FDocumentation::Create()
{
return MakeShareable( new FDocumentation() );
}
FDocumentation::FDocumentation()
{
IMainFrameModule& MainFrameModule = FModuleManager::LoadModuleChecked<IMainFrameModule>(TEXT("MainFrame"));
MainFrameModule.OnMainFrameSDKNotInstalled().AddRaw(this, &FDocumentation::HandleSDKNotInstalled);
if (const UDocumentationSettings* DocSettings = GetDefault<UDocumentationSettings>())
{
for (const FDocumentationBaseUrl& BaseUrl : DocSettings->DocumentationBaseUrls)
{
if (!RegisterBaseUrl(BaseUrl.Id, BaseUrl.Url))
{
UE_LOG(LogDocumentation, Warning, TEXT("Could not register documentation base URL: %s"), *BaseUrl.Id);
}
}
}
AddSourcePath(FPaths::Combine(FPaths::ProjectDir(), TEXT("Documentation"), TEXT("Source")));
for (TSharedRef<IPlugin> Plugin : IPluginManager::Get().GetEnabledPlugins())
{
AddSourcePath(FPaths::Combine(Plugin->GetBaseDir(), TEXT("Documentation"), TEXT("Source")));
}
AddSourcePath(FPaths::Combine(FPaths::EngineDir(), TEXT("Documentation"), TEXT("Source")));
RegisterConfigRedirects();
}
FDocumentation::~FDocumentation()
{
}
bool FDocumentation::OpenHome(FDocumentationSourceInfo Source, const FString& BaseUrlId) const
{
return Open(TEXT("%ROOT%"), Source, BaseUrlId);
}
bool FDocumentation::OpenHome(const FCultureRef& Culture, FDocumentationSourceInfo Source, const FString& BaseUrlId) const
{
return Open(TEXT("%ROOT%"), Culture, Source, BaseUrlId);
}
bool FDocumentation::OpenAPIHome(FDocumentationSourceInfo Source) const
{
FString Url;
FUnrealEdMisc::Get().GetURL(TEXT("APIDocsURL"), Url, true);
if (!Url.IsEmpty())
{
FUnrealEdMisc::Get().ReplaceDocumentationURLWildcards(Url, FInternationalization::Get().GetCurrentCulture());
FPlatformProcess::LaunchURL(*Url, nullptr, nullptr);
return true;
}
return false;
}
bool FDocumentation::Open(const FString& Link, FDocumentationSourceInfo Source, const FString& BaseUrlId) const
{
return OpenUrl(Link, FInternationalization::Get().GetCurrentCulture(), Source, BaseUrlId);
}
bool FDocumentation::Open(const FString& Link, const FCultureRef& Culture, FDocumentationSourceInfo Source, const FString& BaseUrlId) const
{
return OpenUrl(Link, Culture, Source, BaseUrlId);
}
TSharedRef< SWidget > FDocumentation::CreateAnchor( const TAttribute<FString>& Link, const FString& PreviewLink, const FString& PreviewExcerptName, const TAttribute<FString>& BaseUrlId) const
{
return SNew( SDocumentationAnchor )
.Link(Link)
.PreviewLink(PreviewLink)
.PreviewExcerptName(PreviewExcerptName)
.BaseUrlId(BaseUrlId);
}
TSharedRef< IDocumentationPage > FDocumentation::GetPage( const FString& Link, const TSharedPtr< FParserConfiguration >& Config, const FDocumentationStyle& Style )
{
TSharedPtr< IDocumentationPage > Page;
const TWeakPtr< IDocumentationPage >* ExistingPagePtr = LoadedPages.Find( Link );
if ( ExistingPagePtr != NULL )
{
const TSharedPtr< IDocumentationPage > ExistingPage = ExistingPagePtr->Pin();
if ( ExistingPage.IsValid() )
{
Page = ExistingPage;
}
}
if ( !Page.IsValid() )
{
Page = FDocumentationPage::Create( Link, FUDNParser::Create( Config, Style ) );
LoadedPages.Add( Link, TWeakPtr< IDocumentationPage >( Page ) );
}
return Page.ToSharedRef();
}
bool FDocumentation::PageExists(const FString& Link) const
{
const TWeakPtr< IDocumentationPage >* ExistingPagePtr = LoadedPages.Find(Link);
if (ExistingPagePtr != NULL)
{
return true;
}
for (const FString& SourcePath : SourcePaths)
{
FString LinkPath = FDocumentationLink::ToSourcePath(Link, SourcePath);
if (FPaths::FileExists(LinkPath))
{
return true;
}
}
return false;
}
bool FDocumentation::PageExists(const FString& Link, const FCultureRef& Culture) const
{
const TWeakPtr< IDocumentationPage >* ExistingPagePtr = LoadedPages.Find(Link);
if (ExistingPagePtr != NULL)
{
return true;
}
for (const FString& SourcePath : SourcePaths)
{
FString LinkPath = FDocumentationLink::ToSourcePath(Link, Culture, SourcePath);
if (FPaths::FileExists(LinkPath))
{
return true;
}
}
return false;
}
const TArray <FString>& FDocumentation::GetSourcePaths() const
{
return SourcePaths;
}
TSharedRef< class SToolTip > FDocumentation::CreateToolTip(const TAttribute<FText>& Text, const TSharedPtr<SWidget>& OverrideContent, const FString& Link, const FString& ExcerptName) const
{
return CreateToolTip(Text, OverrideContent, Link, ExcerptName, FText());
}
TSharedRef< class SToolTip > FDocumentation::CreateToolTip(const TAttribute<FText>& Text, const TSharedPtr<SWidget>& OverrideContent, const FString& Link, const FString& ExcerptName, const TAttribute<FText>& Shortcut) const
{
TSharedPtr< SDocumentationToolTip > DocToolTip;
if ( !Text.IsBound() && Text.Get().IsEmpty() )
{
return SNew( SToolTip );
}
if ( OverrideContent.IsValid() )
{
SAssignNew( DocToolTip, SDocumentationToolTip )
.DocumentationLink( Link )
.ExcerptName( ExcerptName )
.Shortcut(Shortcut)
[
OverrideContent.ToSharedRef()
];
}
else
{
SAssignNew(DocToolTip, SDocumentationToolTip)
.Text(Text)
.DocumentationLink(Link)
.ExcerptName(ExcerptName)
.Shortcut(Shortcut);
}
return SNew( SToolTip )
.IsInteractive( DocToolTip.ToSharedRef(), &SDocumentationToolTip::IsInteractive )
// Emulate text-only tool-tip styling that SToolTip uses when no custom content is supplied. We want documentation tool-tips to
// be styled just like text-only tool-tips
.BorderImage( FCoreStyle::Get().GetBrush("ToolTip.BrightBackground") )
// The documentation tooltip has padding built in, so that it can style its own background within the tooltip borders.
.TextMargin(FMargin(0.0f))
[
DocToolTip.ToSharedRef()
];
}
TSharedRef< class SToolTip > FDocumentation::CreateToolTip(const TAttribute<FText>& Text, const TSharedRef<SWidget>& OverrideContent, const TSharedPtr<SVerticalBox>& DocVerticalBox, const FString& Link, const FString& ExcerptName) const
{
TSharedRef<SDocumentationToolTip> DocToolTip =
SNew(SDocumentationToolTip)
.Text(Text)
.DocumentationLink(Link)
.ExcerptName(ExcerptName)
.AddDocumentation(false)
.DocumentationMargin(7)
[
OverrideContent
];
if (DocVerticalBox.IsValid())
{
DocToolTip->AddDocumentation(DocVerticalBox);
}
return SNew(SToolTip)
.IsInteractive(DocToolTip, &SDocumentationToolTip::IsInteractive)
// Emulate text-only tool-tip styling that SToolTip uses when no custom content is supplied. We want documentation tool-tips to
// be styled just like text-only tool-tips
.BorderImage( FCoreStyle::Get().GetBrush("ToolTip.BrightBackground") )
// The documentation tooltip has padding built in, so that it can style its own background within the tooltip borders.
.TextMargin(FMargin(0.0f))
[
DocToolTip
];
}
void FDocumentation::HandleSDKNotInstalled(const FString& PlatformName, const FString& InDocumentationPage)
{
if (FPackageName::IsValidLongPackageName(InDocumentationPage, true))
{
return;
}
IDocumentation::Get()->Open(InDocumentationPage);
}
bool FDocumentation::RegisterBaseUrl(const FString& Id, const FString& Url)
{
if (!Id.IsEmpty() && !Url.IsEmpty())
{
if (!RegisteredBaseUrls.Contains(Id))
{
RegisteredBaseUrls.Add(Id, Url);
return true;
}
UE_LOG(LogDocumentation, Warning, TEXT("Could not register documentation base URL with ID: %s. This ID is already in use."), *Id);
return false;
}
return false;
}
FString FDocumentation::GetBaseUrl(const FString& Id) const
{
if (!Id.IsEmpty())
{
const FString* BaseUrl = RegisteredBaseUrls.Find(Id);
if (BaseUrl != NULL && !BaseUrl->IsEmpty())
{
return *BaseUrl;
}
UE_LOG(LogDocumentation, Warning, TEXT("Could not resolve base URL with ID: %s. It may not have been registered."), *Id);
}
FString DefaultUrl;
FUnrealEdMisc::Get().GetURL(TEXT("DocumentationURL"), DefaultUrl, true);
return DefaultUrl;
}
bool FDocumentation::AddSourcePath(const FString& Path)
{
if (!Path.IsEmpty() && FPaths::DirectoryExists(Path))
{
SourcePaths.Add(Path);
return true;
}
return false;
}
bool FDocumentation::RegisterRedirect(const FName& Owner, const FDocumentationRedirect& Redirect)
{
return RedirectRegistry.Register(Owner, Redirect);
}
void FDocumentation::UnregisterRedirects(const FName& Owner)
{
RedirectRegistry.UnregisterAll(Owner);
}
bool FDocumentation::OpenUrl(const FString& Link, const FCultureRef& Culture, FDocumentationSourceInfo Source, const FString& BaseUrlId) const
{
// Original url
const FDocumentationUrl OriginalUrl(Link, BaseUrlId);
// Target url (redirected if applicable)
const FDocumentationUrl TargetUrl = RedirectUrl(OriginalUrl);
// Warn the user if they are opening a URL
if (TargetUrl.Link.StartsWith(TEXT("http")) || TargetUrl.Link.StartsWith(TEXT("https")))
{
FText Message = LOCTEXT("OpeningURLMessage", "You are about to open an external URL. This will open your web browser. Do you want to proceed?");
FText URLDialog = LOCTEXT("OpeningURLTitle", "Open external link");
FSuppressableWarningDialog::FSetupInfo Info(Message, URLDialog, "SuppressOpenURLWarning");
Info.ConfirmText = LOCTEXT("OpenURL_yes", "Yes");
Info.CancelText = LOCTEXT("OpenURL_no", "No");
FSuppressableWarningDialog OpenURLWarning(Info);
if (OpenURLWarning.ShowModal() == FSuppressableWarningDialog::Cancel)
{
return false;
}
else
{
FPlatformProcess::LaunchURL(*TargetUrl.Link, nullptr, nullptr);
return true;
}
}
FString DocumentationUrl;
if (!FParse::Param(FCommandLine::Get(), TEXT("testdocs")))
{
// Prioritize on-disk versions of the requested link over web versions
const FString DiskPath = FDocumentationLink::ToFilePath(TargetUrl.Link, Culture);
if (FPaths::FileExists(DiskPath))
{
DocumentationUrl = FDocumentationLink::ToFileUrl(TargetUrl.Link, Culture, Source);
}
else if (const FCulturePtr FallbackCulture = FInternationalization::Get().GetCulture(TEXT("en")))
{
const FString FallbackDiskPath = FDocumentationLink::ToFilePath(TargetUrl.Link, FallbackCulture.ToSharedRef());
if (FPaths::FileExists(FallbackDiskPath))
{
DocumentationUrl = FDocumentationLink::ToFileUrl(TargetUrl.Link, FallbackCulture.ToSharedRef(), Source);
}
}
}
if (DocumentationUrl.IsEmpty())
{
DocumentationUrl = FDocumentationLink::ToUrl(TargetUrl.Link, Culture, Source, TargetUrl.BaseUrlId);
}
if (!DocumentationUrl.IsEmpty())
{
FPlatformProcess::LaunchURL(*DocumentationUrl, nullptr, nullptr);
if (FEngineAnalytics::IsAvailable())
{
FEngineAnalytics::GetProvider().RecordEvent(TEXT("Editor.Usage.Documentation"), TEXT("OpenedPage"), TargetUrl.Link);
}
return true;
}
else
{
return false;
}
}
FDocumentationUrl FDocumentation::RedirectUrl(const FDocumentationUrl& OriginalUrl) const
{
FDocumentationRedirect Redirect;
if (!RedirectRegistry.GetRedirect(OriginalUrl.Link, Redirect))
{
return OriginalUrl;
}
else
{
UE_LOG(LogDocumentation, Log, TEXT("Documentation link \"%s\" was redirected to \"%s\""), *OriginalUrl.ToString(), *Redirect.ToUrl.ToString());
return Redirect.ToUrl;
}
}
void FDocumentation::RegisterConfigRedirects()
{
// Unregister all global redirects so we start with a clean slate every time we parse the config redirects
UnregisterRedirects(NAME_None);
const UDocumentationSettings* DocumentationSettings = GetDefault<UDocumentationSettings>();
if (!DocumentationSettings)
{
return;
}
// Iterate over redirects in reverse so the redirects deepest in the config hierarchy are registered last (higher priority)
for (const FDocumentationRedirect& ConfigRedirect : ReverseIterate(DocumentationSettings->DocumentationRedirects))
{
RegisterRedirect(NAME_None, ConfigRedirect);
}
}
#undef LOCTEXT_NAMESPACE