1553 lines
52 KiB
C++
1553 lines
52 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "SNewClassDialog.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Misc/App.h"
|
|
#include "SlateOptMacros.h"
|
|
#include "Widgets/Layout/SSeparator.h"
|
|
#include "Widgets/Images/SImage.h"
|
|
#include "Widgets/Layout/SGridPanel.h"
|
|
#include "Widgets/Input/SEditableTextBox.h"
|
|
#include "Widgets/Input/SButton.h"
|
|
#include "Widgets/Input/SCheckBox.h"
|
|
#include "Framework/Docking/TabManager.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "Engine/Blueprint.h"
|
|
#include "Editor/EditorPerProjectUserSettings.h"
|
|
#include "Engine/BlueprintGeneratedClass.h"
|
|
#include "Interfaces/IProjectManager.h"
|
|
#include "SGetSuggestedIDEWidget.h"
|
|
#include "SourceCodeNavigation.h"
|
|
#include "ClassViewerModule.h"
|
|
#include "ClassViewerFilter.h"
|
|
#include "IContentBrowserSingleton.h"
|
|
#include "ContentBrowserModule.h"
|
|
#include "SClassViewer.h"
|
|
#include "DesktopPlatformModule.h"
|
|
#include "IDocumentation.h"
|
|
#include "EditorClassUtils.h"
|
|
#include "UObject/UObjectHash.h"
|
|
#include "Widgets/Workflow/SWizard.h"
|
|
#include "Widgets/Input/SHyperlink.h"
|
|
#include "TutorialMetaData.h"
|
|
#include "Kismet2/KismetEditorUtilities.h"
|
|
#include "IAssetTools.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
|
|
#include "Framework/Notifications/NotificationManager.h"
|
|
#include "Widgets/Notifications/SNotificationList.h"
|
|
#include "FeaturedClasses.inl"
|
|
#include "Subsystems/AssetEditorSubsystem.h"
|
|
#include "Editor.h"
|
|
#include "Styling/StyleColors.h"
|
|
#include "Widgets/Input/SSegmentedControl.h"
|
|
#include "ClassIconFinder.h"
|
|
#include "SWarningOrErrorBox.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "GameProjectGeneration"
|
|
|
|
/** The last selected module name. Meant to keep the same module selected after first selection */
|
|
FString SNewClassDialog::LastSelectedModuleName;
|
|
|
|
|
|
struct FParentClassItem
|
|
{
|
|
FNewClassInfo ParentClassInfo;
|
|
|
|
FParentClassItem(const FNewClassInfo& InParentClassInfo)
|
|
: ParentClassInfo(InParentClassInfo)
|
|
{}
|
|
};
|
|
|
|
class FNativeClassParentFilter : public IClassViewerFilter
|
|
{
|
|
public:
|
|
FNativeClassParentFilter()
|
|
{
|
|
ProjectModules = GameProjectUtils::GetCurrentProjectModules();
|
|
}
|
|
|
|
virtual bool IsClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const UClass* InClass, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs ) override
|
|
{
|
|
// We allow a class that belongs to any module in the current project, as you don't actually choose the destination module until after you've selected your parent class
|
|
return GameProjectUtils::IsValidBaseClassForCreation(InClass, ProjectModules);
|
|
}
|
|
|
|
virtual bool IsUnloadedClassAllowed(const FClassViewerInitializationOptions& InInitOptions, const TSharedRef< const IUnloadedBlueprintData > InUnloadedClassData, TSharedRef< FClassViewerFilterFuncs > InFilterFuncs) override
|
|
{
|
|
return false;
|
|
}
|
|
|
|
private:
|
|
/** The list of currently available modules for this project */
|
|
TArray<FModuleContextInfo> ProjectModules;
|
|
};
|
|
|
|
static void FindPublicEngineHeaderFiles(TArray<FString>& OutFiles, const FString& Path)
|
|
{
|
|
TArray<FString> ModuleDirs;
|
|
IFileManager::Get().FindFiles(ModuleDirs, *(Path / TEXT("*")), false, true);
|
|
for (const FString& ModuleDir : ModuleDirs)
|
|
{
|
|
IFileManager::Get().FindFilesRecursive(OutFiles, *(Path / ModuleDir / TEXT("Classes")), TEXT("*.h"), true, false, false);
|
|
IFileManager::Get().FindFilesRecursive(OutFiles, *(Path / ModuleDir / TEXT("Public")), TEXT("*.h"), true, false, false);
|
|
}
|
|
}
|
|
|
|
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
void SNewClassDialog::Construct( const FArguments& InArgs )
|
|
{
|
|
ClassDomain = InArgs._ClassDomain;
|
|
bSyncContentBrowserToNewClass = InArgs._SyncContentBrowserToNewClass;
|
|
|
|
{
|
|
TArray<FModuleContextInfo> CurrentModules = GameProjectUtils::GetCurrentProjectModules();
|
|
check(CurrentModules.Num()); // this should never happen since GetCurrentProjectModules is supposed to add a dummy runtime module if the project currently has no modules
|
|
|
|
TArray<FModuleContextInfo> CurrentPluginModules = GameProjectUtils::GetCurrentProjectPluginModules();
|
|
|
|
CurrentModules.Append(CurrentPluginModules);
|
|
|
|
AvailableModules.Reserve(CurrentModules.Num());
|
|
for(const FModuleContextInfo& ModuleInfo : CurrentModules)
|
|
{
|
|
AvailableModules.Emplace(MakeShareable(new FModuleContextInfo(ModuleInfo)));
|
|
}
|
|
|
|
Algo::SortBy(AvailableModules, &FModuleContextInfo::ModuleName);
|
|
}
|
|
|
|
// If we've been given an initial path that maps to a valid project module, use that as our initial module and path
|
|
|
|
if (ClassDomain == EClassDomain::Blueprint)
|
|
{
|
|
NewClassPath = InArgs._InitialPath.IsEmpty() ? TEXT("/Game") : InArgs._InitialPath;
|
|
|
|
// Pick a valid default path if the path is not writable
|
|
FContentBrowserModule& ContentBrowserModule = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser");
|
|
NewClassPath = ContentBrowserModule.Get().GetInitialPathToSaveAsset(FContentBrowserItemPath(NewClassPath, EContentBrowserPathType::Internal)).GetInternalPathString();
|
|
}
|
|
else if(!InArgs._InitialPath.IsEmpty())
|
|
{
|
|
const FString AbsoluteInitialPath = FPaths::ConvertRelativePathToFull(InArgs._InitialPath);
|
|
for(const auto& AvailableModule : AvailableModules)
|
|
{
|
|
if(AbsoluteInitialPath.StartsWith(AvailableModule->ModuleSourcePath))
|
|
{
|
|
SelectedModuleInfo = AvailableModule;
|
|
NewClassPath = AbsoluteInitialPath;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
DefaultClassPrefix = InArgs._DefaultClassPrefix;
|
|
DefaultClassName = InArgs._DefaultClassName;
|
|
|
|
// If we didn't get given a valid path override (see above), try and automatically work out the best default module
|
|
// If we have a runtime module with the same name as our project, then use that
|
|
// Otherwise, set out default target module as the first runtime module in the list
|
|
if(ClassDomain == EClassDomain::Native && !SelectedModuleInfo.IsValid())
|
|
{
|
|
const FString ProjectName = FApp::GetProjectName();
|
|
|
|
// Find initially selected module based on simple fallback in this order..
|
|
// Previously selected module, main project module, the first runtime module
|
|
TSharedPtr<FModuleContextInfo> ProjectModule;
|
|
TSharedPtr<FModuleContextInfo> RuntimeModule;
|
|
|
|
for (const auto& AvailableModule : AvailableModules)
|
|
{
|
|
// Check if this module matches our last used
|
|
if (AvailableModule->ModuleName == LastSelectedModuleName)
|
|
{
|
|
SelectedModuleInfo = AvailableModule;
|
|
break;
|
|
}
|
|
|
|
if (AvailableModule->ModuleName == ProjectName)
|
|
{
|
|
ProjectModule = AvailableModule;
|
|
}
|
|
|
|
//Check if this is a runtime module and if we haven't already found one
|
|
if (AvailableModule->ModuleType == EHostType::Runtime && RuntimeModule == nullptr)
|
|
{
|
|
RuntimeModule = AvailableModule;
|
|
}
|
|
}
|
|
|
|
if (!SelectedModuleInfo.IsValid())
|
|
{
|
|
if (ProjectModule.IsValid())
|
|
{
|
|
// use the project module we found
|
|
SelectedModuleInfo = ProjectModule;
|
|
}
|
|
else if (RuntimeModule.IsValid())
|
|
{
|
|
// use the first runtime module we found
|
|
SelectedModuleInfo = RuntimeModule;
|
|
}
|
|
else
|
|
{
|
|
// default to just the first module
|
|
SelectedModuleInfo = AvailableModules[0];
|
|
}
|
|
}
|
|
|
|
NewClassPath = SelectedModuleInfo->ModuleSourcePath;
|
|
}
|
|
|
|
ClassLocation = GameProjectUtils::EClassLocation::UserDefined; // the first call to UpdateInputValidity will set this correctly based on NewClassPath
|
|
|
|
ParentClassInfo = FNewClassInfo(InArgs._Class);
|
|
|
|
bShowFullClassTree = false;
|
|
|
|
LastPeriodicValidityCheckTime = 0;
|
|
PeriodicValidityCheckFrequency = 4;
|
|
bLastInputValidityCheckSuccessful = true;
|
|
bPreventPeriodicValidityChecksUntilNextChange = false;
|
|
|
|
FClassViewerInitializationOptions Options;
|
|
Options.Mode = EClassViewerMode::ClassPicker;
|
|
Options.DisplayMode = EClassViewerDisplayMode::TreeView;
|
|
Options.bIsActorsOnly = false;
|
|
Options.bIsPlaceableOnly = false;
|
|
Options.bIsBlueprintBaseOnly = false;
|
|
Options.bShowUnloadedBlueprints = false;
|
|
Options.bShowNoneOption = false;
|
|
Options.bShowObjectRootClass = true;
|
|
Options.bExpandRootNodes = true;
|
|
|
|
TSharedPtr<IClassViewerFilter> ClassFilter = InArgs._ClassViewerFilter;
|
|
if (!ClassFilter.IsValid() && InArgs._ClassDomain == EClassDomain::Native)
|
|
{
|
|
// Prevent creating native classes based on blueprint classes
|
|
ClassFilter = MakeShared<FNativeClassParentFilter>();
|
|
}
|
|
|
|
if (ClassFilter.IsValid())
|
|
{
|
|
Options.ClassFilters.Add(ClassFilter.ToSharedRef());
|
|
|
|
// Only show the Object root class if it's a valid base (this helps keep the tree clean)
|
|
if (!ClassFilter->IsClassAllowed(Options, UObject::StaticClass(), MakeShared<FClassViewerFilterFuncs>()))
|
|
{
|
|
Options.bShowObjectRootClass = false;
|
|
}
|
|
}
|
|
|
|
ClassViewer = StaticCastSharedRef<SClassViewer>(FModuleManager::LoadModuleChecked<FClassViewerModule>("ClassViewer").CreateClassViewer(Options, FOnClassPicked::CreateSP(this, &SNewClassDialog::OnAdvancedClassSelected)));
|
|
|
|
// Make sure the featured classes all pass the active class filter
|
|
TArray<FNewClassInfo> ValidatedFeaturedClasses;
|
|
ValidatedFeaturedClasses.Reserve(InArgs._FeaturedClasses.Num());
|
|
for (const FNewClassInfo& FeaturedClassInfo : InArgs._FeaturedClasses)
|
|
{
|
|
if (FeaturedClassInfo.ClassType != FNewClassInfo::EClassType::UObject || ClassViewer->IsClassAllowed(FeaturedClassInfo.BaseClass))
|
|
{
|
|
ValidatedFeaturedClasses.Add(FeaturedClassInfo);
|
|
}
|
|
}
|
|
|
|
SetupParentClassItems(ValidatedFeaturedClasses);
|
|
UpdateInputValidity();
|
|
|
|
TSharedRef<SWidget> DocWidget = IDocumentation::Get()->CreateAnchor(TAttribute<FString>(this, &SNewClassDialog::GetSelectedParentDocLink));
|
|
DocWidget->SetVisibility(TAttribute<EVisibility>(this, &SNewClassDialog::GetDocLinkVisibility));
|
|
|
|
const float EditableTextHeight = 26.0f;
|
|
|
|
IContentBrowserSingleton& ContentBrowser = FModuleManager::LoadModuleChecked<FContentBrowserModule>("ContentBrowser").Get();
|
|
|
|
FPathPickerConfig BlueprintPathConfig;
|
|
if (ClassDomain == EClassDomain::Blueprint)
|
|
{
|
|
BlueprintPathConfig.DefaultPath = NewClassPath;
|
|
BlueprintPathConfig.bFocusSearchBoxWhenOpened = false;
|
|
BlueprintPathConfig.bAllowContextMenu = true;
|
|
BlueprintPathConfig.bAllowClassesFolder = false;
|
|
BlueprintPathConfig.bAllowReadOnlyFolders = false;
|
|
BlueprintPathConfig.OnPathSelected = FOnPathSelected::CreateSP(this, &SNewClassDialog::OnBlueprintPathSelected);
|
|
BlueprintPathConfig.bNotifyDefaultPathSelected = true;
|
|
}
|
|
|
|
OnAddedToProject = InArgs._OnAddedToProject;
|
|
|
|
ChildSlot
|
|
[
|
|
SNew(SBorder)
|
|
.Padding(18.0f)
|
|
.BorderImage( FAppStyle::GetBrush("Docking.Tab.ContentAreaBrush") )
|
|
[
|
|
SNew(SVerticalBox)
|
|
.AddMetaData<FTutorialMetaData>(TEXT("AddCodeMajorAnchor"))
|
|
|
|
+SVerticalBox::Slot()
|
|
[
|
|
SAssignNew( MainWizard, SWizard)
|
|
.ShowPageList(false)
|
|
.CanFinish(this, &SNewClassDialog::CanFinish)
|
|
.FinishButtonText( ClassDomain == EClassDomain::Native ? LOCTEXT("FinishButtonText_Native", "Create Class") : FText::Format(LOCTEXT("FinishButtonText_Blueprint", "Create {0} Class"), ParentClassInfo.IsSet() ? ParentClassInfo.GetClassName() : FText::FromStringView(TEXT("Blueprint"))))
|
|
.FinishButtonToolTip (
|
|
ClassDomain == EClassDomain::Native ?
|
|
LOCTEXT("FinishButtonToolTip_Native", "Creates the code files to add your new class.") :
|
|
FText::Format(LOCTEXT("FinishButtonToolTip_Blueprint", "Creates the new class based on the specified parent {0} class."), ParentClassInfo.IsSet() ? ParentClassInfo.GetClassName() : FText::FromStringView(TEXT("Blueprint")))
|
|
)
|
|
.OnCanceled(this, &SNewClassDialog::CancelClicked)
|
|
.OnFinished(this, &SNewClassDialog::FinishClicked)
|
|
.InitialPageIndex(ParentClassInfo.IsSet() ? 1 : 0)
|
|
.PageFooter()
|
|
[
|
|
// Get IDE information
|
|
SNew(SBorder)
|
|
.Visibility( this, &SNewClassDialog::GetGlobalErrorLabelVisibility )
|
|
.BorderImage(FAppStyle::Get().GetBrush("RoundedError"))
|
|
.Padding(FMargin(0.0f, 5.0f))
|
|
.Content()
|
|
[
|
|
SNew(SHorizontalBox)
|
|
|
|
+SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.Padding(5.f, 2.f)
|
|
.AutoWidth()
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::Get().GetBrush("Icons.ErrorWithColor"))
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text( this, &SNewClassDialog::GetGlobalErrorLabelText )
|
|
.AutoWrapText(true)
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.HAlign(HAlign_Center)
|
|
.AutoWidth()
|
|
.Padding(5.f, 0.f)
|
|
[
|
|
SNew(SGetSuggestedIDEWidget)
|
|
]
|
|
]
|
|
]
|
|
|
|
// Choose parent class
|
|
+SWizard::Page()
|
|
.CanShow(!ParentClassInfo.IsSet()) // We can't move to this widget page if we've been given a parent class to use
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
// Title
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall"))
|
|
.Text( LOCTEXT( "ParentClassTitle", "Choose Parent Class" ) )
|
|
.TransformPolicy(ETextTransformPolicy::ToUpper)
|
|
]
|
|
|
|
+ SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.HAlign(HAlign_Center)
|
|
[
|
|
SNew(SSegmentedControl<bool>)
|
|
.OnValueChanged(this, &SNewClassDialog::OnFullClassTreeChanged)
|
|
.Value(this, &SNewClassDialog::IsFullClassTreeShown)
|
|
+SSegmentedControl<bool>::Slot(false)
|
|
.Text(LOCTEXT("CommonClasses", "Common Classes"))
|
|
+ SSegmentedControl<bool>::Slot(true)
|
|
.Text(LOCTEXT("AllClasses", "All Classes"))
|
|
]
|
|
|
|
// Page description and view options
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f, 10.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(
|
|
ClassDomain == EClassDomain::Native ?
|
|
LOCTEXT("ChooseParentClassDescription_Native", "This will add a C++ header and source code file to your game project.") :
|
|
FText::Format(LOCTEXT("ChooseParentClassDescription_Blueprint", "This will add a new class inheriting from {0} to your game project."), ParentClassInfo.IsSet() ? ParentClassInfo.GetClassName() : FText::FromStringView(TEXT("Blueprint")))
|
|
)
|
|
]
|
|
|
|
// Add Code list
|
|
+SVerticalBox::Slot()
|
|
.FillHeight(1.f)
|
|
.Padding(0.0f, 10.0f)
|
|
[
|
|
SNew(SBorder)
|
|
.AddMetaData<FTutorialMetaData>(TEXT("AddCodeOptions"))
|
|
.BorderImage( FAppStyle::GetBrush("ToolPanel.GroupBorder") )
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+SVerticalBox::Slot()
|
|
[
|
|
// Basic view
|
|
SAssignNew(ParentClassListView, SListView< TSharedPtr<FParentClassItem> >)
|
|
.ListItemsSource(&ParentClassItemsSource)
|
|
.SelectionMode(ESelectionMode::Single)
|
|
.ClearSelectionOnClick(false)
|
|
.OnGenerateRow(this, &SNewClassDialog::MakeParentClassListViewWidget)
|
|
.OnMouseButtonDoubleClick( this, &SNewClassDialog::OnParentClassItemDoubleClicked )
|
|
.OnSelectionChanged(this, &SNewClassDialog::OnClassSelected)
|
|
.Visibility(this, &SNewClassDialog::GetBasicParentClassVisibility)
|
|
]
|
|
|
|
+SVerticalBox::Slot()
|
|
[
|
|
// Advanced view
|
|
SNew(SBox)
|
|
.Visibility(this, &SNewClassDialog::GetAdvancedParentClassVisibility)
|
|
[
|
|
ClassViewer.ToSharedRef()
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
// Class selection
|
|
+SVerticalBox::Slot()
|
|
.Padding(30.0f, 2.0f)
|
|
.AutoHeight()
|
|
[
|
|
SNew(SGridPanel)
|
|
.FillColumn(1, 1.0f)
|
|
|
|
// Class label
|
|
+ SGridPanel::Slot(0,0)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(2.0f, 2.0f, 10.0f, 2.0f)
|
|
.HAlign(HAlign_Left)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("ParentClassLabel", "Selected Class"))
|
|
]
|
|
|
|
+ SGridPanel::Slot(0, 1)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(2.0f, 2.0f, 10.0f, 2.0f)
|
|
.HAlign(HAlign_Left)
|
|
[
|
|
SNew(STextBlock)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.Text(LOCTEXT("ParentClassSourceLabel", "Selected Class Source"))
|
|
]
|
|
|
|
+ SGridPanel::Slot(1, 0)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(2.0f)
|
|
.HAlign(HAlign_Left)
|
|
[
|
|
SNew(SHorizontalBox)
|
|
|
|
+ SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.AutoWidth()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SNewClassDialog::GetSelectedParentClassName)
|
|
]
|
|
|
|
+ SHorizontalBox::Slot()
|
|
.VAlign(VAlign_Center)
|
|
.AutoWidth()
|
|
[
|
|
DocWidget
|
|
]
|
|
]
|
|
|
|
+ SGridPanel::Slot(1, 1)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(2.0f)
|
|
.HAlign(HAlign_Left)
|
|
[
|
|
SNew(SHyperlink)
|
|
.Style(FAppStyle::Get(), "Common.GotoNativeCodeHyperlink")
|
|
.OnNavigate(this, &SNewClassDialog::OnEditCodeClicked)
|
|
.Text(this, &SNewClassDialog::GetSelectedParentClassFilename)
|
|
.ToolTipText(FText::Format(LOCTEXT("GoToCode_ToolTip", "Click to open this source file in {0}"), FSourceCodeNavigation::GetSelectedSourceCodeIDE()))
|
|
.Visibility(this, &SNewClassDialog::GetSourceHyperlinkVisibility)
|
|
]
|
|
]
|
|
]
|
|
|
|
// Name class
|
|
+SWizard::Page()
|
|
.OnEnter(this, &SNewClassDialog::OnNamePageEntered)
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
// Title
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Font(FAppStyle::Get().GetFontStyle("HeadingExtraSmall"))
|
|
.Text( this, &SNewClassDialog::GetNameClassTitle )
|
|
.TransformPolicy(ETextTransformPolicy::ToUpper)
|
|
]
|
|
|
|
+SVerticalBox::Slot()
|
|
.FillHeight(1.f)
|
|
.Padding(0.0f, 10.0f)
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f, 0.0f, 0.0f, 5.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("ClassNameDescription", "Enter a name for your new class. Class names may only contain alphanumeric characters, and may not contain a space.") )
|
|
]
|
|
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f, 0.0f, 0.0f, 2.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text( ClassDomain == EClassDomain::Native ?
|
|
LOCTEXT("ClassNameDetails_Native", "When you click the \"Create\" button below, a header (.h) file and a source (.cpp) file will be made using this name.") :
|
|
FText::Format(LOCTEXT("ClassNameDetails_Blueprint", "When you click the \"Create\" button below, a new class inheriting from {0} will be created."), ParentClassInfo.IsSet() ? ParentClassInfo.GetClassName() : FText::FromStringView(TEXT("Blueprint")))
|
|
)
|
|
]
|
|
|
|
// Name Error label
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f, 5.0f)
|
|
[
|
|
SNew(SWarningOrErrorBox)
|
|
.MessageStyle(EMessageStyle::Error)
|
|
.Visibility(this, &SNewClassDialog::GetNameErrorLabelVisibility)
|
|
.Message(this, &SNewClassDialog::GetNameErrorLabelText)
|
|
]
|
|
|
|
// Properties
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop"))
|
|
.BorderBackgroundColor(FLinearColor(0.6f, 0.6f, 0.6f, 1.0f ))
|
|
.Padding(FMargin(6.0f, 4.0f, 7.0f, 4.0f))
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
.Padding(0.0f)
|
|
[
|
|
SNew(SGridPanel)
|
|
.FillColumn(1, 1.0f)
|
|
// Class type label
|
|
|
|
+ SGridPanel::Slot(0, 0)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.Text(LOCTEXT("ClassTypeLabel", "Class Type"))
|
|
]
|
|
|
|
+SGridPanel::Slot(1,0)
|
|
.VAlign(VAlign_Center)
|
|
.HAlign(HAlign_Left)
|
|
.Padding(2.0f)
|
|
[
|
|
SNew(SSegmentedControl<GameProjectUtils::EClassLocation>)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.OnValueChanged(this, &SNewClassDialog::OnClassLocationChanged)
|
|
.Value(this, &SNewClassDialog::IsClassLocationActive)
|
|
+ SSegmentedControl<GameProjectUtils::EClassLocation>::Slot(GameProjectUtils::EClassLocation::Public)
|
|
.Text(LOCTEXT("Public", "Public"))
|
|
.ToolTip(LOCTEXT("ClassLocation_Public", "A public class can be included and used inside other modules in addition to the module it resides in"))
|
|
+ SSegmentedControl<GameProjectUtils::EClassLocation>::Slot(GameProjectUtils::EClassLocation::Private)
|
|
.Text(LOCTEXT("Private", "Private"))
|
|
.ToolTip(LOCTEXT("ClassLocation_Private", "A private class can only be included and used within the module it resides in"))
|
|
]
|
|
// Name label
|
|
+SGridPanel::Slot(0, 1)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text( LOCTEXT( "NameLabel", "Name" ) )
|
|
]
|
|
|
|
// Name edit box
|
|
+SGridPanel::Slot(1, 1)
|
|
.Padding(0.0f, 3.0f)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SBox)
|
|
.HeightOverride(EditableTextHeight)
|
|
.AddMetaData<FTutorialMetaData>(TEXT("ClassName"))
|
|
[
|
|
SNew(SHorizontalBox)
|
|
|
|
+SHorizontalBox::Slot()
|
|
.FillWidth(.7f)
|
|
[
|
|
SAssignNew( ClassNameEditBox, SEditableTextBox)
|
|
.Text( this, &SNewClassDialog::OnGetClassNameText )
|
|
.OnTextChanged( this, &SNewClassDialog::OnClassNameTextChanged )
|
|
.OnTextCommitted( this, &SNewClassDialog::OnClassNameTextCommitted )
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.Padding(6.0f, 0.0f, 0.0f, 0.0f)
|
|
[
|
|
SAssignNew(AvailableModulesCombo, SComboBox<TSharedPtr<FModuleContextInfo>>)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.ToolTipText( LOCTEXT("ModuleComboToolTip", "Choose the target module for your new class") )
|
|
.OptionsSource( &AvailableModules )
|
|
.InitiallySelectedItem( SelectedModuleInfo )
|
|
.OnSelectionChanged( this, &SNewClassDialog::SelectedModuleComboBoxSelectionChanged )
|
|
.OnGenerateWidget( this, &SNewClassDialog::MakeWidgetForSelectedModuleCombo )
|
|
[
|
|
SNew(STextBlock)
|
|
.Text( this, &SNewClassDialog::GetSelectedModuleComboText )
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
// Path label
|
|
+SGridPanel::Slot(0, 2)
|
|
.VAlign(ClassDomain == EClassDomain::Blueprint ? VAlign_Top : VAlign_Center)
|
|
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text( LOCTEXT( "PathLabel", "Path" ) )
|
|
]
|
|
|
|
// Path edit box
|
|
+SGridPanel::Slot(1, 2)
|
|
.Padding(0.0f, 3.0f)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
// Blueprint Class asset path
|
|
+ SVerticalBox::Slot()
|
|
.Padding(0.0f)
|
|
[
|
|
SNew(SBox)
|
|
// Height override to force the visibility of a scrollbar (our parent is autoheight)
|
|
.HeightOverride(220.0f)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Visible : EVisibility::Collapsed)
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
+SVerticalBox::Slot()
|
|
.AutoHeight()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SNewClassDialog::OnGetClassPathText)
|
|
]
|
|
|
|
+SVerticalBox::Slot()
|
|
[
|
|
ContentBrowser.CreatePathPicker(BlueprintPathConfig)
|
|
]
|
|
]
|
|
]
|
|
|
|
// Native C++ path
|
|
+ SVerticalBox::Slot()
|
|
.Padding(0.0f)
|
|
.AutoHeight()
|
|
[
|
|
SNew(SBox)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.HeightOverride(EditableTextHeight)
|
|
.AddMetaData<FTutorialMetaData>(TEXT("Path"))
|
|
[
|
|
SNew(SHorizontalBox)
|
|
|
|
+SHorizontalBox::Slot()
|
|
.FillWidth(1.0f)
|
|
[
|
|
SNew(SEditableTextBox)
|
|
.Text(this, &SNewClassDialog::OnGetClassPathText)
|
|
.OnTextChanged(this, &SNewClassDialog::OnClassPathTextChanged)
|
|
]
|
|
|
|
+SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.Padding(6.0f, 1.0f, 0.0f, 0.0f)
|
|
[
|
|
SNew(SButton)
|
|
.VAlign(VAlign_Center)
|
|
.ButtonStyle(FAppStyle::Get(), "SimpleButton")
|
|
.OnClicked(this, &SNewClassDialog::HandleChooseFolderButtonClicked)
|
|
[
|
|
SNew(SImage)
|
|
.Image(FAppStyle::Get().GetBrush("Icons.FolderClosed"))
|
|
.ColorAndOpacity(FSlateColor::UseForeground())
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
|
|
// Header output label
|
|
+SGridPanel::Slot(0, 3)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.Text( LOCTEXT( "HeaderFileLabel", "Header File" ) )
|
|
]
|
|
|
|
// Header output text
|
|
+SGridPanel::Slot(1, 3)
|
|
.Padding(0.0f, 3.0f)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SBox)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.VAlign(VAlign_Center)
|
|
.HeightOverride(EditableTextHeight)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SNewClassDialog::OnGetClassHeaderFileText)
|
|
]
|
|
]
|
|
|
|
// Source output label
|
|
+SGridPanel::Slot(0, 4)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
|
|
[
|
|
SNew(STextBlock)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.Text( LOCTEXT( "SourceFileLabel", "Source File" ) )
|
|
]
|
|
|
|
// Source output text
|
|
+SGridPanel::Slot(1, 4)
|
|
.Padding(0.0f, 3.0f)
|
|
.VAlign(VAlign_Center)
|
|
[
|
|
SNew(SBox)
|
|
.Visibility(ClassDomain == EClassDomain::Blueprint ? EVisibility::Collapsed : EVisibility::Visible)
|
|
.VAlign(VAlign_Center)
|
|
.HeightOverride(EditableTextHeight)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(this, &SNewClassDialog::OnGetClassSourceFileText)
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
|
|
// Select the first item
|
|
if ( InArgs._Class == NULL && ParentClassItemsSource.Num() > 0 )
|
|
{
|
|
ParentClassListView->SetSelection(ParentClassItemsSource[0], ESelectInfo::Direct);
|
|
}
|
|
|
|
TSharedPtr<SWindow> ParentWindow = InArgs._ParentWindow;
|
|
if (ParentWindow.IsValid())
|
|
{
|
|
ParentWindow.Get()->SetWidgetToFocusOnActivate(ParentClassListView);
|
|
}
|
|
|
|
}
|
|
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
|
|
|
|
FReply SNewClassDialog::OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent)
|
|
{
|
|
if (InKeyEvent.GetKey() == EKeys::Escape)
|
|
{
|
|
// Pressing Escape returns as if the user clicked Cancel
|
|
CancelClicked();
|
|
return FReply::Handled();
|
|
}
|
|
else if (InKeyEvent.GetKey() == EKeys::Enter)
|
|
{
|
|
// Pressing Enter move to the next page like a double-click or the Next button
|
|
OnParentClassItemDoubleClicked(TSharedPtr<FParentClassItem>());
|
|
return FReply::Handled();
|
|
}
|
|
|
|
return FReply::Unhandled();
|
|
}
|
|
|
|
void SNewClassDialog::Tick( const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime )
|
|
{
|
|
// Every few seconds, the class name/path is checked for validity in case the disk contents changed and the location is now valid or invalid.
|
|
// After class creation, periodic checks are disabled to prevent a brief message indicating that the class you created already exists.
|
|
// This feature is re-enabled if the user did not restart and began editing parameters again.
|
|
if ( !bPreventPeriodicValidityChecksUntilNextChange && (InCurrentTime > LastPeriodicValidityCheckTime + PeriodicValidityCheckFrequency) )
|
|
{
|
|
UpdateInputValidity();
|
|
}
|
|
}
|
|
|
|
TSharedRef<ITableRow> SNewClassDialog::MakeParentClassListViewWidget(TSharedPtr<FParentClassItem> ParentClassItem, const TSharedRef<STableViewBase>& OwnerTable)
|
|
{
|
|
if ( !ensure(ParentClassItem.IsValid()) )
|
|
{
|
|
return SNew( STableRow<TSharedPtr<FParentClassItem>>, OwnerTable );
|
|
}
|
|
|
|
if ( !ParentClassItem->ParentClassInfo.IsSet() )
|
|
{
|
|
return SNew( STableRow<TSharedPtr<FParentClassItem>>, OwnerTable );
|
|
}
|
|
|
|
const FText ClassName = ParentClassItem->ParentClassInfo.GetClassName();
|
|
const FText ClassFullDescription = ParentClassItem->ParentClassInfo.GetClassDescription(/*bFullDescription*/true);
|
|
const FText ClassShortDescription = ParentClassItem->ParentClassInfo.GetClassDescription(/*bFullDescription*/false);
|
|
const UClass* Class = ParentClassItem->ParentClassInfo.BaseClass;
|
|
const FSlateBrush* const ClassBrush = FClassIconFinder::FindThumbnailForClass(Class);
|
|
|
|
const int32 ItemHeight = 64;
|
|
return
|
|
SNew( STableRow<TSharedPtr<FParentClassItem>>, OwnerTable )
|
|
.Padding(4.0f)
|
|
.Style(FAppStyle::Get(), "NewClassDialog.ParentClassListView.TableRow")
|
|
.ToolTip(IDocumentation::Get()->CreateToolTip(ClassFullDescription, nullptr, FEditorClassUtils::GetDocumentationPage(Class), FEditorClassUtils::GetDocumentationExcerpt(Class)))
|
|
[
|
|
SNew(SBox)
|
|
.HeightOverride(static_cast<float>(ItemHeight))
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot()
|
|
.AutoWidth()
|
|
.VAlign(VAlign_Center)
|
|
.HAlign(HAlign_Center)
|
|
.Padding(8.0f)
|
|
[
|
|
SNew(SBox)
|
|
.HeightOverride(ItemHeight / 2.0f)
|
|
.WidthOverride(ItemHeight / 2.0f)
|
|
[
|
|
SNew(SImage)
|
|
.Image(ClassBrush)
|
|
]
|
|
]
|
|
|
|
+ SHorizontalBox::Slot()
|
|
.FillWidth(1.0f)
|
|
.VAlign(VAlign_Center)
|
|
.Padding(4.0f)
|
|
[
|
|
SNew(SVerticalBox)
|
|
+SVerticalBox::Slot()
|
|
[
|
|
SNew(STextBlock)
|
|
.TextStyle(FAppStyle::Get(), "DialogButtonText")
|
|
.Text(ClassName)
|
|
]
|
|
|
|
+SVerticalBox::Slot()
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(ClassShortDescription)
|
|
.AutoWrapText(true)
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
FText SNewClassDialog::GetSelectedParentClassName() const
|
|
{
|
|
return ParentClassInfo.IsSet() ? ParentClassInfo.GetClassName() : FText::GetEmpty();
|
|
}
|
|
|
|
FString GetClassHeaderPath(const UClass* Class)
|
|
{
|
|
if (Class)
|
|
{
|
|
FString ClassHeaderPath;
|
|
if (FSourceCodeNavigation::FindClassHeaderPath(Class, ClassHeaderPath) && IFileManager::Get().FileSize(*ClassHeaderPath) != INDEX_NONE)
|
|
{
|
|
return ClassHeaderPath;
|
|
}
|
|
}
|
|
return FString();
|
|
}
|
|
|
|
EVisibility SNewClassDialog::GetSourceHyperlinkVisibility() const
|
|
{
|
|
if (ClassDomain == EClassDomain::Blueprint)
|
|
{
|
|
return EVisibility::Collapsed;
|
|
}
|
|
|
|
return (ParentClassInfo.GetBaseClassHeaderFilename().Len() > 0 ? EVisibility::Visible : EVisibility::Hidden);
|
|
}
|
|
|
|
FText SNewClassDialog::GetSelectedParentClassFilename() const
|
|
{
|
|
const FString ClassHeaderPath = ParentClassInfo.GetBaseClassHeaderFilename();
|
|
if (ClassHeaderPath.Len() > 0)
|
|
{
|
|
return FText::FromString(FPaths::GetCleanFilename(*ClassHeaderPath));
|
|
}
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
EVisibility SNewClassDialog::GetDocLinkVisibility() const
|
|
{
|
|
return (ParentClassInfo.BaseClass == nullptr || FEditorClassUtils::GetDocumentationLink(ParentClassInfo.BaseClass).IsEmpty() ? EVisibility::Hidden : EVisibility::Visible);
|
|
}
|
|
|
|
FString SNewClassDialog::GetSelectedParentDocLink() const
|
|
{
|
|
return FEditorClassUtils::GetDocumentationLink(ParentClassInfo.BaseClass);
|
|
}
|
|
|
|
void SNewClassDialog::OnEditCodeClicked()
|
|
{
|
|
const FString ClassHeaderPath = ParentClassInfo.GetBaseClassHeaderFilename();
|
|
if (ClassHeaderPath.Len() > 0)
|
|
{
|
|
const FString AbsoluteHeaderPath = IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*ClassHeaderPath);
|
|
FSourceCodeNavigation::OpenSourceFile(AbsoluteHeaderPath);
|
|
}
|
|
}
|
|
|
|
|
|
void SNewClassDialog::OnParentClassItemDoubleClicked( TSharedPtr<FParentClassItem> TemplateItem )
|
|
{
|
|
// Advance to the name page
|
|
const int32 NamePageIdx = 1;
|
|
if ( MainWizard->CanShowPage(NamePageIdx) )
|
|
{
|
|
MainWizard->ShowPage(NamePageIdx);
|
|
}
|
|
}
|
|
|
|
void SNewClassDialog::OnClassSelected(TSharedPtr<FParentClassItem> Item, ESelectInfo::Type SelectInfo)
|
|
{
|
|
if ( Item.IsValid() )
|
|
{
|
|
ClassViewer->ClearSelection();
|
|
ParentClassInfo = Item->ParentClassInfo;
|
|
}
|
|
else
|
|
{
|
|
ParentClassInfo = FNewClassInfo();
|
|
}
|
|
}
|
|
|
|
void SNewClassDialog::OnAdvancedClassSelected(UClass* Class)
|
|
{
|
|
ParentClassListView->ClearSelection();
|
|
ParentClassInfo = FNewClassInfo(Class);
|
|
}
|
|
|
|
bool SNewClassDialog::IsFullClassTreeShown() const
|
|
{
|
|
return bShowFullClassTree;
|
|
}
|
|
|
|
void SNewClassDialog::OnFullClassTreeChanged(bool bInShowFullClassTree)
|
|
{
|
|
bShowFullClassTree = bInShowFullClassTree;
|
|
}
|
|
|
|
EVisibility SNewClassDialog::GetBasicParentClassVisibility() const
|
|
{
|
|
return bShowFullClassTree ? EVisibility::Collapsed : EVisibility::Visible;
|
|
}
|
|
|
|
EVisibility SNewClassDialog::GetAdvancedParentClassVisibility() const
|
|
{
|
|
return bShowFullClassTree ? EVisibility::Visible : EVisibility::Collapsed;
|
|
}
|
|
|
|
EVisibility SNewClassDialog::GetNameErrorLabelVisibility() const
|
|
{
|
|
return GetNameErrorLabelText().IsEmpty() ? EVisibility::Hidden : EVisibility::Visible;
|
|
}
|
|
|
|
FText SNewClassDialog::GetNameErrorLabelText() const
|
|
{
|
|
if ( !bLastInputValidityCheckSuccessful )
|
|
{
|
|
return LastInputValidityErrorText;
|
|
}
|
|
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
EVisibility SNewClassDialog::GetGlobalErrorLabelVisibility() const
|
|
{
|
|
return GetGlobalErrorLabelText().IsEmpty() ? EVisibility::Collapsed : EVisibility::Visible;
|
|
}
|
|
|
|
FText SNewClassDialog::GetGlobalErrorLabelText() const
|
|
{
|
|
if ( ClassDomain == EClassDomain::Native && !FSourceCodeNavigation::IsCompilerAvailable() )
|
|
{
|
|
#if PLATFORM_LINUX
|
|
return FText::Format(LOCTEXT("NoCompilerFoundNewClassLinux", "Your IDE {0} is missing or incorrectly configured, please consider using {1}"),
|
|
FSourceCodeNavigation::GetSelectedSourceCodeIDE(), FSourceCodeNavigation::GetSuggestedSourceCodeIDE());
|
|
#else
|
|
return FText::Format(LOCTEXT("NoCompilerFoundNewClass", "No compiler was found. In order to use C++ code, you must first install {0}."), FSourceCodeNavigation::GetSuggestedSourceCodeIDE());
|
|
#endif
|
|
}
|
|
|
|
return FText::GetEmpty();
|
|
}
|
|
|
|
void SNewClassDialog::OnNamePageEntered()
|
|
{
|
|
// Set the default class name based on the selected parent class, eg MyActor
|
|
const FString ParentClassName = ParentClassInfo.GetClassNameCPP();
|
|
const FString PotentialNewClassName = FString::Printf(TEXT("%s%s"),
|
|
DefaultClassPrefix.IsEmpty() ? TEXT("My") : *DefaultClassPrefix,
|
|
DefaultClassName.IsEmpty() ? (ParentClassName.IsEmpty() ? TEXT("Class") : *ParentClassName) : *DefaultClassName);
|
|
|
|
// Only set the default if the user hasn't changed the class name from the previous default
|
|
if(LastAutoGeneratedClassName.IsEmpty() || NewClassName == LastAutoGeneratedClassName)
|
|
{
|
|
NewClassName = PotentialNewClassName;
|
|
LastAutoGeneratedClassName = PotentialNewClassName;
|
|
}
|
|
|
|
UpdateInputValidity();
|
|
|
|
// Steal keyboard focus to accelerate name entering
|
|
FSlateApplication::Get().SetKeyboardFocus(ClassNameEditBox, EFocusCause::SetDirectly);
|
|
}
|
|
|
|
FText SNewClassDialog::GetNameClassTitle() const
|
|
{
|
|
static const FString NoneString = TEXT("None");
|
|
|
|
const FText ParentClassName = GetSelectedParentClassName();
|
|
if(!ParentClassName.IsEmpty() && ParentClassName.ToString() != NoneString)
|
|
{
|
|
return FText::Format( LOCTEXT( "NameClassTitle", "Name Your New {0}" ), ParentClassName );
|
|
}
|
|
|
|
return LOCTEXT( "NameClassGenericTitle", "Name Your New Class" );
|
|
}
|
|
|
|
FText SNewClassDialog::OnGetClassNameText() const
|
|
{
|
|
return FText::FromString(NewClassName);
|
|
}
|
|
|
|
void SNewClassDialog::OnClassNameTextChanged(const FText& NewText)
|
|
{
|
|
NewClassName = NewText.ToString();
|
|
UpdateInputValidity();
|
|
}
|
|
|
|
void SNewClassDialog::OnClassNameTextCommitted(const FText& NewText, ETextCommit::Type CommitType)
|
|
{
|
|
if (CommitType == ETextCommit::OnEnter)
|
|
{
|
|
if (CanFinish())
|
|
{
|
|
FinishClicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
FText SNewClassDialog::OnGetClassPathText() const
|
|
{
|
|
return FText::FromString(NewClassPath);
|
|
}
|
|
|
|
void SNewClassDialog::OnClassPathTextChanged(const FText& NewText)
|
|
{
|
|
NewClassPath = NewText.ToString();
|
|
|
|
// If the user has selected a path which matches the root of a known module, then update our selected module to be that module
|
|
for(const auto& AvailableModule : AvailableModules)
|
|
{
|
|
if(NewClassPath.StartsWith(AvailableModule->ModuleSourcePath))
|
|
{
|
|
SelectedModuleInfo = AvailableModule;
|
|
AvailableModulesCombo->SetSelectedItem(SelectedModuleInfo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
UpdateInputValidity();
|
|
}
|
|
|
|
void SNewClassDialog::OnBlueprintPathSelected(const FString& NewPath)
|
|
{
|
|
IsBlueprintPathSelected = true;
|
|
NewClassPath = NewPath;
|
|
UpdateInputValidity();
|
|
}
|
|
|
|
FText SNewClassDialog::OnGetClassHeaderFileText() const
|
|
{
|
|
return FText::FromString(CalculatedClassHeaderName);
|
|
}
|
|
|
|
FText SNewClassDialog::OnGetClassSourceFileText() const
|
|
{
|
|
return FText::FromString(CalculatedClassSourceName);
|
|
}
|
|
|
|
void SNewClassDialog::CancelClicked()
|
|
{
|
|
CloseContainingWindow();
|
|
}
|
|
|
|
bool SNewClassDialog::CanFinish() const
|
|
{
|
|
return bLastInputValidityCheckSuccessful && ParentClassInfo.IsSet() && (ClassDomain == EClassDomain::Blueprint || FSourceCodeNavigation::IsCompilerAvailable()) && (ClassDomain != EClassDomain::Blueprint || IsBlueprintPathSelected);
|
|
}
|
|
|
|
void SNewClassDialog::FinishClicked()
|
|
{
|
|
check(CanFinish());
|
|
|
|
if (ClassDomain == EClassDomain::Blueprint)
|
|
{
|
|
FString PackagePath = NewClassPath / NewClassName;
|
|
|
|
if (!ParentClassInfo.BaseClass)
|
|
{
|
|
// @todo show fail reason in error label
|
|
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("AddCodeFailed_Blueprint_NoBase", "No parent class has been specified. Failed to generate new {0} class."), FText::FromString(NewClassName)));
|
|
}
|
|
else if (FindObject<UBlueprint>(nullptr, *PackagePath))
|
|
{
|
|
// @todo show fail reason in error label
|
|
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(LOCTEXT("AddCodeFailed_Blueprint_AlreadyExists", "The chosen class name ({0}) already exists, please try again with a different name."), FText::FromString(NewClassName)));
|
|
}
|
|
else if (!NewClassPath.IsEmpty() && !NewClassName.IsEmpty())
|
|
{
|
|
UPackage* Package = CreatePackage( *PackagePath);
|
|
if (Package)
|
|
{
|
|
// Create and init a new Blueprint
|
|
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(const_cast<UClass*>(ParentClassInfo.BaseClass), Package, FName(*NewClassName), BPTYPE_Normal);
|
|
if (NewBP)
|
|
{
|
|
// Set the default "AssetAccessSpecifier" state
|
|
Package->SetAssetAccessSpecifier(IAssetTools::Get().ShouldCreateAssetsAsExternallyReferenceableForPath(PackagePath) ? EAssetAccessSpecifier::Public : EAssetAccessSpecifier::Private);
|
|
|
|
// Notify the asset registry
|
|
FAssetRegistryModule::AssetCreated(NewBP);
|
|
|
|
// Mark the package dirty...
|
|
Package->MarkPackageDirty();
|
|
|
|
OnAddedToProject.ExecuteIfBound( NewClassName, PackagePath, FString() );
|
|
|
|
if (bSyncContentBrowserToNewClass)
|
|
{
|
|
// Sync the content browser to the new asset
|
|
GEditor->SyncBrowserToObject(NewBP);
|
|
}
|
|
|
|
// Open the editor for the new asset
|
|
GEditor->GetEditorSubsystem<UAssetEditorSubsystem>()->OpenEditorForAsset(NewBP);
|
|
|
|
// Successfully created the code and potentially opened the IDE. Close the dialog.
|
|
CloseContainingWindow();
|
|
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// @todo show fail reason in error label
|
|
// Failed to add blueprint
|
|
const FText Message = FText::Format( LOCTEXT("AddCodeFailed_Blueprint", "Failed to create package for class {0}. Please try again with a different name."), FText::FromString(NewClassName) );
|
|
FMessageDialog::Open(EAppMsgType::Ok, Message);
|
|
}
|
|
else
|
|
{
|
|
|
|
FString HeaderFilePath;
|
|
FString CppFilePath;
|
|
|
|
// Track the selected module name so we can default to this next time
|
|
LastSelectedModuleName = SelectedModuleInfo->ModuleName;
|
|
|
|
GameProjectUtils::EReloadStatus ReloadStatus;
|
|
FText FailReason;
|
|
const TSet<FString>& DisallowedHeaderNames = FSourceCodeNavigation::GetSourceFileDatabase().GetDisallowedHeaderNames();
|
|
const GameProjectUtils::EAddCodeToProjectResult AddCodeResult = GameProjectUtils::AddCodeToProject(NewClassName, NewClassPath, *SelectedModuleInfo, ParentClassInfo, DisallowedHeaderNames, HeaderFilePath, CppFilePath, FailReason, ReloadStatus);
|
|
if (AddCodeResult == GameProjectUtils::EAddCodeToProjectResult::Succeeded)
|
|
{
|
|
OnAddedToProject.ExecuteIfBound( NewClassName, NewClassPath, SelectedModuleInfo->ModuleName );
|
|
|
|
// Reload current project to take into account any new state
|
|
IProjectManager::Get().LoadProjectFile(FPaths::GetProjectFilePath());
|
|
|
|
// Prevent periodic validity checks. This is to prevent a brief error message about the class already existing while you are exiting.
|
|
bPreventPeriodicValidityChecksUntilNextChange = true;
|
|
|
|
// Display a nag if we didn't automatically hot-reload for the newly added class
|
|
bool bWasReloaded = ReloadStatus == GameProjectUtils::EReloadStatus::Reloaded;
|
|
|
|
if( bWasReloaded )
|
|
{
|
|
FNotificationInfo Notification( FText::Format( LOCTEXT("AddedClassSuccessNotification", "Added new class {0}"), FText::FromString(NewClassName) ) );
|
|
FSlateNotificationManager::Get().AddNotification( Notification );
|
|
}
|
|
|
|
if ( HeaderFilePath.IsEmpty() || CppFilePath.IsEmpty() || !FSlateApplication::Get().SupportsSourceAccess() )
|
|
{
|
|
if( !bWasReloaded )
|
|
{
|
|
// Code successfully added, notify the user. We are either running on a platform that does not support source access or a file was not given so don't ask about editing the file
|
|
const FText Message = FText::Format(
|
|
LOCTEXT("AddCodeSuccessWithHotReload", "Successfully added class '{0}', however you must recompile the '{1}' module before it will appear in the Content Browser.")
|
|
, FText::FromString(NewClassName), FText::FromString(SelectedModuleInfo->ModuleName) );
|
|
FMessageDialog::Open(EAppMsgType::Ok, Message);
|
|
}
|
|
else
|
|
{
|
|
// Code was added and hot reloaded into the editor, but the user doesn't have a code IDE installed so we can't open the file to edit it now
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool bEditSourceFilesNow = false;
|
|
if( bWasReloaded )
|
|
{
|
|
// Code was hot reloaded, so always edit the new classes now
|
|
bEditSourceFilesNow = true;
|
|
}
|
|
else
|
|
{
|
|
// Code successfully added, notify the user and ask about opening the IDE now
|
|
const FText Message = FText::Format(
|
|
LOCTEXT("AddCodeSuccessWithHotReloadAndSync", "Successfully added class '{0}', however you must recompile the '{1}' module before it will appear in the Content Browser.\n\nWould you like to edit the code now?")
|
|
, FText::FromString(NewClassName), FText::FromString(SelectedModuleInfo->ModuleName) );
|
|
bEditSourceFilesNow = ( FMessageDialog::Open( EAppMsgType::YesNo, Message ) == EAppReturnType::Yes );
|
|
}
|
|
|
|
if( bEditSourceFilesNow )
|
|
{
|
|
TArray<FString> SourceFiles;
|
|
SourceFiles.Add(IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*HeaderFilePath));
|
|
SourceFiles.Add(IFileManager::Get().ConvertToAbsolutePathForExternalAppForRead(*CppFilePath));
|
|
|
|
FSourceCodeNavigation::OpenSourceFiles(SourceFiles);
|
|
}
|
|
}
|
|
|
|
// Sync the content browser to the new class
|
|
UPackage* const ClassPackage = FindPackage(nullptr, *(FString("/Script/") + SelectedModuleInfo->ModuleName));
|
|
if ( ClassPackage )
|
|
{
|
|
UClass* const NewClass = static_cast<UClass*>(FindObjectWithOuter(ClassPackage, UClass::StaticClass(), *NewClassName));
|
|
if ( NewClass )
|
|
{
|
|
GEditor->SyncBrowserToObject(NewClass);
|
|
}
|
|
}
|
|
|
|
// Successfully created the code and potentially opened the IDE. Close the dialog.
|
|
CloseContainingWindow();
|
|
}
|
|
else if (AddCodeResult == GameProjectUtils::EAddCodeToProjectResult::FailedToHotReload)
|
|
{
|
|
OnAddedToProject.ExecuteIfBound( NewClassName, NewClassPath, SelectedModuleInfo->ModuleName );
|
|
|
|
// Prevent periodic validity checks. This is to prevent a brief error message about the class already existing while you are exiting.
|
|
bPreventPeriodicValidityChecksUntilNextChange = true;
|
|
|
|
// Failed to compile new code
|
|
const FText Message = FText::Format(
|
|
LOCTEXT("AddCodeFailed_HotReloadFailed", "Successfully added class '{0}', however you must recompile the '{1}' module before it will appear in the Content Browser. {2}\n\nWould you like to open the Output Log to see more details?")
|
|
, FText::FromString(NewClassName), FText::FromString(SelectedModuleInfo->ModuleName), FailReason );
|
|
if( FMessageDialog::Open(EAppMsgType::YesNo, Message) == EAppReturnType::Yes )
|
|
{
|
|
FGlobalTabmanager::Get()->TryInvokeTab(FName("OutputLog"));
|
|
}
|
|
|
|
// We did manage to add the code itself, so we can close the dialog.
|
|
CloseContainingWindow();
|
|
}
|
|
else
|
|
{
|
|
// @todo show fail reason in error label
|
|
// Failed to add code
|
|
const FText Message = FText::Format( LOCTEXT("AddCodeFailed_AddCodeFailed", "Failed to add class '{0}'. {1}"), FText::FromString(NewClassName), FailReason );
|
|
FMessageDialog::Open(EAppMsgType::Ok, Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
FReply SNewClassDialog::HandleChooseFolderButtonClicked()
|
|
{
|
|
IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get();
|
|
if ( DesktopPlatform )
|
|
{
|
|
TSharedPtr<SWindow> ParentWindow = FSlateApplication::Get().FindWidgetWindow(AsShared());
|
|
void* ParentWindowWindowHandle = (ParentWindow.IsValid()) ? ParentWindow->GetNativeWindow()->GetOSWindowHandle() : nullptr;
|
|
|
|
FString FolderName;
|
|
const FString Title = LOCTEXT("NewClassBrowseTitle", "Choose a source location").ToString();
|
|
const bool bFolderSelected = DesktopPlatform->OpenDirectoryDialog(
|
|
ParentWindowWindowHandle,
|
|
Title,
|
|
NewClassPath,
|
|
FolderName
|
|
);
|
|
|
|
if ( bFolderSelected )
|
|
{
|
|
if ( !FolderName.EndsWith(TEXT("/")) )
|
|
{
|
|
FolderName += TEXT("/");
|
|
}
|
|
|
|
NewClassPath = FolderName;
|
|
|
|
// If the user has selected a path which matches the root of a known module, then update our selected module to be that module
|
|
for(const auto& AvailableModule : AvailableModules)
|
|
{
|
|
if(NewClassPath.StartsWith(AvailableModule->ModuleSourcePath))
|
|
{
|
|
SelectedModuleInfo = AvailableModule;
|
|
AvailableModulesCombo->SetSelectedItem(SelectedModuleInfo);
|
|
break;
|
|
}
|
|
}
|
|
|
|
UpdateInputValidity();
|
|
}
|
|
}
|
|
|
|
return FReply::Handled();
|
|
}
|
|
|
|
FText SNewClassDialog::GetSelectedModuleComboText() const
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("ModuleName"), FText::FromString(SelectedModuleInfo->ModuleName));
|
|
Args.Add(TEXT("ModuleType"), FText::FromString(EHostType::ToString(SelectedModuleInfo->ModuleType)));
|
|
return FText::Format(LOCTEXT("ModuleComboEntry", "{ModuleName} ({ModuleType})"), Args);
|
|
}
|
|
|
|
void SNewClassDialog::SelectedModuleComboBoxSelectionChanged(TSharedPtr<FModuleContextInfo> Value, ESelectInfo::Type SelectInfo)
|
|
{
|
|
const FString& OldModulePath = SelectedModuleInfo->ModuleSourcePath;
|
|
const FString& NewModulePath = Value->ModuleSourcePath;
|
|
|
|
SelectedModuleInfo = Value;
|
|
|
|
// Update the class path to be rooted to the new module location
|
|
const FString AbsoluteClassPath = FPaths::ConvertRelativePathToFull(NewClassPath) / ""; // Ensure trailing /
|
|
if(AbsoluteClassPath.StartsWith(OldModulePath))
|
|
{
|
|
NewClassPath = AbsoluteClassPath.Replace(*OldModulePath, *NewModulePath);
|
|
}
|
|
|
|
UpdateInputValidity();
|
|
}
|
|
|
|
TSharedRef<SWidget> SNewClassDialog::MakeWidgetForSelectedModuleCombo(TSharedPtr<FModuleContextInfo> Value)
|
|
{
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("ModuleName"), FText::FromString(Value->ModuleName));
|
|
Args.Add(TEXT("ModuleType"), FText::FromString(EHostType::ToString(Value->ModuleType)));
|
|
return SNew(STextBlock)
|
|
.Text(FText::Format(LOCTEXT("ModuleComboEntry", "{ModuleName} ({ModuleType})"), Args));
|
|
}
|
|
|
|
FSlateColor SNewClassDialog::GetClassLocationTextColor(GameProjectUtils::EClassLocation InLocation) const
|
|
{
|
|
return (ClassLocation == InLocation) ? FSlateColor(FLinearColor(0, 0, 0)) : FSlateColor(FLinearColor(0.72f, 0.72f, 0.72f, 1.f));
|
|
}
|
|
|
|
GameProjectUtils::EClassLocation SNewClassDialog::IsClassLocationActive() const
|
|
{
|
|
return ClassLocation;
|
|
}
|
|
|
|
void SNewClassDialog::OnClassLocationChanged(GameProjectUtils::EClassLocation InLocation)
|
|
{
|
|
const FString AbsoluteClassPath = FPaths::ConvertRelativePathToFull(NewClassPath) / ""; // Ensure trailing /
|
|
|
|
GameProjectUtils::EClassLocation TmpClassLocation = GameProjectUtils::EClassLocation::UserDefined;
|
|
GameProjectUtils::GetClassLocation(AbsoluteClassPath, *SelectedModuleInfo, TmpClassLocation);
|
|
|
|
const FString RootPath = SelectedModuleInfo->ModuleSourcePath;
|
|
const FString PublicPath = RootPath / "Public" / ""; // Ensure trailing /
|
|
const FString PrivatePath = RootPath / "Private" / ""; // Ensure trailing /
|
|
|
|
// Update the class path to be rooted to the Public or Private folder based on InVisibility
|
|
switch (InLocation)
|
|
{
|
|
case GameProjectUtils::EClassLocation::Public:
|
|
if (AbsoluteClassPath.StartsWith(PrivatePath))
|
|
{
|
|
NewClassPath = AbsoluteClassPath.Replace(*PrivatePath, *PublicPath);
|
|
}
|
|
else if (AbsoluteClassPath.StartsWith(RootPath))
|
|
{
|
|
NewClassPath = AbsoluteClassPath.Replace(*RootPath, *PublicPath);
|
|
}
|
|
else
|
|
{
|
|
NewClassPath = PublicPath;
|
|
}
|
|
break;
|
|
|
|
case GameProjectUtils::EClassLocation::Private:
|
|
if (AbsoluteClassPath.StartsWith(PublicPath))
|
|
{
|
|
NewClassPath = AbsoluteClassPath.Replace(*PublicPath, *PrivatePath);
|
|
}
|
|
else if (AbsoluteClassPath.StartsWith(RootPath))
|
|
{
|
|
NewClassPath = AbsoluteClassPath.Replace(*RootPath, *PrivatePath);
|
|
}
|
|
else
|
|
{
|
|
NewClassPath = PrivatePath;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Will update ClassVisibility correctly
|
|
UpdateInputValidity();
|
|
}
|
|
|
|
void SNewClassDialog::UpdateInputValidity()
|
|
{
|
|
bLastInputValidityCheckSuccessful = true;
|
|
|
|
if (ClassDomain == EClassDomain::Blueprint)
|
|
{
|
|
bLastInputValidityCheckSuccessful = GameProjectUtils::IsValidClassNameForCreation(NewClassName, LastInputValidityErrorText);
|
|
ClassLocation = GameProjectUtils::EClassLocation::UserDefined;
|
|
if (bLastInputValidityCheckSuccessful)
|
|
{
|
|
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(AssetRegistryConstants::ModuleName).Get();
|
|
const FSoftObjectPath ObjectPath(NewClassPath / NewClassName + "." + NewClassName);
|
|
if (AssetRegistry.GetAssetByObjectPath(ObjectPath).IsValid())
|
|
{
|
|
bLastInputValidityCheckSuccessful = false;
|
|
LastInputValidityErrorText = FText::Format(LOCTEXT("AssetAlreadyExists", "An asset called {0} already exists in {1}."), FText::FromString(NewClassName), FText::FromString(NewClassPath));
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Validate the path first since this has the side effect of updating the UI
|
|
bLastInputValidityCheckSuccessful = GameProjectUtils::CalculateSourcePaths(NewClassPath, *SelectedModuleInfo, CalculatedClassHeaderName, CalculatedClassSourceName, &LastInputValidityErrorText);
|
|
CalculatedClassHeaderName /= ParentClassInfo.GetHeaderFilename(NewClassName);
|
|
CalculatedClassSourceName /= ParentClassInfo.GetSourceFilename(NewClassName);
|
|
|
|
// If the source paths check as succeeded, check to see if we're using a Public/Private class
|
|
if(bLastInputValidityCheckSuccessful)
|
|
{
|
|
GameProjectUtils::GetClassLocation(NewClassPath, *SelectedModuleInfo, ClassLocation);
|
|
|
|
// We only care about the Public and Private folders
|
|
if(ClassLocation != GameProjectUtils::EClassLocation::Public && ClassLocation != GameProjectUtils::EClassLocation::Private)
|
|
{
|
|
ClassLocation = GameProjectUtils::EClassLocation::UserDefined;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClassLocation = GameProjectUtils::EClassLocation::UserDefined;
|
|
}
|
|
|
|
// Validate the class name only if the path is valid
|
|
if ( bLastInputValidityCheckSuccessful )
|
|
{
|
|
const TSet<FString>& DisallowedHeaderNames = FSourceCodeNavigation::GetSourceFileDatabase().GetDisallowedHeaderNames();
|
|
bLastInputValidityCheckSuccessful = GameProjectUtils::IsValidClassNameForCreation(NewClassName, *SelectedModuleInfo, DisallowedHeaderNames, LastInputValidityErrorText);
|
|
}
|
|
|
|
// Validate that the class is valid for the currently selected module
|
|
// As a project can have multiple modules, this lets us update the class validity as the user changes the target module
|
|
if ( bLastInputValidityCheckSuccessful && ParentClassInfo.BaseClass )
|
|
{
|
|
bLastInputValidityCheckSuccessful = GameProjectUtils::IsValidBaseClassForCreation(ParentClassInfo.BaseClass, *SelectedModuleInfo);
|
|
if ( !bLastInputValidityCheckSuccessful )
|
|
{
|
|
LastInputValidityErrorText = FText::Format(
|
|
LOCTEXT("NewClassError_InvalidBaseClassForModule", "{0} cannot be used as a base class in the {1} module. Please make sure that {0} is API exported."),
|
|
FText::FromString(ParentClassInfo.BaseClass->GetName()),
|
|
FText::FromString(SelectedModuleInfo->ModuleName)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
LastPeriodicValidityCheckTime = FSlateApplication::Get().GetCurrentTime();
|
|
|
|
// Since this function was invoked, periodic validity checks should be re-enabled if they were disabled.
|
|
bPreventPeriodicValidityChecksUntilNextChange = false;
|
|
}
|
|
|
|
const FNewClassInfo& SNewClassDialog::GetSelectedParentClassInfo() const
|
|
{
|
|
return ParentClassInfo;
|
|
}
|
|
|
|
void SNewClassDialog::SetupParentClassItems(const TArray<FNewClassInfo>& UserSpecifiedFeaturedClasses)
|
|
{
|
|
TArray<FNewClassInfo> DefaultFeaturedClasses;
|
|
const TArray<FNewClassInfo>* ArrayToUse = &UserSpecifiedFeaturedClasses;
|
|
|
|
// Setup the featured classes list
|
|
if (ArrayToUse->Num() == 0)
|
|
{
|
|
DefaultFeaturedClasses = ClassDomain == EClassDomain::Native ? FFeaturedClasses::AllNativeClasses() : FFeaturedClasses::ActorClasses();
|
|
ArrayToUse = &DefaultFeaturedClasses;
|
|
}
|
|
|
|
for (const auto& Featured : *ArrayToUse)
|
|
{
|
|
ParentClassItemsSource.Add( MakeShareable( new FParentClassItem(Featured) ) );
|
|
}
|
|
}
|
|
|
|
void SNewClassDialog::CloseContainingWindow()
|
|
{
|
|
TSharedPtr<SWindow> ContainingWindow = FSlateApplication::Get().FindWidgetWindow(AsShared());
|
|
|
|
if ( ContainingWindow.IsValid() )
|
|
{
|
|
ContainingWindow->RequestDestroyWindow();
|
|
}
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|